""" app/models/dto.py — Pydantic-Modelle (DTOs) für WP-04 Endpunkte Zweck: Laufzeit-Modelle für FastAPI (Requests/Responses), getrennt von JSON-Schemas. Deckt die Graph-/Retriever-Endpunkte ab. Enthält Erweiterungen für WP-04b (Explanation Layer). Kompatibilität: Python 3.12+, Pydantic 2.x, FastAPI 0.110+ Version: 0.2.0 (Update für WP-04b Explanation Layer) Stand: 2025-12-07 Bezug: - schemas/*.json (Speicherschema für Notes/Chunks/Edges) - WP-04 API-Design (Query- und Graph-Endpunkte) Nutzung: from app.models.dto import QueryRequest, QueryResponse, GraphResponse """ from __future__ import annotations from pydantic import BaseModel, Field from typing import List, Literal, Optional, Dict, Any EdgeKind = Literal["references", "references_at", "backlink", "next", "prev", "belongs_to", "depends_on", "related_to", "similar_to"] class NodeDTO(BaseModel): """Darstellung eines Knotens (Note oder Chunk) im API-Graph.""" id: str type: Literal["note", "chunk"] title: Optional[str] = None note_id: Optional[str] = None tags: Optional[List[str]] = None in_degree: Optional[int] = None out_degree: Optional[int] = None score: Optional[float] = None section_title: Optional[str] = None section_path: Optional[str] = None path: Optional[str] = None class EdgeDTO(BaseModel): """Darstellung einer Kante im API-Graph.""" id: str kind: str # String statt Literal, um flexibel für Custom-Types zu bleiben source: str target: str weight: float direction: Literal["out", "in", "undirected"] = "out" class QueryRequest(BaseModel): """ Request für /query: - mode: 'semantic' | 'edge' | 'hybrid' - query: (optional) Freitext; Embedding wird später angebunden - query_vector: (optional) direkter Vektor (384d) für Quick-Tests ohne Embedding - explain: (optional) Fordert detaillierte Erklärungen an (WP-04b) """ mode: Literal["semantic", "edge", "hybrid"] = "hybrid" query: Optional[str] = None query_vector: Optional[List[float]] = None top_k: int = 10 expand: Dict = {"depth": 1, "edge_types": ["references", "belongs_to", "prev", "next", "depends_on", "related_to"]} filters: Optional[Dict] = None # Flags zur Steuerung der Rückgabe ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True} # WP-04b: Soll eine Erklärung generiert werden? explain: bool = False # --- WP-04b Explanation Models --- class ScoreBreakdown(BaseModel): """ Aufschlüsselung der Score-Komponenten. Zeigt die gewichteten Beiträge zum Total Score. """ semantic_contribution: float = Field(..., description="W_sem * semantic_score * weight") edge_contribution: float = Field(..., description="W_edge * edge_bonus") centrality_contribution: float = Field(..., description="W_cent * centrality_bonus") # Rohwerte für Transparenz raw_semantic: float raw_edge_bonus: float raw_centrality: float node_weight: float = Field(..., description="Typ-Gewicht (retriever_weight)") class Reason(BaseModel): """ Ein semantischer Grund für das Ranking. z.B. 'Verlinkt von Projekt X', 'Hohe Textähnlichkeit'. """ kind: Literal["semantic", "edge", "type", "centrality"] message: str score_impact: Optional[float] = None details: Optional[Dict[str, Any]] = None class Explanation(BaseModel): """ Container für alle Erklärungsdaten eines Treffers. """ breakdown: ScoreBreakdown reasons: List[Reason] # Optional: Pfade im Graphen, die zu diesem Treffer geführt haben related_edges: Optional[List[EdgeDTO]] = None # --- End Explanation Models --- class QueryHit(BaseModel): """Einzelnes Trefferobjekt für /query.""" node_id: str note_id: Optional[str] # Flache Scores (Kompatibilität WP-04a) semantic_score: float edge_bonus: float centrality_bonus: float total_score: float paths: Optional[List[List[Dict]]] = None source: Optional[Dict] = None # WP-04b: Erklärungsobjekt (nur gefüllt, wenn explain=True) explanation: Optional[Explanation] = None class QueryResponse(BaseModel): """Antwortstruktur für /query (Liste von Treffern + Telemetrie).""" results: List[QueryHit] used_mode: str latency_ms: int class GraphResponse(BaseModel): """Antwortstruktur für /graph/{note_id} (Nachbarschaft).""" center_note_id: str nodes: List[NodeDTO] edges: List[EdgeDTO] stats: Dict[str, int]