AIdea-Server / src /api /chat_routes.py
Ali Hashhash
feat: implement chat API routes, note generation logic, and model loading utilities
3bc6c02
Raw
History Blame Contribute Delete
4.45 kB
"""
Chat routes โ€” Document-specific Q&A powered by local Qwen model.
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from src.auth.dependencies import get_current_user
from src.db.models import User
from src.summarization.note_generator import NoteGenerator
from src.utils.logger import setup_logger
from src.utils.model_loader import INFERENCE_MODE, get_groq_client
logger = setup_logger(__name__)
router = APIRouter(tags=["Chat"])
# โ”€โ”€โ”€ Schemas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class ChatMessage(BaseModel):
role: str = Field(..., description="Either 'user' or 'assistant'")
content: str = Field(..., description="Message content")
class ChatRequest(BaseModel):
note_content: str = Field(
...,
description="The full text of the note to use as context",
)
question: str = Field(
...,
min_length=1,
description="The user's question about the note",
)
history: Optional[List[ChatMessage]] = Field(
default=None,
description="Previous conversation turns for multi-turn context",
)
class ChatResponse(BaseModel):
answer: str = Field(..., description="AI-generated answer")
# โ”€โ”€โ”€ Endpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@router.post("/chat/note", response_model=ChatResponse)
async def chat_with_note(
request: ChatRequest,
current_user: User = Depends(get_current_user),
):
"""Ask a question about a specific note. Answers are grounded in the note content."""
logger.info(
"Chat request from user %s โ€” question length: %d, context length: %d",
current_user.id,
len(request.question),
len(request.note_content),
)
if not request.note_content.strip():
raise HTTPException(status_code=400, detail="Note content cannot be empty.")
history_dicts = None
if request.history:
history_dicts = [msg.model_dump() for msg in request.history]
if INFERENCE_MODE == "groq":
logger.info("๐ŸŸข Chat request routed directly to Groq API (llama-3.3-70b-versatile)...")
messages = [
{
"role": "system",
"content": (
"You are a helpful study assistant. "
"Answer the user's question based ONLY on the note content provided below. "
"If the answer is not in the note, say so honestly. "
"Reply in the same language the user uses.\n\n"
f"--- NOTE CONTENT ---\n{request.note_content[:4000]}\n--- END NOTE ---"
),
},
]
if history_dicts:
for msg in history_dicts[-6:]:
role = msg.get("role", "user")
content = msg.get("content", "")
if role in ("user", "assistant") and content:
messages.append({"role": role, "content": content})
messages.append({"role": "user", "content": request.question})
groq_client = get_groq_client()
try:
chat_completion = groq_client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
max_tokens=300,
temperature=0.0,
)
answer = chat_completion.choices[0].message.content or ""
answer = answer.strip()
if not answer or len(answer) < 3:
answer = "ุนุฐุฑู‹ุงุŒ ู„ู… ุฃุชู…ูƒู† ู…ู† ุชูˆู„ูŠุฏ ุฅุฌุงุจุฉ. ูŠุฑุฌู‰ ุฅุนุงุฏุฉ ุตูŠุงุบุฉ ุงู„ุณุคุงู„."
except Exception as e:
logger.error("โŒ Groq chat error: %s", e, exc_info=True)
answer = "ุญุฏุซ ุฎุทุฃ ุฃุซู†ุงุก ู…ุนุงู„ุฌุฉ ุณุคุงู„ูƒ. ูŠุฑุฌู‰ ุงู„ู…ุญุงูˆู„ุฉ ู…ุฑุฉ ุฃุฎุฑู‰."
else:
logger.info("๐Ÿค– Chat request routed to local Qwen pipeline...")
note_gen = NoteGenerator()
answer = note_gen.chat_with_note(
note_content=request.note_content,
question=request.question,
history=history_dicts,
)
return ChatResponse(answer=answer)