78448220b2
voice_metrics.py: neben den rollenden Stats jetzt ein echter Per-Turn-Trace (TurnTrace + Ringpuffer der letzten 60 Turns). Balken-Stufen zeitlich disjunkt (stt · vision · hirn · gen); hirn misst ab mark_brain_start() VOR dem Hermes- Request, damit die Vision-Zeit nicht doppelt gezaehlt wird. STT (davor) und Mem0-Retrieve (Rueckruf waehrend) werden per park()/_take_* best-effort dem Turn zugeordnet (Ein-Nutzer-Geraet, kein Turn-ID noetig). Mem0 = Unter-Detail INNERHALB hirn (nicht addieren) -> ehrlich, kein Doppelzaehlen. voice.py: TurnTrace in voice_chat's gen() (commit garantiert 1x via finally, auch bei Fehler/Abbruch). STT-Endpoint parkt seine Dauer. NEU: GET /api/voice/metrics (schliesst die Luecke - selbstkritik-feed.sh curlte das, existierte nie -> 404) + GET /api/voice/trace?limit=N. memory.py: GET /api/memory?q=... (= Hermes' Mem0-Prefetch) misst + parkt die Retrieve-Zeit. Frontend: LatencyCard (gestapelte Balken je Turn, "Taeter" = groesste Stufe, Fehler-Turns rot, Tooltip mit "davon Mem0 X s"), Query useVoiceTrace, in der Zentrale unter "Stack & Telemetrie". Lokal gegen Seed-Server verifiziert: 30,3-s- Haenger -> Hirn-Balken 94% + "davon Mem0 26,1 s". Grenzen (ehrlich, als Fussnote in der Karte): Tool-Runden im Hermes-LLM-Loop haben keinen Callback an MC2 -> stecken in "Antwort". Reine Telegram-Text-Turns laufen an MC2 vorbei und erscheinen hier nicht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
2.7 KiB
Python
94 lines
2.7 KiB
Python
"""Memory-Endpoints (geteiltes Gedächtnis). LAN-only, kein Token in 2.0-Phase 3."""
|
|
|
|
import time
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from services import memory
|
|
from services.voice_metrics import park, record_stage
|
|
|
|
router = APIRouter(prefix="/api")
|
|
|
|
|
|
class MemIn(BaseModel):
|
|
content: str
|
|
category: str = "knowledge"
|
|
source: str = "manual"
|
|
|
|
|
|
class MemUp(BaseModel):
|
|
content: str | None = None
|
|
category: str | None = None
|
|
|
|
|
|
class DedupeIn(BaseModel):
|
|
apply: bool = False
|
|
threshold: float = 0.85
|
|
|
|
|
|
class LearnIn(BaseModel):
|
|
text: str | None = None
|
|
messages: list[dict] | None = None
|
|
source: str = "auto"
|
|
category: str = "knowledge"
|
|
|
|
|
|
@router.get("/memory/export")
|
|
def export() -> dict:
|
|
return memory.export_text()
|
|
|
|
|
|
@router.get("/memory/graph")
|
|
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
|
"""Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die Visualisierung."""
|
|
return memory.graph(min_score=min_score, top_k=top_k)
|
|
|
|
|
|
@router.post("/memory/learn", status_code=201)
|
|
def learn(body: LearnIn) -> dict:
|
|
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 extrahiert Fakten selbst."""
|
|
return memory.learn(text=body.text, messages=body.messages,
|
|
source=body.source, category=body.category)
|
|
|
|
|
|
@router.post("/memory/dedupe")
|
|
def dedupe(body: DedupeIn) -> dict:
|
|
return memory.dedupe(apply=body.apply, threshold=body.threshold)
|
|
|
|
|
|
@router.get("/memory")
|
|
def list_mem(q: str = "", category: str = "") -> list[dict]:
|
|
# Semantischer Retrieve (q gesetzt) = u.a. Hermes' Mem0-Prefetch VOR jedem Turn. Dauer messen
|
|
# + parken, damit der laufende Voice-Chat-Turn sie als Unter-Detail seiner Hirn-Zeit einsammelt.
|
|
if q:
|
|
_t0 = time.perf_counter()
|
|
res = memory.list_memories(q=q, category=category)
|
|
_ms = (time.perf_counter() - _t0) * 1000.0
|
|
record_stage("memory_retrieve", _ms)
|
|
park("retrieve", _ms)
|
|
return res
|
|
return memory.list_memories(q=q, category=category)
|
|
|
|
|
|
@router.post("/memory", status_code=201)
|
|
def add(body: MemIn) -> dict:
|
|
if body.category not in memory.CATEGORIES:
|
|
raise HTTPException(400, f"Kategorie '{body.category}' unbekannt.")
|
|
return memory.add_memory(body.content, body.category, body.source)
|
|
|
|
|
|
@router.put("/memory/{mid}")
|
|
def update(mid: str, body: MemUp) -> dict:
|
|
res = memory.update_memory(mid, content=body.content, category=body.category)
|
|
if not res:
|
|
raise HTTPException(404, "Eintrag nicht gefunden")
|
|
return res
|
|
|
|
|
|
@router.delete("/memory/{mid}")
|
|
def delete(mid: str) -> dict:
|
|
if not memory.delete_memory(mid):
|
|
raise HTTPException(404, "Eintrag nicht gefunden")
|
|
return {"ok": True}
|