diff --git a/app/models/dto.py b/app/models/dto.py new file mode 100644 index 0000000..d767e38 --- /dev/null +++ b/app/models/dto.py @@ -0,0 +1,94 @@ +""" +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, ohne bestehende Funktionen zu ändern. +Kompatibilität: + Python 3.12+, Pydantic 2.x, FastAPI 0.110+ +Version: + 0.1.0 (Erstanlage für WP-04) +Stand: + 2025-10-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 +Änderungsverlauf: + 0.1.0 (2025-10-07) – Erstanlage. +""" + +from __future__ import annotations +from pydantic import BaseModel +from typing import List, Literal, Optional, Dict + +EdgeKind = Literal["references", "references_at", "backlink", "next", "prev", "belongs_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: EdgeKind + 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 + """ + 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"]} + filters: Optional[Dict] = None + ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True} + + +class QueryHit(BaseModel): + """Einzelnes Trefferobjekt für /query.""" + node_id: str + note_id: Optional[str] + semantic_score: float + edge_bonus: float + centrality_bonus: float + total_score: float + paths: Optional[List[List[Dict]]] = None + source: Optional[Dict] = 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]