""" app/routers/chat.py — RAG Endpunkt (WP-05) Zweck: Verbindet Retrieval (WP-04) mit LLM-Generation (WP-05). 1. Empfängt User-Frage. 2. Sucht relevante Chunks (Retriever). 3. Baut Kontext-String. 4. Generiert Antwort via Ollama. Version: 0.1.0 """ from fastapi import APIRouter, HTTPException, Depends from typing import List import time import uuid import logging from app.models.dto import ChatRequest, ChatResponse, QueryRequest, QueryHit from app.services.llm_service import LLMService # Annahme: Der Retriever aus WP-04 liegt hier. # Falls Import-Fehler: Bitte Pfad prüfen (z.B. app.services.retriever oder app.core.retriever) from app.core.retriever import Retriever router = APIRouter() logger = logging.getLogger(__name__) # Dependency für Services (Singletons oder Factory wäre sauberer, hier pragmatisch instanziiert) def get_llm_service(): return LLMService() def get_retriever(): return Retriever() def _build_context_from_hits(hits: List[QueryHit]) -> str: """ Formatiert die Suchtreffer zu einem String für den Prompt. Extrahiert Text aus hit.source (wo der Chunk-Inhalt liegt). """ context_parts = [] for i, hit in enumerate(hits, 1): # Wir versuchen, den Text aus verschiedenen gängigen Feldern zu holen source = hit.source or {} content = source.get("text") or source.get("content") or "No text content available." title = hit.note_id or "Unknown Note" # Formatierung: # [1] Titel der Notiz (Score: 0.85) # Inhalt... entry = ( f"SOURCE [{i}]: {title} (Score: {hit.total_score:.2f})\n" f"CONTENT: {content}\n" ) context_parts.append(entry) return "\n---\n".join(context_parts) @router.post("/", response_model=ChatResponse) async def chat_endpoint( request: ChatRequest, llm: LLMService = Depends(get_llm_service), retriever: Retriever = Depends(get_retriever) ): start_time = time.time() query_id = str(uuid.uuid4()) logger.info(f"Chat request [{query_id}]: {request.message}") try: # 1. Retrieval: Wir nutzen den existierenden Retriever # Wir mappen ChatRequest auf QueryRequest (WP-04 Logik) query_req = QueryRequest( query=request.message, mode="hybrid", # Hybrid ist am robustesten für RAG top_k=request.top_k, explain=request.explain # Traceability weitergeben ) # Retrieval ausführen (retriever.search erwartet QueryRequest) # Hinweis: retrieve_result ist vom Typ QueryResponse (aus DTO) retrieve_result = await retriever.search(query_req) hits = retrieve_result.results # 2. Kontext bauen if not hits: logger.info(f"[{query_id}] No hits found for context.") context_str = "Keine relevanten Notizen gefunden." else: context_str = _build_context_from_hits(hits) # 3. LLM Generation logger.info(f"[{query_id}] Generating answer with {len(hits)} context chunks...") answer_text = await llm.generate_rag_response( query=request.message, context_str=context_str ) # 4. Response bauen duration_ms = int((time.time() - start_time) * 1000) return ChatResponse( query_id=retrieve_result.query_id, # Wir nutzen die ID vom Retriever für Konsistenz answer=answer_text, sources=hits, latency_ms=duration_ms ) except Exception as e: logger.error(f"Error in chat endpoint: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e))