initial version
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from typing import Literal
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||
|
||||
ai_mode: Literal["online", "offline"] = "offline"
|
||||
|
||||
# Online (Claude) settings
|
||||
anthropic_api_key: str = ""
|
||||
claude_model: str = "claude-sonnet-4-6"
|
||||
|
||||
# Offline (Ollama) settings
|
||||
ollama_host: str = "http://localhost:11434"
|
||||
ollama_model: str = "llama3.2:3b"
|
||||
|
||||
cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"]
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,32 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api.services.ai import chat
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
messages: list[Message]
|
||||
context: dict | None = None
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
reply: str
|
||||
|
||||
|
||||
@router.post("/chat", response_model=ChatResponse)
|
||||
async def chat_endpoint(request: ChatRequest):
|
||||
try:
|
||||
reply = await chat(
|
||||
messages=[m.model_dump() for m in request.messages],
|
||||
context=request.context,
|
||||
)
|
||||
return ChatResponse(reply=reply)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,50 @@
|
||||
from api.config import settings
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """You are a friendly, beginner-aware music theory assistant for WhatTheFlat — a live jam helper app.
|
||||
You help musicians understand what's happening harmonically during a jam session.
|
||||
Keep answers short, warm, and jargon-free. If you use a music term, briefly explain it.
|
||||
Never say someone played a "wrong" note — always frame it as "try these instead".
|
||||
When given context about the current key and chord, use it in your response."""
|
||||
|
||||
|
||||
async def chat(messages: list[dict], context: dict | None = None) -> str:
|
||||
system = SYSTEM_PROMPT
|
||||
if context:
|
||||
parts = []
|
||||
if context.get("key"):
|
||||
parts.append(f"Current key: {context['key']}")
|
||||
if context.get("chord"):
|
||||
parts.append(f"Current chord: {context['chord']}")
|
||||
if parts:
|
||||
system += "\n\nLive session context:\n" + "\n".join(parts)
|
||||
|
||||
if settings.ai_mode == "online":
|
||||
return await _chat_claude(messages, system)
|
||||
else:
|
||||
return await _chat_ollama(messages, system)
|
||||
|
||||
|
||||
async def _chat_claude(messages: list[dict], system: str) -> str:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
response = await client.messages.create(
|
||||
model=settings.claude_model,
|
||||
max_tokens=512,
|
||||
system=system,
|
||||
messages=messages,
|
||||
)
|
||||
return response.content[0].text
|
||||
|
||||
|
||||
async def _chat_ollama(messages: list[dict], system: str) -> str:
|
||||
import ollama
|
||||
|
||||
client = ollama.AsyncClient(host=settings.ollama_host)
|
||||
full_messages = [{"role": "system", "content": system}] + messages
|
||||
response = await client.chat(
|
||||
model=settings.ollama_model,
|
||||
messages=full_messages,
|
||||
)
|
||||
return response.message.content
|
||||
Reference in New Issue
Block a user