""" app/services/feedback_service.py Service zum Loggen von Suchanfragen und Feedback (WP-04c). Speichert Daten als JSONL für späteres Self-Tuning (WP-08). Version: 1.1 (Chat-Support) """ import json import os import time import logging from pathlib import Path from typing import Dict, Any, List, Union from app.models.dto import QueryRequest, QueryResponse, FeedbackRequest, QueryHit logger = logging.getLogger(__name__) # Pfad für Logs (lokal auf dem Beelink/PC) LOG_DIR = Path("data/logs") SEARCH_LOG_FILE = LOG_DIR / "search_history.jsonl" FEEDBACK_LOG_FILE = LOG_DIR / "feedback.jsonl" def _ensure_log_dir(): if not LOG_DIR.exists(): os.makedirs(LOG_DIR, exist_ok=True) def _append_jsonl(file_path: Path, data: dict): try: with open(file_path, "a", encoding="utf-8") as f: f.write(json.dumps(data, ensure_ascii=False) + "\n") except Exception as e: logger.error(f"Failed to write log: {e}") def log_search( query_id: str, query_text: str, results: List[QueryHit], mode: str = "unknown", metadata: Dict[str, Any] = None ): """ Generische Logging-Funktion für Suche UND Chat. Args: query_id: UUID der Anfrage. query_text: User-Eingabe. results: Liste der Treffer (QueryHit Objekte). mode: z.B. "semantic", "hybrid", "chat_rag". metadata: Zusätzliche Infos (z.B. generierte Antwort, Intent). """ _ensure_log_dir() hits_summary = [] for hit in results: # Pydantic Model Dump für saubere Serialisierung breakdown = None if hit.explanation and hit.explanation.breakdown: breakdown = hit.explanation.breakdown.model_dump() hits_summary.append({ "node_id": hit.node_id, "note_id": hit.note_id, "total_score": hit.total_score, "breakdown": breakdown, "rank_semantic": hit.semantic_score, "rank_edge": hit.edge_bonus, "type": hit.source.get("type") if hit.source else "unknown" }) entry = { "timestamp": time.time(), "query_id": query_id, "query_text": query_text, "mode": mode, "hits_count": len(hits_summary), "hits": hits_summary, "metadata": metadata or {} } _append_jsonl(SEARCH_LOG_FILE, entry) logger.info(f"Logged search/chat interaction {query_id}") def log_feedback(fb: FeedbackRequest): """ Speichert das User-Feedback. """ _ensure_log_dir() entry = { "timestamp": time.time(), "query_id": fb.query_id, "node_id": fb.node_id, "score": fb.score, "comment": fb.comment } _append_jsonl(FEEDBACK_LOG_FILE, entry) logger.info(f"Logged feedback for {fb.query_id}")