WP04c #2
27
Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md
Normal file
27
Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md
Normal file
|
|
@ -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.
|
||||||
18
app/main.py
18
app/main.py
|
|
@ -1,43 +1,43 @@
|
||||||
"""
|
"""
|
||||||
app/main.py — mindnet API bootstrap (WP-04 Hooks)
|
app/main.py — mindnet API bootstrap
|
||||||
Version: 0.4.2 • Stand: 2025-10-07
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
from .routers.embed_router import router as embed_router
|
from .routers.embed_router import router as embed_router
|
||||||
from .routers.qdrant_router import router as qdrant_router
|
from .routers.qdrant_router import router as qdrant_router
|
||||||
|
|
||||||
# WP-04 Router:
|
|
||||||
from .routers.query import router as query_router
|
from .routers.query import router as query_router
|
||||||
from .routers.graph import router as graph_router
|
from .routers.graph import router as graph_router
|
||||||
from .routers.tools import router as tools_router
|
from .routers.tools import router as tools_router
|
||||||
# Optional:
|
# NEU: Feedback Router
|
||||||
|
from .routers.feedback import router as feedback_router
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .routers.admin import router as admin_router
|
from .routers.admin import router as admin_router
|
||||||
except Exception:
|
except Exception:
|
||||||
admin_router = None
|
admin_router = None
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
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()
|
s = get_settings()
|
||||||
|
|
||||||
@app.get("/healthz")
|
@app.get("/healthz")
|
||||||
def healthz():
|
def healthz():
|
||||||
return {"status": "ok", "qdrant": s.QDRANT_URL, "prefix": s.COLLECTION_PREFIX}
|
return {"status": "ok", "qdrant": s.QDRANT_URL, "prefix": s.COLLECTION_PREFIX}
|
||||||
|
|
||||||
# Bestehende Router (unverändert)
|
|
||||||
app.include_router(embed_router)
|
app.include_router(embed_router)
|
||||||
app.include_router(qdrant_router)
|
app.include_router(qdrant_router)
|
||||||
|
|
||||||
# WP-04 Endpunkte
|
|
||||||
app.include_router(query_router, prefix="/query", tags=["query"])
|
app.include_router(query_router, prefix="/query", tags=["query"])
|
||||||
app.include_router(graph_router, prefix="/graph", tags=["graph"])
|
app.include_router(graph_router, prefix="/graph", tags=["graph"])
|
||||||
app.include_router(tools_router, prefix="/tools", tags=["tools"])
|
app.include_router(tools_router, prefix="/tools", tags=["tools"])
|
||||||
|
# NEU:
|
||||||
|
app.include_router(feedback_router, prefix="/feedback", tags=["feedback"])
|
||||||
|
|
||||||
if admin_router:
|
if admin_router:
|
||||||
app.include_router(admin_router, prefix="/admin", tags=["admin"])
|
app.include_router(admin_router, prefix="/admin", tags=["admin"])
|
||||||
|
|
||||||
return app
|
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:
|
Zweck:
|
||||||
Laufzeit-Modelle für FastAPI (Requests/Responses), getrennt von JSON-Schemas.
|
Laufzeit-Modelle für FastAPI (Requests/Responses), getrennt von JSON-Schemas.
|
||||||
Deckt die Graph-/Retriever-Endpunkte ab.
|
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:
|
Kompatibilität:
|
||||||
Python 3.12+, Pydantic 2.x, FastAPI 0.110+
|
Python 3.12+, Pydantic 2.x, FastAPI 0.110+
|
||||||
Version:
|
Version:
|
||||||
0.2.0 (Update für WP-04b Explanation Layer)
|
0.3.0 (Update für WP-04c Feedback)
|
||||||
Stand:
|
Stand:
|
||||||
2025-12-07
|
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 __future__ import annotations
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import List, Literal, Optional, Dict, Any
|
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"]
|
EdgeKind = Literal["references", "references_at", "backlink", "next", "prev", "belongs_to", "depends_on", "related_to", "similar_to"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Basis-DTOs ---
|
||||||
|
|
||||||
class NodeDTO(BaseModel):
|
class NodeDTO(BaseModel):
|
||||||
"""Darstellung eines Knotens (Note oder Chunk) im API-Graph."""
|
"""Darstellung eines Knotens (Note oder Chunk) im API-Graph."""
|
||||||
id: str
|
id: str
|
||||||
|
|
@ -51,12 +49,14 @@ class EdgeDTO(BaseModel):
|
||||||
direction: Literal["out", "in", "undirected"] = "out"
|
direction: Literal["out", "in", "undirected"] = "out"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Request Models ---
|
||||||
|
|
||||||
class QueryRequest(BaseModel):
|
class QueryRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
Request für /query:
|
Request für /query:
|
||||||
- mode: 'semantic' | 'edge' | 'hybrid'
|
- mode: 'semantic' | 'edge' | 'hybrid'
|
||||||
- query: (optional) Freitext; Embedding wird später angebunden
|
- query: (optional) Freitext
|
||||||
- query_vector: (optional) direkter Vektor (384d) für Quick-Tests ohne Embedding
|
- query_vector: (optional) direkter Vektor
|
||||||
- explain: (optional) Fordert detaillierte Erklärungen an (WP-04b)
|
- explain: (optional) Fordert detaillierte Erklärungen an (WP-04b)
|
||||||
"""
|
"""
|
||||||
mode: Literal["semantic", "edge", "hybrid"] = "hybrid"
|
mode: Literal["semantic", "edge", "hybrid"] = "hybrid"
|
||||||
|
|
@ -65,24 +65,29 @@ class QueryRequest(BaseModel):
|
||||||
top_k: int = 10
|
top_k: int = 10
|
||||||
expand: Dict = {"depth": 1, "edge_types": ["references", "belongs_to", "prev", "next", "depends_on", "related_to"]}
|
expand: Dict = {"depth": 1, "edge_types": ["references", "belongs_to", "prev", "next", "depends_on", "related_to"]}
|
||||||
filters: Optional[Dict] = None
|
filters: Optional[Dict] = None
|
||||||
# Flags zur Steuerung der Rückgabe
|
|
||||||
ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True}
|
ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True}
|
||||||
# WP-04b: Soll eine Erklärung generiert werden?
|
|
||||||
explain: bool = False
|
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 ---
|
# --- WP-04b Explanation Models ---
|
||||||
|
|
||||||
class ScoreBreakdown(BaseModel):
|
class ScoreBreakdown(BaseModel):
|
||||||
"""
|
"""Aufschlüsselung der Score-Komponenten."""
|
||||||
Aufschlüsselung der Score-Komponenten.
|
|
||||||
Zeigt die gewichteten Beiträge zum Total Score.
|
|
||||||
"""
|
|
||||||
semantic_contribution: float = Field(..., description="W_sem * semantic_score * weight")
|
semantic_contribution: float = Field(..., description="W_sem * semantic_score * weight")
|
||||||
edge_contribution: float = Field(..., description="W_edge * edge_bonus")
|
edge_contribution: float = Field(..., description="W_edge * edge_bonus")
|
||||||
centrality_contribution: float = Field(..., description="W_cent * centrality_bonus")
|
centrality_contribution: float = Field(..., description="W_cent * centrality_bonus")
|
||||||
|
|
||||||
# Rohwerte für Transparenz
|
# Rohwerte
|
||||||
raw_semantic: float
|
raw_semantic: float
|
||||||
raw_edge_bonus: float
|
raw_edge_bonus: float
|
||||||
raw_centrality: float
|
raw_centrality: float
|
||||||
|
|
@ -90,10 +95,7 @@ class ScoreBreakdown(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Reason(BaseModel):
|
class Reason(BaseModel):
|
||||||
"""
|
"""Ein semantischer Grund für das Ranking."""
|
||||||
Ein semantischer Grund für das Ranking.
|
|
||||||
z.B. 'Verlinkt von Projekt X', 'Hohe Textähnlichkeit'.
|
|
||||||
"""
|
|
||||||
kind: Literal["semantic", "edge", "type", "centrality"]
|
kind: Literal["semantic", "edge", "type", "centrality"]
|
||||||
message: str
|
message: str
|
||||||
score_impact: Optional[float] = None
|
score_impact: Optional[float] = None
|
||||||
|
|
@ -101,24 +103,20 @@ class Reason(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Explanation(BaseModel):
|
class Explanation(BaseModel):
|
||||||
"""
|
"""Container für alle Erklärungsdaten eines Treffers."""
|
||||||
Container für alle Erklärungsdaten eines Treffers.
|
|
||||||
"""
|
|
||||||
breakdown: ScoreBreakdown
|
breakdown: ScoreBreakdown
|
||||||
reasons: List[Reason]
|
reasons: List[Reason]
|
||||||
# Optional: Pfade im Graphen, die zu diesem Treffer geführt haben
|
|
||||||
related_edges: Optional[List[EdgeDTO]] = None
|
related_edges: Optional[List[EdgeDTO]] = None
|
||||||
|
|
||||||
|
|
||||||
# --- End Explanation Models ---
|
# --- Response Models ---
|
||||||
|
|
||||||
|
|
||||||
class QueryHit(BaseModel):
|
class QueryHit(BaseModel):
|
||||||
"""Einzelnes Trefferobjekt für /query."""
|
"""Einzelnes Trefferobjekt für /query."""
|
||||||
node_id: str
|
node_id: str
|
||||||
note_id: Optional[str]
|
note_id: Optional[str]
|
||||||
|
|
||||||
# Flache Scores (Kompatibilität WP-04a)
|
# Flache Scores
|
||||||
semantic_score: float
|
semantic_score: float
|
||||||
edge_bonus: float
|
edge_bonus: float
|
||||||
centrality_bonus: float
|
centrality_bonus: float
|
||||||
|
|
@ -127,19 +125,23 @@ class QueryHit(BaseModel):
|
||||||
paths: Optional[List[List[Dict]]] = None
|
paths: Optional[List[List[Dict]]] = None
|
||||||
source: Optional[Dict] = None
|
source: Optional[Dict] = None
|
||||||
|
|
||||||
# WP-04b: Erklärungsobjekt (nur gefüllt, wenn explain=True)
|
# WP-04b: Erklärungsobjekt
|
||||||
explanation: Optional[Explanation] = None
|
explanation: Optional[Explanation] = None
|
||||||
|
|
||||||
|
|
||||||
class QueryResponse(BaseModel):
|
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]
|
results: List[QueryHit]
|
||||||
used_mode: str
|
used_mode: str
|
||||||
latency_ms: int
|
latency_ms: int
|
||||||
|
|
||||||
|
|
||||||
class GraphResponse(BaseModel):
|
class GraphResponse(BaseModel):
|
||||||
"""Antwortstruktur für /graph/{note_id} (Nachbarschaft)."""
|
"""Antwortstruktur für /graph/{note_id}."""
|
||||||
center_note_id: str
|
center_note_id: str
|
||||||
nodes: List[NodeDTO]
|
nodes: List[NodeDTO]
|
||||||
edges: List[EdgeDTO]
|
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:
|
Nutzung:
|
||||||
app.include_router(query.router, prefix="/query", tags=["query"])
|
app.include_router(query.router, prefix="/query", tags=["query"])
|
||||||
Änderungsverlauf:
|
Änderungsverlauf:
|
||||||
|
0.2.0 (2025-12-07) - Update für WP04c Feedback
|
||||||
0.1.0 (2025-10-07) – Erstanlage.
|
0.1.0 (2025-10-07) – Erstanlage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||||
from app.models.dto import QueryRequest, QueryResponse
|
from app.models.dto import QueryRequest, QueryResponse
|
||||||
from app.core.retriever import hybrid_retrieve, semantic_retrieve
|
from app.core.retriever import hybrid_retrieve, semantic_retrieve
|
||||||
|
# NEU:
|
||||||
|
from app.services.feedback_service import log_search
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("", response_model=QueryResponse)
|
@router.post("", response_model=QueryResponse)
|
||||||
def post_query(req: QueryRequest) -> QueryResponse:
|
def post_query(req: QueryRequest, background_tasks: BackgroundTasks) -> QueryResponse:
|
||||||
try:
|
try:
|
||||||
if req.mode == "semantic":
|
if req.mode == "semantic":
|
||||||
return semantic_retrieve(req)
|
res = semantic_retrieve(req)
|
||||||
# default: hybrid
|
else:
|
||||||
return hybrid_retrieve(req)
|
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:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as 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}")
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Mindnet v2.2 – Entwickler-Workflow
|
# Mindnet v2.2 – Entwickler-Workflow
|
||||||
**Datei:** `DEV_WORKFLOW.md`
|
**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).
|
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.
|
Hier erstellst du die neue Funktion in einer sicheren Umgebung.
|
||||||
|
|
||||||
1. **Sicherstellen, dass Git bereit ist:**
|
1. **Basis aktualisieren (WICHTIG!):**
|
||||||
* Öffne VS Code.
|
Bevor du startest, muss dein lokales `main` auf dem Stand des Servers sein.
|
||||||
* Unten links in der blauen Leiste sollte der aktuelle Branch stehen (z.B. `main`).
|
* 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):**
|
2. **Branch erstellen:**
|
||||||
* Klicke unten links auf den Branch-Namen.
|
* Klicke wieder unten links auf `main`.
|
||||||
* Wähle `+ Create new branch...`.
|
* Wähle `+ Create new branch...`.
|
||||||
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp04b-explanation`).
|
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp04b-explanation`).
|
||||||
* Drücke **Enter**.
|
* 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):**
|
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.
|
* 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**:
|
Starten auf **Port 8002**:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -127,6 +129,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
|
||||||
```
|
```
|
||||||
3. **VS Code:**
|
3. **VS Code:**
|
||||||
* Auf `main` wechseln.
|
* Auf `main` wechseln.
|
||||||
|
* Sync drücken (um Löschung vom Server zu erfahren).
|
||||||
* `F1` -> `Git: Delete Branch` -> Branch auswählen.
|
* `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? |
|
| Wo? | Befehl | Was tut es? |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **VS Code** | `Klick auf Branch-Namen` | Branch erstellen oder wechseln. |
|
| **VS Code** | `Sync (auf main)` | **WICHTIG:** Holt neuesten Code vom Server. |
|
||||||
| **VS Code** | `Sync Changes` | Lädt Code zu Gitea hoch. |
|
| **VS Code** | `Klick auf Branch` | Branch erstellen oder wechseln. |
|
||||||
| **Beelink** | `cd ~/mindnet_dev` | Gehe in den Test-Ordner. |
|
|
||||||
| **Beelink** | `source .venv/bin/activate` | **Aktiviert Python-Umgebung.** |
|
| **Beelink** | `source .venv/bin/activate` | **Aktiviert Python-Umgebung.** |
|
||||||
| **Beelink** | `git fetch` | Aktualisiert die Liste der Remote-Branches. |
|
| **Beelink** | `git fetch` | Aktualisiert Remote-Branches. |
|
||||||
| **Beelink** | `git branch -r` | Zeigt alle Branches auf dem Server an. |
|
| **Beelink** | `git checkout <name>` | Wechsle Branch. |
|
||||||
| **Beelink** | `git checkout <name>` | Wechsle auf einen anderen Branch. |
|
| **Beelink** | `git pull` | Aktualisiere aktuellen Branch. |
|
||||||
| **Beelink** | `uvicorn ... --port 8002` | Startet Test-Server (Dev). |
|
| **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`.
|
**"Hilfe, in meinem neuen Branch fehlen Dateien!"**
|
||||||
2. **Niemals** beide Umgebungen auf denselben `COLLECTION_PREFIX` zeigen lassen.
|
Das passiert, wenn du beim Erstellen nicht aktuell warst.
|
||||||
* Prod `.env`: `COLLECTION_PREFIX="mindnet"`
|
* **Lösung:**
|
||||||
* Dev `.env`: `COLLECTION_PREFIX="mindnet_dev"`
|
```bash
|
||||||
3. **Port-Disziplin:**
|
# In VS Code Terminal:
|
||||||
* **PROD:** Port 8001
|
git checkout feature/mein-kaputter-branch
|
||||||
* **DEV:** Port 8002
|
git merge main
|
||||||
|
# (Das holt die fehlenden Dateien aus main nach)
|
||||||
65
tests/test_feedback_smoke.py
Normal file
65
tests/test_feedback_smoke.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue
Block a user