WP04c
This commit is contained in:
parent
d49da1b5fe
commit
caab41a760
18
app/main.py
18
app/main.py
|
|
@ -1,43 +1,43 @@
|
|||
"""
|
||||
app/main.py — mindnet API bootstrap (WP-04 Hooks)
|
||||
Version: 0.4.2 • Stand: 2025-10-07
|
||||
app/main.py — mindnet API bootstrap
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from fastapi import FastAPI
|
||||
from .config import get_settings
|
||||
from .routers.embed_router import router as embed_router
|
||||
from .routers.qdrant_router import router as qdrant_router
|
||||
|
||||
# WP-04 Router:
|
||||
from .routers.query import router as query_router
|
||||
from .routers.graph import router as graph_router
|
||||
from .routers.tools import router as tools_router
|
||||
# Optional:
|
||||
# NEU: Feedback Router
|
||||
from .routers.feedback import router as feedback_router
|
||||
|
||||
try:
|
||||
from .routers.admin import router as admin_router
|
||||
except Exception:
|
||||
admin_router = None
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(title="mindnet API", version="0.1.0")
|
||||
app = FastAPI(title="mindnet API", version="0.4.3") # Version bump
|
||||
s = get_settings()
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz():
|
||||
return {"status": "ok", "qdrant": s.QDRANT_URL, "prefix": s.COLLECTION_PREFIX}
|
||||
|
||||
# Bestehende Router (unverändert)
|
||||
app.include_router(embed_router)
|
||||
app.include_router(qdrant_router)
|
||||
|
||||
# WP-04 Endpunkte
|
||||
app.include_router(query_router, prefix="/query", tags=["query"])
|
||||
app.include_router(graph_router, prefix="/graph", tags=["graph"])
|
||||
app.include_router(tools_router, prefix="/tools", tags=["tools"])
|
||||
# NEU:
|
||||
app.include_router(feedback_router, prefix="/feedback", tags=["feedback"])
|
||||
|
||||
if admin_router:
|
||||
app.include_router(admin_router, prefix="/admin", tags=["admin"])
|
||||
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
app = create_app()
|
||||
|
|
@ -4,28 +4,26 @@ 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).
|
||||
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.2.0 (Update für WP-04b Explanation Layer)
|
||||
0.3.0 (Update für WP-04c Feedback)
|
||||
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
|
||||
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
|
||||
|
|
@ -51,12 +49,14 @@ class EdgeDTO(BaseModel):
|
|||
direction: Literal["out", "in", "undirected"] = "out"
|
||||
|
||||
|
||||
# --- Request Models ---
|
||||
|
||||
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
|
||||
- query: (optional) Freitext
|
||||
- query_vector: (optional) direkter Vektor
|
||||
- explain: (optional) Fordert detaillierte Erklärungen an (WP-04b)
|
||||
"""
|
||||
mode: Literal["semantic", "edge", "hybrid"] = "hybrid"
|
||||
|
|
@ -65,24 +65,29 @@ class QueryRequest(BaseModel):
|
|||
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
|
||||
|
||||
|
||||
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.
|
||||
Zeigt die gewichteten Beiträge zum Total Score.
|
||||
"""
|
||||
"""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 für Transparenz
|
||||
# Rohwerte
|
||||
raw_semantic: float
|
||||
raw_edge_bonus: float
|
||||
raw_centrality: float
|
||||
|
|
@ -90,10 +95,7 @@ class ScoreBreakdown(BaseModel):
|
|||
|
||||
|
||||
class Reason(BaseModel):
|
||||
"""
|
||||
Ein semantischer Grund für das Ranking.
|
||||
z.B. 'Verlinkt von Projekt X', 'Hohe Textähnlichkeit'.
|
||||
"""
|
||||
"""Ein semantischer Grund für das Ranking."""
|
||||
kind: Literal["semantic", "edge", "type", "centrality"]
|
||||
message: str
|
||||
score_impact: Optional[float] = None
|
||||
|
|
@ -101,24 +103,20 @@ class Reason(BaseModel):
|
|||
|
||||
|
||||
class Explanation(BaseModel):
|
||||
"""
|
||||
Container für alle Erklärungsdaten eines Treffers.
|
||||
"""
|
||||
"""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 ---
|
||||
|
||||
# --- Response Models ---
|
||||
|
||||
class QueryHit(BaseModel):
|
||||
"""Einzelnes Trefferobjekt für /query."""
|
||||
node_id: str
|
||||
note_id: Optional[str]
|
||||
|
||||
# Flache Scores (Kompatibilität WP-04a)
|
||||
# Flache Scores
|
||||
semantic_score: float
|
||||
edge_bonus: float
|
||||
centrality_bonus: float
|
||||
|
|
@ -127,19 +125,23 @@ class QueryHit(BaseModel):
|
|||
paths: Optional[List[List[Dict]]] = None
|
||||
source: Optional[Dict] = None
|
||||
|
||||
# WP-04b: Erklärungsobjekt (nur gefüllt, wenn explain=True)
|
||||
# WP-04b: Erklärungsobjekt
|
||||
explanation: Optional[Explanation] = None
|
||||
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
"""Antwortstruktur für /query (Liste von Treffern + Telemetrie)."""
|
||||
"""
|
||||
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} (Nachbarschaft)."""
|
||||
"""Antwortstruktur für /graph/{note_id}."""
|
||||
center_note_id: str
|
||||
nodes: List[NodeDTO]
|
||||
edges: List[EdgeDTO]
|
||||
|
|
|
|||
20
app/routers/feedback.py
Normal file
20
app/routers/feedback.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
app/routers/feedback.py
|
||||
Endpunkt für User-Feedback (WP-04c).
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.models.dto import FeedbackRequest
|
||||
from app.services.feedback_service import log_feedback
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def post_feedback(fb: FeedbackRequest):
|
||||
"""
|
||||
Nimmt Feedback entgegen (z.B. Daumen hoch für einen Treffer).
|
||||
"""
|
||||
try:
|
||||
log_feedback(fb)
|
||||
return {"status": "recorded", "query_id": fb.query_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -15,24 +15,31 @@ Bezug:
|
|||
Nutzung:
|
||||
app.include_router(query.router, prefix="/query", tags=["query"])
|
||||
Änderungsverlauf:
|
||||
0.2.0 (2025-12-07) - Update für WP04c Feedback
|
||||
0.1.0 (2025-10-07) – Erstanlage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||
from app.models.dto import QueryRequest, QueryResponse
|
||||
from app.core.retriever import hybrid_retrieve, semantic_retrieve
|
||||
# NEU:
|
||||
from app.services.feedback_service import log_search
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("", response_model=QueryResponse)
|
||||
def post_query(req: QueryRequest) -> QueryResponse:
|
||||
def post_query(req: QueryRequest, background_tasks: BackgroundTasks) -> QueryResponse:
|
||||
try:
|
||||
if req.mode == "semantic":
|
||||
return semantic_retrieve(req)
|
||||
# default: hybrid
|
||||
return hybrid_retrieve(req)
|
||||
res = semantic_retrieve(req)
|
||||
else:
|
||||
res = hybrid_retrieve(req)
|
||||
|
||||
# WP-04c: Logging im Hintergrund (bremst Antwort nicht)
|
||||
background_tasks.add_task(log_search, req, res)
|
||||
|
||||
return res
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"query failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"query failed: {e}")
|
||||
80
app/services/feedback_service.py
Normal file
80
app/services/feedback_service.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""
|
||||
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).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
from app.models.dto import QueryRequest, QueryResponse, FeedbackRequest
|
||||
|
||||
# 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 log_search(req: QueryRequest, res: QueryResponse):
|
||||
"""
|
||||
Speichert den "Snapshot" der Suche.
|
||||
WICHTIG: Wir speichern die Scores (Breakdown), damit wir später wissen,
|
||||
warum das System so entschieden hat.
|
||||
"""
|
||||
_ensure_log_dir()
|
||||
|
||||
# Wir reduzieren die Datenmenge etwas (z.B. keine vollen Texte)
|
||||
hits_summary = []
|
||||
for hit in res.results:
|
||||
# Falls Explanation an war, speichern wir den Breakdown, sonst die Scores
|
||||
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, # Wichtig für Training!
|
||||
"rank_semantic": hit.semantic_score,
|
||||
"rank_edge": hit.edge_bonus
|
||||
})
|
||||
|
||||
entry = {
|
||||
"timestamp": time.time(),
|
||||
"query_id": res.query_id,
|
||||
"query_text": req.query,
|
||||
"mode": req.mode,
|
||||
"top_k": req.top_k,
|
||||
"hits": hits_summary
|
||||
}
|
||||
|
||||
try:
|
||||
with open(SEARCH_LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
except Exception as e:
|
||||
print(f"ERROR logging search: {e}")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
try:
|
||||
with open(FEEDBACK_LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
except Exception as e:
|
||||
print(f"ERROR logging feedback: {e}")
|
||||
Loading…
Reference in New Issue
Block a user