""" 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)