mindnet/app/models/dto.py
2025-12-07 19:29:48 +01:00

148 lines
4.4 KiB
Python

"""
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) und WP-04c (Feedback).
Kompatibilität:
Python 3.12+, Pydantic 2.x, FastAPI 0.110+
Version:
0.3.0 (Update für WP-04c Feedback)
Stand:
2025-12-07
"""
from __future__ import annotations
from pydantic import BaseModel, Field
from typing import List, Literal, Optional, Dict, Any
import uuid
EdgeKind = Literal["references", "references_at", "backlink", "next", "prev", "belongs_to", "depends_on", "related_to", "similar_to"]
# --- Basis-DTOs ---
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"
# --- Request Models ---
class QueryRequest(BaseModel):
"""
Request für /query:
- mode: 'semantic' | 'edge' | 'hybrid'
- query: (optional) Freitext
- query_vector: (optional) direkter Vektor
- 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
ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True}
explain: bool = False
class FeedbackRequest(BaseModel):
"""
User-Feedback zu einem spezifischen Treffer (WP-04c).
"""
query_id: str = Field(..., description="ID der ursprünglichen Suche")
node_id: str = Field(..., description="ID des bewerteten Treffers")
score: int = Field(..., ge=0, le=1, description="1 (Positiv) oder 0 (Negativ/Irrelevant)")
comment: Optional[str] = None
# --- WP-04b Explanation Models ---
class ScoreBreakdown(BaseModel):
"""Aufschlüsselung der Score-Komponenten."""
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
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."""
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]
related_edges: Optional[List[EdgeDTO]] = None
# --- Response Models ---
class QueryHit(BaseModel):
"""Einzelnes Trefferobjekt für /query."""
node_id: str
note_id: Optional[str]
# Flache Scores
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
explanation: Optional[Explanation] = None
class QueryResponse(BaseModel):
"""
Antwortstruktur für /query (Liste von Treffern + Telemetrie).
Enthält query_id für Traceability (WP-04c).
"""
query_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
results: List[QueryHit]
used_mode: str
latency_ms: int
class GraphResponse(BaseModel):
"""Antwortstruktur für /graph/{note_id}."""
center_note_id: str
nodes: List[NodeDTO]
edges: List[EdgeDTO]
stats: Dict[str, int]