""" app/routers/chat.py — RAG Endpunkt (WP-05 Final) Zweck: Verbindet Retrieval mit LLM-Generation. Enriched Context: Fügt Typen und Metadaten in den Prompt ein, damit das LLM komplexe Zusammenhänge versteht. Version: 0.3.0 (Audit Finalization) """ 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 from app.core.retriever import Retriever router = APIRouter() logger = logging.getLogger(__name__) def get_llm_service(): return LLMService() def get_retriever(): return Retriever() def _build_enriched_context(hits: List[QueryHit]) -> str: """ Baut einen 'Rich Context' String. Statt nur Text, injizieren wir Metadaten (Typ, Tags), damit das LLM die semantische Rolle des Schnipsels versteht. """ context_parts = [] for i, hit in enumerate(hits, 1): source = hit.source or {} # 1. Content extrahieren (Robust) content = ( source.get("text") or source.get("content") or source.get("page_content") or source.get("chunk_text") or "[Kein Textinhalt verfügbar]" ) # 2. Metadaten für "Context Intelligence" title = hit.note_id or "Unbekannte Notiz" # Versuche, den Typ aus dem Payload zu lesen (wichtig für Decision/Project Unterscheidung) # In WP-03 Import landen diese Infos oft in 'metadata' oder direkt im Payload root. # Wir schauen defensiv an beiden Orten. note_type = source.get("type", "unknown").upper() tags = source.get("tags", []) if isinstance(tags, list): tags_str = ", ".join(tags[:3]) # Nur die ersten 3 Tags else: tags_str = str(tags) # 3. Formatierung für das LLM # Wir nutzen ein Format, das wie ein strukturiertes Dokument aussieht. # Das hilft dem Modell, Grenzen zwischen Quellen zu erkennen. entry = ( f"### SOURCE [{i}]: {title}\n" f"METADATA: [TYPE: {note_type}] [SCORE: {hit.total_score:.2f}] [TAGS: {tags_str}]\n" f"CONTENT:\n{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()) # Logging verkürzt für Übersichtlichkeit logger.info(f"Chat request [{query_id}]: {request.message[:50]}...") try: # 1. Retrieval (Graph-Awareness) # Wir erzwingen 'hybrid', damit graph-basierte Nachbarn gefunden werden. query_req = QueryRequest( query=request.message, mode="hybrid", # <--- Audit Check: Hybrid Mode active top_k=request.top_k, explain=request.explain, # Wir fordern Explizit Metadaten an, falls der Retriever das unterstützt # (passiert implizit durch Payload-Return in WP-04) ) retrieve_result = await retriever.search(query_req) hits = retrieve_result.results # 2. Context Assembly if not hits: logger.info(f"[{query_id}] No hits found.") context_str = "Keine relevanten Notizen gefunden." else: context_str = _build_enriched_context(hits) # 3. Generation logger.info(f"[{query_id}] Context built with {len(hits)} chunks. Sending to LLM...") answer_text = await llm.generate_rag_response( query=request.message, context_str=context_str ) # 4. Response duration_ms = int((time.time() - start_time) * 1000) logger.info(f"[{query_id}] Completed in {duration_ms}ms") return ChatResponse( query_id=retrieve_result.query_id, 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))