From 78d8ab11eda05e88a1539b9fcc754e1b9a6a480d Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 7 Dec 2025 19:09:03 +0100 Subject: [PATCH 1/4] docs/dev_workflow.md aktualisiert --- docs/dev_workflow.md | 47 +++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/dev_workflow.md b/docs/dev_workflow.md index ddc4f48..01af4c8 100644 --- a/docs/dev_workflow.md +++ b/docs/dev_workflow.md @@ -1,6 +1,6 @@ # Mindnet v2.2 – Entwickler-Workflow **Datei:** `DEV_WORKFLOW.md` -**Stand:** 2025-12-07 (Aktualisiert nach WP-04b) +**Stand:** 2025-12-07 (Aktualisiert: Sync-First Strategie) Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server). @@ -22,12 +22,14 @@ Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), Hier erstellst du die neue Funktion in einer sicheren Umgebung. -1. **Sicherstellen, dass Git bereit ist:** - * Öffne VS Code. - * Unten links in der blauen Leiste sollte der aktuelle Branch stehen (z.B. `main`). +1. **Basis aktualisieren (WICHTIG!):** + Bevor du startest, muss dein lokales `main` auf dem Stand des Servers sein. + * Klicke unten links auf den aktuellen Branch und wähle **`main`**. + * Klicke links im Menü "Source Control" auf die **drei Punkte (...)** -> **Pull** (oder das Synchronisieren-Symbol). + * *Erst jetzt hast du alle Dateien!* -2. **Branch erstellen (WICHTIG):** - * Klicke unten links auf den Branch-Namen. +2. **Branch erstellen:** + * Klicke wieder unten links auf `main`. * Wähle `+ Create new branch...`. * Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp04b-explanation`). * Drücke **Enter**. @@ -69,9 +71,9 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft. ``` 5. **Test-Server neustarten (WICHTIG):** - Falls noch ein alter Prozess läuft, musst du ihn beenden, damit der neue Code geladen wird. + Falls noch ein alter Prozess läuft, musst du ihn beenden. * Drücke `Strg + C` falls der Server noch im Vordergrund läuft. - * Oder nutze `pkill -f "uvicorn app.main:app"` um Hintergrunde-Prozesse zu stoppen. + * Oder nutze `pkill -f "uvicorn app.main:app"`. Starten auf **Port 8002**: ```bash @@ -127,6 +129,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch. ``` 3. **VS Code:** * Auf `main` wechseln. + * Sync drücken (um Löschung vom Server zu erfahren). * `F1` -> `Git: Delete Branch` -> Branch auswählen. --- @@ -135,23 +138,23 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch. | Wo? | Befehl | Was tut es? | | :--- | :--- | :--- | -| **VS Code** | `Klick auf Branch-Namen` | Branch erstellen oder wechseln. | -| **VS Code** | `Sync Changes` | Lädt Code zu Gitea hoch. | -| **Beelink** | `cd ~/mindnet_dev` | Gehe in den Test-Ordner. | +| **VS Code** | `Sync (auf main)` | **WICHTIG:** Holt neuesten Code vom Server. | +| **VS Code** | `Klick auf Branch` | Branch erstellen oder wechseln. | | **Beelink** | `source .venv/bin/activate` | **Aktiviert Python-Umgebung.** | -| **Beelink** | `git fetch` | Aktualisiert die Liste der Remote-Branches. | -| **Beelink** | `git branch -r` | Zeigt alle Branches auf dem Server an. | -| **Beelink** | `git checkout ` | Wechsle auf einen anderen Branch. | +| **Beelink** | `git fetch` | Aktualisiert Remote-Branches. | +| **Beelink** | `git checkout ` | Wechsle Branch. | +| **Beelink** | `git pull` | Aktualisiere aktuellen Branch. | | **Beelink** | `uvicorn ... --port 8002` | Startet Test-Server (Dev). | --- -## 4. Sicherheitsregeln +## 4. Troubleshooting -1. **Niemals** in `~/mindnet` (Prod-Ordner) experimentieren. Dieser Ordner bleibt immer auf `main`. -2. **Niemals** beide Umgebungen auf denselben `COLLECTION_PREFIX` zeigen lassen. - * Prod `.env`: `COLLECTION_PREFIX="mindnet"` - * Dev `.env`: `COLLECTION_PREFIX="mindnet_dev"` -3. **Port-Disziplin:** - * **PROD:** Port 8001 - * **DEV:** Port 8002 \ No newline at end of file +**"Hilfe, in meinem neuen Branch fehlen Dateien!"** +Das passiert, wenn du beim Erstellen nicht aktuell warst. +* **Lösung:** + ```bash + # In VS Code Terminal: + git checkout feature/mein-kaputter-branch + git merge main + # (Das holt die fehlenden Dateien aus main nach) \ No newline at end of file -- 2.43.0 From caab41a760ab2a2c74e7d24a87d7c79bd2d8b50b Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 7 Dec 2025 19:29:48 +0100 Subject: [PATCH 2/4] WP04c --- app/main.py | 18 +++---- app/models/dto.py | 62 +++++++++++++------------ app/routers/feedback.py | 20 ++++++++ app/routers/query.py | 21 ++++++--- app/services/feedback_service.py | 80 ++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 46 deletions(-) create mode 100644 app/routers/feedback.py create mode 100644 app/services/feedback_service.py diff --git a/app/main.py b/app/main.py index e395f89..4f974b9 100644 --- a/app/main.py +++ b/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() \ No newline at end of file diff --git a/app/models/dto.py b/app/models/dto.py index 9efaf7f..a0e583c 100644 --- a/app/models/dto.py +++ b/app/models/dto.py @@ -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] diff --git a/app/routers/feedback.py b/app/routers/feedback.py new file mode 100644 index 0000000..2ebaf4a --- /dev/null +++ b/app/routers/feedback.py @@ -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)) \ No newline at end of file diff --git a/app/routers/query.py b/app/routers/query.py index ca3e90e..4edb689 100644 --- a/app/routers/query.py +++ b/app/routers/query.py @@ -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}") \ No newline at end of file diff --git a/app/services/feedback_service.py b/app/services/feedback_service.py new file mode 100644 index 0000000..6e945fe --- /dev/null +++ b/app/services/feedback_service.py @@ -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}") \ No newline at end of file -- 2.43.0 From f98eb8fc1638fab744215f3134e335f79a21e9de Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 7 Dec 2025 19:32:30 +0100 Subject: [PATCH 3/4] =?UTF-8?q?neuer=20Test=20f=C3=BCr=20Feedback=20(WP04c?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_feedback_smoke.py | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_feedback_smoke.py diff --git a/tests/test_feedback_smoke.py b/tests/test_feedback_smoke.py new file mode 100644 index 0000000..3cc73f0 --- /dev/null +++ b/tests/test_feedback_smoke.py @@ -0,0 +1,65 @@ +import requests +import json +import sys +import time + +# Konfiguration +BASE_URL = "http://localhost:8002" +QUERY_URL = f"{BASE_URL}/query" +FEEDBACK_URL = f"{BASE_URL}/feedback" + +def run_test(): + print(f"--- 1. Sende Suchanfrage an {QUERY_URL} ---") + query_payload = { + "query": "mindnet architecture", + "mode": "hybrid", + "top_k": 2, + "explain": True # Wir wollen auch Breakdown loggen + } + + try: + r = requests.post(QUERY_URL, json=query_payload) + r.raise_for_status() + res_data = r.json() + except Exception as e: + print(f"ERROR bei Suche: {e}") + sys.exit(1) + + query_id = res_data.get("query_id") + results = res_data.get("results", []) + + if not query_id: + print("FAIL: Keine query_id in der Antwort erhalten!") + sys.exit(1) + + if not results: + print("WARNUNG: Keine Treffer gefunden, Test kann Feedback nur simuliert senden.") + target_node_id = "fake-node-id" + else: + target_node_id = results[0].get("node_id") + + print(f"SUCCESS: Suche OK. Erhaltene Query-ID: {query_id}") + print(f"Target Node für Feedback: {target_node_id}") + + # Kurze Pause, damit Background-Task Zeit hat zu schreiben (nur für Log-Check relevant) + time.sleep(0.5) + + print(f"\n--- 2. Sende Feedback an {FEEDBACK_URL} ---") + feedback_payload = { + "query_id": query_id, + "node_id": target_node_id, + "score": 1, + "comment": "Automatisierter Smoke-Test: Dieser Treffer war hilfreich." + } + + try: + r_fb = requests.post(FEEDBACK_URL, json=feedback_payload) + r_fb.raise_for_status() + print(f"SUCCESS: Feedback gesendet. Status: {r_fb.status_code}") + print(f"Response: {r_fb.json()}") + except Exception as e: + print(f"ERROR bei Feedback: {e}") + sys.exit(1) + +if __name__ == "__main__": + run_test() \ No newline at end of file -- 2.43.0 From 7631b93900b0f2dba9d6ea240e7007974dcc11d4 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 8 Dec 2025 06:53:04 +0100 Subject: [PATCH 4/4] =?UTF-8?q?Programmmanagement/ARCHITECTURE=5FSNAPSHOT?= =?UTF-8?q?=5Fv2.2.1.md=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ARCHITECTURE_SNAPSHOT_v2.2.1.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md diff --git a/Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md b/Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md new file mode 100644 index 0000000..1960599 --- /dev/null +++ b/Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md @@ -0,0 +1,27 @@ +# Architecture Snapshot v2.2.1 +**Stand:** Nach Abschluss von WP-04c +**Kontext:** Wichtig für WP-05 Entwicklung. + +## 1. Neue Komponenten (Seit v2.2.0) + +### A. Feedback Service (`app/services/feedback_service.py`) +- **Zweck:** Logging von Trainingsdaten für späteres Self-Tuning (WP-08). +- **Storage:** Lokale JSONL-Dateien in `data/logs/` (Append-Only). + - `search_history.jsonl`: Query + Snapshot der Ergebnisse (Scores). + - `feedback.jsonl`: User-Rating zu spezifischer `node_id`. + +### B. Explanation Layer (`app/core/retriever.py`) +- **Logik:** Der Retriever berechnet nicht nur Scores, sondern generiert `Explanation`-Objekte. +- **Graph:** `Subgraph` (in `graph_adapter.py`) führt jetzt auch `reverse_adj` (Incoming Edges), um zu erklären, warum ein Knoten wichtig ist ("Referenziert von..."). + +### C. DTOs (`app/models/dto.py`) +Das Datenmodell wurde massiv erweitert. Wichtige Klassen für WP-05: +- `QueryResponse`: Enthält jetzt `query_id` (UUID). +- `QueryHit`: Enthält optional `explanation` (Typ `Explanation`). +- `FeedbackRequest`: Für den Feedback-Loop. + +## 2. Implikationen für WP-05 (Chat) + +1. **Logging:** Auch der neue `/chat` Endpoint sollte idealerweise die `query_id` loggen oder nutzen, um Konsistenz zu wahren. +2. **DTO-Nutzung:** Der Chat-Service wird intern den Retriever aufrufen. Er muss mit den `QueryHit`-Objekten arbeiten, um den Kontext für das LLM zu bauen. +3. **Config:** Die Persönlichkeit wird in `config/prompts.yaml` definiert (Late Binding), nicht im Python-Code hardcodiert. \ No newline at end of file -- 2.43.0