mindnet/app/routers/chat.py
2025-12-08 13:39:16 +01:00

131 lines
4.3 KiB
Python

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