fix(stack): Mem0 restlos ausgebaut + vier stille Defekte behoben
Systemaudit vom 27.08.2026. Alle Befunde gemessen, nicht vermutet.
VIER STILLE DEFEKTE
1. mc2-steward startete seit Wochen nicht (live: 207.609 Neustarts).
steward.py importierte services.memory, das beim Mem0-Ausbau geloescht
wurde -> ImportError bei jedem Start. Re-Warm- und Health-Waechter
waren damit tot.
2. Jedes Hermes-Update wurde automatisch zurueckgerollt.
hermes-postcheck.sh prueft vier Dinge, die es seit dem 07.08. nicht mehr
gibt (Sidecar :8765, /api/memory, memory.provider, mc2-memory-Plugin).
Die Checks konnten nicht gruen werden -> autoupdate.sh wertete jedes
Update als rot und rollte es zurueck. Checks ersatzlos entfernt; der
Tool-Smoke laeuft ohnehin durch den echten Agenten.
3. 7 von 12 Skills waren per Knopfdruck nicht startbar.
deploy.sh kopiert Skills mit tr '-' '_' nach ~/.hermes/skills/,
routers/skills.py gab Hermes aber den Ordnernamen MIT Bindestrich.
Der Knopf meldete Erfolg, ausgefuehrt wurde nichts. Neu: _hermes_name().
4. deploy.sh warf bei jedem Deploy die Live-Modellkonfiguration weg.
MC2 schreibt /etc/llama-swap/config.yaml selbst; die Repo-Datei ist nur
ein Abzug (ihm fehlt u.a. kritiker/Devstral). Jetzt: erst sichern, Diff
zeigen, dann kopieren. MC_DEPLOY_SKIP_SWAP_CONFIG=1 ueberspringt.
MEM0-AUSBAU VOLLENDET (Kriterium 3: 17 -> 0 Dateien)
- mem0_service/, mcp/mcp_memory.py und hermes/plugins/mc2-memory entfernt;
das Plugin schickte bei JEDEM Turn zwei 404-Requests an tote Routen.
- MEMORY_DB/MEM0_SERVICE_URL, _mem0_reachable(), MC_MEMORY_DB und
MC_MEM_DEDUPE_ENABLED aus Config/Router/Unit entfernt.
- mem0_ms war strukturell tot (park("retrieve") wird nirgends mehr
aufgerufen) -> aus Backend, API-Typ und Latenzkarte entfernt.
- Verbinden-Tab: tote Gedaechtnis-MCP-Leitung raus, Status-Kachel bleibt.
- AGENTS.md beschrieb Mem0 noch als aktiv - korrigiert.
GATEWAY-ROBUSTHEIT
- _proxy gab bei ungueltigen Payloads HTTP 500 (gemessen 5/5: Rohtext,
leerer Body, JSON-Liste, JSON-String, null) -> jetzt 5/5 HTTP 400.
- Bild-Weiche ohne Deckel: 10 Bilder x 2 Versuche x 240 s hielten den
Client bis zu 80 min. Neu: MC_CODER_IMAGE_MAX (4), Rueckfall auf die
Vision-Umleitung.
- /v1/models: nicht-JSON von der Engine gab 500 -> jetzt 502.
UNITS UND DEPLOY
- mc2-steward.service, dessen warmset-Drop-in und voice-service.service
fehlten im Repo, obwohl maintenance.py und stack-postcheck.sh sie
voraussetzen. 1:1 von der laufenden Box uebernommen.
- deploy.sh startete mc2-steward nie neu; restore.sh liess mc2-gateway und
mc2-steward mit alter Config weiterlaufen. Beide ergaenzt.
FRONTEND
- useEigenleben rief /api/eigenleben - existiert im Backend nicht und wurde
nirgends genutzt. Samt Typen entfernt.
- Anleitung beschrieb einen Gedaechtnis-Tab, den es nicht gibt.
- Abgeglichen: alle uebrigen 63 Frontend-Aufrufe treffen echte Routen, alle
5 SSE-Invalidation-Keys sind gemappt, keine ungefangenen Promises.
Gates: compileall gruen - ruff "All checks passed" - tsc gruen - vite build
gruen (dist aktualisiert) - Importe app/steward/gateway_app gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
34a9862591
commit
84dc34c0a3
@@ -1,11 +1,5 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mc2-memory": {
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mc2-system": {
|
"mc2-system": {
|
||||||
"command": "python",
|
"command": "python",
|
||||||
"args": [
|
"args": [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI +
|
|||||||
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
||||||
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
||||||
- **Inference-Limit:** Modelle im GGUF-Format dürfen maximal ca. 100-110 GB groß sein (entspricht ca. 150B Parametern bei Q4_K_M). Größere Modelle (wie 200B+ oder 2T Parameter) **können lokal nicht ausgeführt werden** und sind auszuschließen, es sei denn, es handelt sich um stark quantisierte MoEs.
|
- **Inference-Limit:** Modelle im GGUF-Format dürfen maximal ca. 100-110 GB groß sein (entspricht ca. 150B Parametern bei Q4_K_M). Größere Modelle (wie 200B+ oder 2T Parameter) **können lokal nicht ausgeführt werden** und sind auszuschließen, es sei denn, es handelt sich um stark quantisierte MoEs.
|
||||||
**Gedächtnis & Dienste:** Hermes ist das autonome Agenten-Gehirn („Lucy"). Mem0 läuft als dedizierter semantischer Langzeit-Gedächtnis-Sidecar (`mem0-service` auf `:8765`) und wird von Hermes via MCP (`mcp_memory.py`) eingebunden. Governor, Zed-Workflows und alte Mittelschichten sind komplett gelöscht.
|
**Gedächtnis & Dienste:** Hermes ist das autonome Agenten-Gehirn („Lucy") **und die alleinige Gedächtnis-Wahrheit** — es führt sein Gedächtnis selbst. Der frühere Sidecar auf `:8765` samt Memory-MCP-Server und MC2-Memory-Plugin wurde am 07.08.2026 abgelöst und am 27.08.2026 restlos ausgebaut (`docs/wissen/VERDIKTE.md`): MC2 hat **keine** `/api/memory`-Routen mehr, nichts darf mehr dorthin greifen. Governor, Zed-Workflows und alte Mittelschichten sind komplett gelöscht.
|
||||||
|
|
||||||
## Sprache (nicht verhandelbar)
|
## Sprache (nicht verhandelbar)
|
||||||
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
||||||
|
|||||||
+6
-9
@@ -19,13 +19,9 @@ MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
|
|||||||
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
||||||
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
||||||
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
||||||
# Geteiltes Gedächtnis (SQLite, WAL). Persistent neben den Modellen.
|
# Gedächtnis: MC2 hält KEINS mehr. Bis August 2026 lief hier erst eine eigene SQLite-DB,
|
||||||
# Hinweis: nur noch für die einmalige Mem0-Migration relevant — das aktive Gedächtnis
|
# dann ein semantischer Sidecar auf :8765 — beides ist abgelöst, der Port ist tot.
|
||||||
# liegt jetzt in Mem0/Chroma hinter dem Sidecar (siehe MEM0_SERVICE_URL).
|
# Einzige Gedächtnis-Wahrheit ist jetzt Hermes selbst (HERMES_API_URL, siehe unten).
|
||||||
MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mc2-memory.db")))
|
|
||||||
# Mem0-Sidecar (auto-lernendes, semantisches Gedächtnis). Läuft im ~/.mem0/venv (Python 3.12),
|
|
||||||
# weil mem0+chromadb unter dem 3.14-Backend nicht laufen. MC2 spricht ihn lokal per HTTP an.
|
|
||||||
MEM0_SERVICE_URL = os.environ.get("MC_MEM0_SERVICE_URL", "http://127.0.0.1:8765").rstrip("/")
|
|
||||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||||
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
||||||
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
||||||
@@ -72,7 +68,7 @@ V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
|||||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||||
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
||||||
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
||||||
# (Tools + geteiltes Mem0) wie CLI/Telegram, nur über HTTP.
|
# (Tools + eigenes Gedächtnis) wie CLI/Telegram, nur über HTTP.
|
||||||
def _read_hermes_env(key: str) -> str:
|
def _read_hermes_env(key: str) -> str:
|
||||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
||||||
@@ -97,7 +93,8 @@ HERMES_API_KEY = (
|
|||||||
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||||
|
|
||||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
||||||
# Eigenes Python-3.12-venv (~/.voice/venv), analog Mem0-Sidecar. MC2 proxyt nach außen.
|
# Eigenes Python-3.12-venv (~/.voice/venv), weil Parakeet/Piper nicht ins 3.14-Backend-venv
|
||||||
|
# passen. MC2 proxyt nach außen.
|
||||||
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
||||||
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
||||||
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Connect-Endpoint: erzeugt IDE-/Agent-Snippets (auf den Gateway + Memory-MCP)."""
|
"""Connect-Endpoint: erzeugt IDE-/Agent-Snippets für den Gateway."""
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from services.connect import DEFAULT_HOST, build_snippets, check_health
|
from services.connect import DEFAULT_HOST, build_snippets, check_health
|
||||||
@@ -7,14 +7,11 @@ router = APIRouter(prefix="/api")
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/connect")
|
@router.get("/connect")
|
||||||
def connect(host: str = DEFAULT_HOST, mcp_path: str | None = None) -> dict:
|
def connect(host: str = DEFAULT_HOST) -> dict:
|
||||||
kwargs = {}
|
return build_snippets(host=host)
|
||||||
if mcp_path:
|
|
||||||
kwargs["mcp_script_path"] = mcp_path
|
|
||||||
return build_snippets(host=host, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/connect/health")
|
@router.get("/connect/health")
|
||||||
def connect_health() -> dict:
|
def connect_health() -> dict:
|
||||||
"""Live-Status der zwei Leitungen (Gateway + Gedächtnis) für den Verbinden-Tab."""
|
"""Live-Status der drei Leitungen (Gateway, Gedächtnis, Desktop-Gateway) für den Verbinden-Tab."""
|
||||||
return check_health()
|
return check_health()
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ _LANG_DIRECTIVE = os.environ.get(
|
|||||||
"Quellcode, Bezeichner und Shell-Befehle bleiben unverändert.")
|
"Quellcode, Bezeichner und Shell-Befehle bleiben unverändert.")
|
||||||
|
|
||||||
|
|
||||||
|
# Deckel für die Bild-Weiche. Ohne ihn wächst die Wartezeit linear mit der Bildzahl:
|
||||||
|
# je Bild bis zu 2 Versuche à _BILD_TIMEOUT_S, und der Client hängt so lange am offenen
|
||||||
|
# Request. Über _BILD_MAX Bilder wird gar nicht erst beschrieben — der Aufrufer fällt
|
||||||
|
# dann auf die normale Vision-Umleitung zurück (ehrlich langsam statt scheinbar hängend).
|
||||||
|
_BILD_TIMEOUT_S = float(os.environ.get("MC_CODER_IMAGE_TIMEOUT_S", "240"))
|
||||||
|
_BILD_MAX = int(os.environ.get("MC_CODER_IMAGE_MAX", "4"))
|
||||||
|
|
||||||
# Bild-Beschreibung für Coder-Ziele: Prompt bewusst auf wörtliche Wiedergabe von
|
# Bild-Beschreibung für Coder-Ziele: Prompt bewusst auf wörtliche Wiedergabe von
|
||||||
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
||||||
_BILD_BESCHREIB_PROMPT = os.environ.get(
|
_BILD_BESCHREIB_PROMPT = os.environ.get(
|
||||||
@@ -85,6 +92,10 @@ async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str)
|
|||||||
for i, part in enumerate(m["content"]):
|
for i, part in enumerate(m["content"]):
|
||||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||||
fundstellen.append((m, i, part))
|
fundstellen.append((m, i, part))
|
||||||
|
if len(fundstellen) > _BILD_MAX:
|
||||||
|
log.warning("Bild-Weiche v2: %s Bilder (Deckel %s) — Request geht an das Vision-Modell "
|
||||||
|
"statt einzeln beschrieben zu werden", len(fundstellen), _BILD_MAX)
|
||||||
|
return False
|
||||||
for nr, (msg, idx, part) in enumerate(fundstellen, start=1):
|
for nr, (msg, idx, part) in enumerate(fundstellen, start=1):
|
||||||
frage = {
|
frage = {
|
||||||
"model": vision_alias,
|
"model": vision_alias,
|
||||||
@@ -99,7 +110,7 @@ async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str)
|
|||||||
text = ""
|
text = ""
|
||||||
for versuch in (1, 2):
|
for versuch in (1, 2):
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=240.0) as c:
|
async with httpx.AsyncClient(timeout=_BILD_TIMEOUT_S) as c:
|
||||||
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||||
@@ -138,7 +149,15 @@ def _inject_language(body: dict, alias: str) -> None:
|
|||||||
async def models(request: Request):
|
async def models(request: Request):
|
||||||
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
||||||
r = await client.get(f"{LLAMA_SWAP_URL}/v1/models", timeout=10.0)
|
r = await client.get(f"{LLAMA_SWAP_URL}/v1/models", timeout=10.0)
|
||||||
data = r.json()
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except ValueError:
|
||||||
|
# Engine antwortet, aber nicht mit JSON (Startphase/Fehlerseite) — ehrlicher 502
|
||||||
|
# statt eines 500ers aus dem Parser.
|
||||||
|
log.warning("/v1/models: Engine-Antwort ist kein JSON (HTTP %s): %.200s", r.status_code, r.text)
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": {"message": "Engine lieferte keine gültige Modell-Liste.",
|
||||||
|
"type": "upstream_error"}}, status_code=502)
|
||||||
# Aufgeräumt (07/2026): Die Router-Lanes (coding/chat) werden NICHT mehr als „Modell"
|
# Aufgeräumt (07/2026): Die Router-Lanes (coding/chat) werden NICHT mehr als „Modell"
|
||||||
# angeboten — sie verwirrten (coding vs. coder) und seit dem Zed-Aus wählt sie niemand
|
# angeboten — sie verwirrten (coding vs. coder) und seit dem Zed-Aus wählt sie niemand
|
||||||
# mehr. Das Routing bleibt erhalten (siehe _proxy), sie stehen nur nicht mehr in der Liste.
|
# mehr. Das Routing bleibt erhalten (siehe _proxy), sie stehen nur nicht mehr in der Liste.
|
||||||
@@ -187,7 +206,20 @@ async def models(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
async def _proxy(path: str, request: Request):
|
async def _proxy(path: str, request: Request):
|
||||||
body = await request.json()
|
# Ungültige Payloads gehören dem Client, nicht dem Server: ohne diese Prüfung wirft
|
||||||
|
# request.json() bzw. body.get(...) durch und der Aufrufer bekommt einen 500er
|
||||||
|
# (gemessen 27.08.2026: Rohtext, leerer Body, JSON-Liste, JSON-String und null — 5/5 mal 500).
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except Exception:
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": {"message": "Ungültiger Request-Body: JSON erwartet.",
|
||||||
|
"type": "invalid_request_error"}}, status_code=400)
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": {"message": "Ungültiger Request-Body: JSON-Objekt erwartet, "
|
||||||
|
f"'{type(body).__name__}' bekommen.",
|
||||||
|
"type": "invalid_request_error"}}, status_code=400)
|
||||||
_apply_no_think_marker(body) # Lucy-Voice-Schnellspur: Marker strippen + Thinking aus
|
_apply_no_think_marker(body) # Lucy-Voice-Schnellspur: Marker strippen + Thinking aus
|
||||||
requested = str(body.get("model") or "auto")
|
requested = str(body.get("model") or "auto")
|
||||||
if requested.lower() in ("auto", "chat", "coding"):
|
if requested.lower() in ("auto", "chat", "coding"):
|
||||||
|
|||||||
@@ -7,6 +7,20 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
def _hermes_name(ordner: str) -> str:
|
||||||
|
"""Repo-Ordnername -> Hermes-Skillname.
|
||||||
|
|
||||||
|
deploy/deploy.sh kopiert `deploy/skills/<name>` nach `~/.hermes/skills/<name mit _>`
|
||||||
|
(`tr '-' '_'`), weil Hermes keine Bindestriche in Skillnamen mag. Ohne dieselbe
|
||||||
|
Umschreibung hier bekam Hermes den Ordnernamen MIT Bindestrich und fand den Skill
|
||||||
|
nicht - betroffen waren 7 der 12 Skills (betrieb-playbook, konzept-fliessband,
|
||||||
|
llm-wiki, morning-report, pc-pfad-cache, projekt-start, trend-radar): der Startknopf
|
||||||
|
meldete Erfolg, ausgefuehrt wurde nichts. deploy.sh bleibt die Quelle der Wahrheit.
|
||||||
|
"""
|
||||||
|
return ordner.replace("-", "_")
|
||||||
|
|
||||||
|
|
||||||
class RunSkillRequest(BaseModel):
|
class RunSkillRequest(BaseModel):
|
||||||
skill_name: str
|
skill_name: str
|
||||||
|
|
||||||
@@ -57,8 +71,11 @@ def list_skills():
|
|||||||
|
|
||||||
skills.append({
|
skills.append({
|
||||||
"name": entry.name,
|
"name": entry.name,
|
||||||
|
# Der Name, unter dem Hermes den Skill kennt - auch fuer den Laufend-
|
||||||
|
# Vergleich, weil im Prozess-cmdline genau dieser Name steht.
|
||||||
|
"hermes_name": _hermes_name(entry.name),
|
||||||
"description": description or "Keine Beschreibung verfügbar.",
|
"description": description or "Keine Beschreibung verfügbar.",
|
||||||
"running": entry.name in running_skills
|
"running": _hermes_name(entry.name) in running_skills
|
||||||
})
|
})
|
||||||
return {"skills": skills}
|
return {"skills": skills}
|
||||||
|
|
||||||
@@ -70,13 +87,15 @@ def run_skill(req: RunSkillRequest):
|
|||||||
# Fallback falls lokal (auf Windows) entwickelt wird
|
# Fallback falls lokal (auf Windows) entwickelt wird
|
||||||
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
||||||
|
|
||||||
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat
|
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat - mit dem Namen, den Hermes
|
||||||
cmd = [hermes_bin, "-z", f"Führe den '{req.skill_name}' Skill aus", "chat"]
|
# nach dem Deploy-Sync wirklich kennt (Bindestriche -> Unterstriche, s. _hermes_name).
|
||||||
|
skill = _hermes_name(req.skill_name)
|
||||||
|
cmd = [hermes_bin, "-z", f"Führe den '{skill}' Skill aus", "chat"]
|
||||||
try:
|
try:
|
||||||
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{req.skill_name.replace('/', '_')}.log")
|
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{skill.replace('/', '_')}.log")
|
||||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||||
with open(log_path, "a") as f:
|
with open(log_path, "a", encoding="utf-8") as f:
|
||||||
f.write(f"\n--- Starting Skill: {req.skill_name} ---\n")
|
f.write(f"\n--- Starting Skill: {skill} ---\n")
|
||||||
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
||||||
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from services import backup as backup_svc
|
from services import backup as backup_svc
|
||||||
@@ -42,13 +42,6 @@ def history(minutes: int = 60) -> dict:
|
|||||||
return metrics_history.history(minutes)
|
return metrics_history.history(minutes)
|
||||||
|
|
||||||
|
|
||||||
def _mem0_reachable() -> bool:
|
|
||||||
try:
|
|
||||||
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _voice_reachable() -> bool:
|
def _voice_reachable() -> bool:
|
||||||
try:
|
try:
|
||||||
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Voice-Endpoints für „Mit Hermes reden" (Browser-Voice + 3D-Avatar).
|
|||||||
|
|
||||||
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
||||||
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
||||||
geteiltem Mem0 wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
eigenem Gedächtnis wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
||||||
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
||||||
|
|
||||||
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
||||||
@@ -169,7 +169,7 @@ def voice_metrics() -> dict:
|
|||||||
@router.get("/voice/trace")
|
@router.get("/voice/trace")
|
||||||
def voice_trace(limit: int = 20) -> dict:
|
def voice_trace(limit: int = 20) -> dict:
|
||||||
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
||||||
Generierung, Mem0 als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
Generierung). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||||
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
||||||
return {"turns": get_trace(limit)}
|
return {"turns": get_trace(limit)}
|
||||||
|
|
||||||
@@ -289,8 +289,8 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
headers["X-Hermes-Session-Key"] = body.session_key
|
headers["X-Hermes-Session-Key"] = body.session_key
|
||||||
|
|
||||||
async def gen():
|
async def gen():
|
||||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Mem0
|
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung zu EINEM
|
||||||
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
# Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||||
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
||||||
first = True
|
first = True
|
||||||
first_content = True
|
first_content = True
|
||||||
@@ -325,7 +325,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
||||||
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
||||||
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
||||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
|
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py).
|
||||||
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||||
try:
|
try:
|
||||||
@@ -342,7 +342,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
||||||
trace.note_ttfb()
|
trace.note_ttfb()
|
||||||
first = False
|
first = False
|
||||||
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT) —
|
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT) —
|
||||||
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
||||||
if first_content and b'"content"' in chunk:
|
if first_content and b'"content"' in chunk:
|
||||||
trace.note_first_content()
|
trace.note_first_content()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def list_snapshots() -> dict:
|
|||||||
|
|
||||||
@router.get("/zeitmaschine/inhalt")
|
@router.get("/zeitmaschine/inhalt")
|
||||||
def snapshot_contents(file: str) -> dict:
|
def snapshot_contents(file: str) -> dict:
|
||||||
"""Top-Level-Komponenten eines Snapshots (mem0, hermes, llama-swap, MANIFEST …)."""
|
"""Top-Level-Komponenten eines Snapshots (hermes, llama-swap, MANIFEST …)."""
|
||||||
if not _FILE_RX.match(file):
|
if not _FILE_RX.match(file):
|
||||||
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
||||||
p = backup_svc.BACKUP_DIR / file
|
p = backup_svc.BACKUP_DIR / file
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Voll-Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config).
|
Voll-Zustands-Backup (Hermes-Configs/Secrets + llama-swap config).
|
||||||
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
|
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
|
||||||
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
|
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
|
||||||
LOKALEN PC (separate Maschine im LAN). Alle zeigen auf den **Gateway** der Box
|
LOKALEN PC (separate Maschine im LAN). Alle zeigen auf den **Gateway** der Box
|
||||||
(reale Modelle coder/heavy/vision, Cockpit-Port :9001/v1) + den **Shared-Memory-MCP** (MC :9001).
|
(reale Modelle coder/heavy/vision, Cockpit-Port :9001/v1).
|
||||||
|
|
||||||
|
Gedächtnis ist hier bewusst KEINE eigene Leitung mehr: bis August 2026 gab es dafür einen
|
||||||
|
Memory-MCP-Server gegen MC2s /api/memory; seit dessen Ablösung (docs/wissen/VERDIKTE.md,
|
||||||
|
07.08.2026) führt Hermes sein Gedächtnis
|
||||||
|
selbst — es hängt am Agenten, nicht am Gateway. check_health() prüft es weiterhin (Leitung 2),
|
||||||
|
es ist nur nichts mehr zu konfigurieren.
|
||||||
|
|
||||||
Wichtig: Host ist die LAN-IP der Box (NICHT eine Proxy-Domain) — das war in v1
|
Wichtig: Host ist die LAN-IP der Box (NICHT eine Proxy-Domain) — das war in v1
|
||||||
die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
||||||
@@ -40,9 +46,7 @@ def _gw(host: str) -> str:
|
|||||||
return f"http://{host}:{PORT}/v1"
|
return f"http://{host}:{PORT}/v1"
|
||||||
|
|
||||||
|
|
||||||
def build_snippets(host: str = DEFAULT_HOST,
|
def build_snippets(host: str = DEFAULT_HOST) -> dict:
|
||||||
mcp_script_path: str = r"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py",
|
|
||||||
mcp_python: str = "python") -> dict:
|
|
||||||
gw = _gw(host)
|
gw = _gw(host)
|
||||||
mc_url = f"http://{host}:{PORT}"
|
mc_url = f"http://{host}:{PORT}"
|
||||||
|
|
||||||
@@ -88,16 +92,6 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
f'export ANTHROPIC_MODEL="coder"'
|
f'export ANTHROPIC_MODEL="coder"'
|
||||||
)
|
)
|
||||||
|
|
||||||
memory_mcp = json.dumps({
|
|
||||||
"mcpServers": {
|
|
||||||
"mission-control-memory": {
|
|
||||||
"command": mcp_python,
|
|
||||||
"args": [mcp_script_path],
|
|
||||||
"env": {"MC_URL": mc_url},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, indent=2)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"host": host,
|
"host": host,
|
||||||
"gateway_url": gw,
|
"gateway_url": gw,
|
||||||
@@ -116,11 +110,8 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · "
|
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · "
|
||||||
"Ask/Debug→Lane 'chat' (virtuelles Modell, wählt selbst fast oder heavy)."},
|
"Ask/Debug→Lane 'chat' (virtuelles Modell, wählt selbst fast oder heavy)."},
|
||||||
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
|
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
|
||||||
"note": "Für die starken Sessions. Lokal-Betrieb bräuchte einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway; Gedächtnis-Anbindung via Memory-MCP unten."},
|
"note": "Für die starken Sessions. Lokal-Betrieb bräuchte einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway."},
|
||||||
},
|
},
|
||||||
# Leitung 2 — das GEDÄCHTNIS. Separater MCP-Server, gilt zusätzlich zu jedem Tool oben.
|
|
||||||
"memory": {"label": "Shared Memory (MCP)", "lang": "json", "snippet": memory_mcp,
|
|
||||||
"note": "Eigene Leitung: MCP-Block für jedes MCP-fähige Tool. mcp_memory.py muss lokal liegen."},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
||||||
|
|
||||||
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway, Mem0,
|
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway,
|
||||||
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
|
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
|
||||||
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ Zwei Sichten auf dieselben Messungen:
|
|||||||
|
|
||||||
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
||||||
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
||||||
**Mem0-Retrieve** läuft zwar in Hermes, ruft aber MC2s `/api/memory` per HTTP zurück → messbar und als
|
**Gedächtnis-Abruf** dagegen läuft seit der Ablösung des Sidecars (07.08.2026) Hermes-intern — es
|
||||||
Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden** dagegen laufen
|
gibt keinen Rückruf an MC2 mehr, also auch keine Messung; er steckt jetzt in der Hirn-Zeit. Ebenso die
|
||||||
im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im „Generierung"-Bucket.
|
**Tool-Runden**: im Hermes-LLM-Loop ohne Callback an MC2 → unsichtbar, im „Generierung"-Bucket.
|
||||||
|
|
||||||
STT (davor) und Mem0-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
STT läuft als eigener HTTP-Request VOR dem Turn, ohne Turn-ID. Auf einem EIN-Nutzer-Gerät genügt eine
|
||||||
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
|
schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer wird global „geparkt" und vom
|
||||||
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
|
nächsten Chat-Turn eingesammelt (mit Frist-Check). Kein Turn-ID-Durchreichen durch den Lucy-Client
|
||||||
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
|
nötig.
|
||||||
|
|
||||||
In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset).
|
In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset).
|
||||||
"""
|
"""
|
||||||
@@ -34,11 +34,11 @@ _STAGES: dict[str, deque] = {}
|
|||||||
_TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-Trace)
|
_TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-Trace)
|
||||||
|
|
||||||
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
||||||
STAGES = ("stt", "vision", "memory_retrieve", "chat_ttfb", "chat_first_content", "tts")
|
STAGES = ("stt", "vision", "chat_ttfb", "chat_first_content", "tts")
|
||||||
|
|
||||||
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Mem0-Retrieve, je
|
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer als
|
||||||
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
|
# (ms, perf_counter-Zeitstempel). Der nächste Chat-Turn sammelt sie ein und leert sie.
|
||||||
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
|
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None}
|
||||||
|
|
||||||
|
|
||||||
def record_stage(stage: str, ms: float) -> None:
|
def record_stage(stage: str, ms: float) -> None:
|
||||||
@@ -53,8 +53,8 @@ def record_stage(stage: str, ms: float) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def park(kind: str, ms: float) -> None:
|
def park(kind: str, ms: float) -> None:
|
||||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Mem0-Retrieve als
|
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT läuft davor), global parken,
|
||||||
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
|
damit der nächste Chat-Turn sie einsammeln kann."""
|
||||||
if ms is None or ms < 0 or kind not in _PARKED:
|
if ms is None or ms < 0 or kind not in _PARKED:
|
||||||
return
|
return
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
@@ -71,17 +71,6 @@ def _take_stt(max_age: float = 20.0) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
|
|
||||||
"""Geparkten Mem0-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
|
||||||
Turns) und frisch ist."""
|
|
||||||
with _LOCK:
|
|
||||||
v = _PARKED.get("retrieve")
|
|
||||||
if v and v[1] >= since_perf and (time.perf_counter() - v[1]) <= max_age:
|
|
||||||
_PARKED["retrieve"] = None
|
|
||||||
return round(v[0], 1)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Timer:
|
class Timer:
|
||||||
"""Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`.
|
"""Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`.
|
||||||
Funktioniert um `await`-Aufrufe herum (enter → await → exit)."""
|
Funktioniert um `await`-Aufrufe herum (enter → await → exit)."""
|
||||||
@@ -102,8 +91,7 @@ class TurnTrace:
|
|||||||
"""Ein Per-Turn-Trace für den Voice/Lucy-Chatpfad. In `voice.py` über die Dauer eines Chat-Turns
|
"""Ein Per-Turn-Trace für den Voice/Lucy-Chatpfad. In `voice.py` über die Dauer eines Chat-Turns
|
||||||
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
||||||
|
|
||||||
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
|
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen."""
|
||||||
Unter-Detail: mem0 (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
|
||||||
|
|
||||||
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
||||||
self.id = uuid.uuid4().hex[:8]
|
self.id = uuid.uuid4().hex[:8]
|
||||||
@@ -113,7 +101,7 @@ class TurnTrace:
|
|||||||
self.session_id = (session_id or "")[:24]
|
self.session_id = (session_id or "")[:24]
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
||||||
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Mem0 + TTFT)
|
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Gedächtnis + TTFT)
|
||||||
self.had_images = False
|
self.had_images = False
|
||||||
self.error: str | None = None
|
self.error: str | None = None
|
||||||
|
|
||||||
@@ -130,7 +118,7 @@ class TurnTrace:
|
|||||||
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
||||||
|
|
||||||
def note_first_content(self) -> None:
|
def note_first_content(self) -> None:
|
||||||
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT)."""
|
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT)."""
|
||||||
ms = (time.perf_counter() - self._brain0) * 1000.0
|
ms = (time.perf_counter() - self._brain0) * 1000.0
|
||||||
self.hirn_ms = round(ms, 1)
|
self.hirn_ms = round(ms, 1)
|
||||||
record_stage("chat_first_content", ms)
|
record_stage("chat_first_content", ms)
|
||||||
@@ -138,7 +126,6 @@ class TurnTrace:
|
|||||||
def commit(self) -> dict:
|
def commit(self) -> dict:
|
||||||
total = (time.perf_counter() - self.perf0) * 1000.0
|
total = (time.perf_counter() - self.perf0) * 1000.0
|
||||||
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
||||||
mem0 = _take_retrieve(self.perf0) # rollend bereits in /api/memory erfasst
|
|
||||||
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
||||||
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
||||||
rec = {
|
rec = {
|
||||||
@@ -151,7 +138,6 @@ class TurnTrace:
|
|||||||
"vision_ms": self.vision_ms,
|
"vision_ms": self.vision_ms,
|
||||||
"hirn_ms": self.hirn_ms,
|
"hirn_ms": self.hirn_ms,
|
||||||
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
||||||
"mem0_ms": mem0,
|
|
||||||
"total_ms": round(total, 1),
|
"total_ms": round(total, 1),
|
||||||
"error": self.error,
|
"error": self.error,
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-8
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
||||||
|
|
||||||
Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am
|
Bisher hingen Re-Warm-Wächter und Health-Wächter (sentry) am
|
||||||
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
||||||
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
||||||
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
||||||
@@ -11,14 +11,16 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
|||||||
In-Process-Nudge aus llamaswap.write_config)
|
In-Process-Nudge aus llamaswap.write_config)
|
||||||
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
||||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
||||||
• memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar)
|
|
||||||
|
Das dritte Loop (Gedächtnis-Dubletten gegen den Sidecar auf :8765) ist mit dessen Ablösung
|
||||||
|
am 07.08.2026 entfallen — Hermes führt sein Gedächtnis selbst (docs/wissen/VERDIKTE.md).
|
||||||
|
|
||||||
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
|
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
|
||||||
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
||||||
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
||||||
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
||||||
|
|
||||||
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED)
|
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED)
|
||||||
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
||||||
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +30,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from config import CONFIG_PATH
|
from config import CONFIG_PATH
|
||||||
from services import memory as memory_svc
|
|
||||||
from services import sentry, warmer
|
from services import sentry, warmer
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -66,10 +67,6 @@ async def main() -> None:
|
|||||||
warmer.INTERVAL, CONFIG_WATCH_S)
|
warmer.INTERVAL, CONFIG_WATCH_S)
|
||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
|
||||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
|
||||||
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
|
||||||
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
|
|
||||||
if not tasks:
|
if not tasks:
|
||||||
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ hint=""
|
|||||||
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
||||||
ctx="$(cat <<EOF
|
ctx="$(cat <<EOF
|
||||||
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
||||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weiterer Dienst: hermes-gateway :8642 (fuehrt sein Gedaechtnis selbst — es gibt KEINEN separaten Gedaechtnis-Dienst mehr). Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
||||||
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
||||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||||
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ check_hermes(){
|
|||||||
done
|
done
|
||||||
if [ "$state" = "done" ]; then
|
if [ "$state" = "done" ]; then
|
||||||
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
||||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Mem0, Tools, Voice) grün — alles läuft."
|
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Patches, Tools, Voice) grün — alles läuft."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
||||||
|
|||||||
+8
-9
@@ -3,8 +3,10 @@
|
|||||||
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
||||||
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
||||||
#
|
#
|
||||||
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) ·
|
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
|
||||||
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.)
|
# known-good/ (Versions-Manifest, s. u.)
|
||||||
|
# Das abgeloeste Gedaechtnis-Sidecar (/srv/models, Port 8765) faellt seit 27.08.2026 weg —
|
||||||
|
# sein Datenverzeichnis existiert nicht mehr (Hintergrund: docs/wissen/VERDIKTE.md).
|
||||||
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
||||||
#
|
#
|
||||||
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
||||||
@@ -20,16 +22,14 @@ RETAIN="${MC_BACKUP_RETAIN:-14}"
|
|||||||
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
||||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
TS="$(date +%Y%m%d-%H%M%S)"
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
STAGE="$(mktemp -d)"
|
STAGE="$(mktemp -d)"
|
||||||
trap 'rm -rf "$STAGE"' EXIT
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
mkdir -p "$STAGE/mem0" "$STAGE/hermes" "$STAGE/llama-swap"
|
mkdir -p "$STAGE/hermes" "$STAGE/llama-swap"
|
||||||
|
|
||||||
[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true
|
|
||||||
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
||||||
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
||||||
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
||||||
@@ -60,7 +60,7 @@ SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
|||||||
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
||||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||||
|
|
||||||
# pip freeze je venv; mem0-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
# pip freeze je venv; das voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||||
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||||
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
||||||
"$1" -m pip freeze > "$2" 2>/dev/null \
|
"$1" -m pip freeze > "$2" 2>/dev/null \
|
||||||
@@ -68,7 +68,6 @@ freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
|||||||
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
||||||
}
|
}
|
||||||
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
||||||
freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"
|
|
||||||
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -79,7 +78,7 @@ freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
|||||||
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
||||||
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
||||||
echo "python-venvs :"
|
echo "python-venvs :"
|
||||||
for P in "$SRC/backend/.venv/bin/python" "$HOME/.mem0/venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
for P in "$SRC/backend/.venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
||||||
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
||||||
done
|
done
|
||||||
} > "$KG/versions.txt" || true
|
} > "$KG/versions.txt" || true
|
||||||
@@ -92,7 +91,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
|
|||||||
mc2-state backup
|
mc2-state backup
|
||||||
created : $TS
|
created : $TS
|
||||||
host : $(hostname)
|
host : $(hostname)
|
||||||
inhalt : mem0 (chroma + history.db), hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
inhalt : hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
||||||
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
+28
-5
@@ -12,11 +12,30 @@ echo "Aktualisiere Python Abhängigkeiten..."
|
|||||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||||
|
|
||||||
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
||||||
|
#
|
||||||
|
# ‼️ ACHTUNG (27.08.2026): /etc/llama-swap/config.yaml ist die LEBENDE Wahrheit — MC2
|
||||||
|
# schreibt sie selbst (backend/services/llamaswap.py::write_config), sobald jemand über
|
||||||
|
# die Oberfläche ein Modell hinzufügt oder eine Rolle ändert. deploy/llama-swap.config.yaml
|
||||||
|
# ist nur ein Abzug und hinkt regelmäßig hinterher (aktuell fehlt ihm z. B. der
|
||||||
|
# `kritiker`/Devstral-Eintrag, den STACK.md und der Modell-Katalog voraussetzen).
|
||||||
|
# Bis hierher wurde die Live-Datei kommentarlos mit dem Abzug überschrieben — alles per
|
||||||
|
# UI Hinzugefügte war nach jedem Deploy weg. Jetzt: erst sichern, dann kopieren, und die
|
||||||
|
# Abweichung sichtbar machen. MC_DEPLOY_SKIP_SWAP_CONFIG=1 überspringt den Schritt ganz.
|
||||||
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
||||||
echo "Llama-Swap Konfiguration hat sich geändert, synchronisiere..."
|
if [ "${MC_DEPLOY_SKIP_SWAP_CONFIG:-0}" = "1" ]; then
|
||||||
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
echo "Llama-Swap Konfiguration weicht ab — uebersprungen (MC_DEPLOY_SKIP_SWAP_CONFIG=1)."
|
||||||
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
else
|
||||||
sudo systemctl restart llama-swap
|
SWAP_BAK="/etc/llama-swap/config.yaml.bak-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
echo "Llama-Swap Konfiguration hat sich geändert."
|
||||||
|
echo " Abweichung (live -> Repo-Abzug):"
|
||||||
|
diff -u /etc/llama-swap/config.yaml deploy/llama-swap.config.yaml | head -40 || true
|
||||||
|
echo " Sichere die laufende Datei nach $SWAP_BAK"
|
||||||
|
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
||||||
|
sudo cp /etc/llama-swap/config.yaml "$SWAP_BAK"
|
||||||
|
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
||||||
|
sudo systemctl restart llama-swap
|
||||||
|
echo " Rueckweg bei Verlust: sudo cp $SWAP_BAK /etc/llama-swap/config.yaml"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||||
@@ -35,7 +54,11 @@ for skill_dir in deploy/skills/*; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# 5. Dienste neu starten
|
# 5. Dienste neu starten
|
||||||
|
# ‼️ mc2-steward gehoert hier dazu: er laeuft aus DEMSELBEN Checkout (backend/steward.py).
|
||||||
|
# Ohne ihn lief der Waechter-Prozess nach jedem Deploy mit dem alten Code weiter — der
|
||||||
|
# Neustart wurde 27.08.2026 nachgetragen. `--user restart` auf einer nicht installierten
|
||||||
|
# Unit ist folgenlos, darum ohne Vorabpruefung.
|
||||||
echo "Starte Backend neu..."
|
echo "Starte Backend neu..."
|
||||||
systemctl --user restart mc2-gateway.service mission-control-2.service
|
systemctl --user restart mc2-gateway.service mission-control-2.service mc2-steward.service
|
||||||
|
|
||||||
echo "✅ Deployment erfolgreich abgeschlossen!"
|
echo "✅ Deployment erfolgreich abgeschlossen!"
|
||||||
|
|||||||
+10
-31
@@ -1,35 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
# Post-Update-Check nach einem Hermes-Agent-Update: laeuft unser "Gehirn" noch?
|
||||||
# (Mem0 + die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
# Exit 0 = alles ok, sonst 1 -> der Job wird im UI rot, damit ein kaputtes Gehirn auffaellt.
|
||||||
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
#
|
||||||
|
# WICHTIG (27.08.2026) - VIER TOTE CHECKS ENTFERNT: bis hierher prueften die ersten Bloecke
|
||||||
|
# den abgeloesten Gedaechtnis-Sidecar (:8765), MC2s /api/memory und das zugehoerige
|
||||||
|
# Hermes-Plugin. Alle drei fielen am 07.08. weg (docs/wissen/VERDIKTE.md) - die Checks KONNTEN nicht
|
||||||
|
# mehr gruen werden. Folge: jedes Hermes-Update endete rot und wurde von autoupdate.sh
|
||||||
|
# zurueckgerollt. Kein Ersatz-Check auf Hermes native Gedaechtnis, weil es dafuer keinen
|
||||||
|
# nachgemessenen Endpunkt gibt - geraten waere eine Fassade. Der Tool-Smoke weiter unten
|
||||||
|
# laeuft ohnehin durch den ECHTEN Agenten und faengt ein kaputtes Gehirn.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||||
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
|
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
||||||
|
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
|
||||||
else
|
|
||||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if curl -sf -m 8 "$MC_URL/api/memory" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · /api/memory antwortet"
|
|
||||||
else
|
|
||||||
echo "FAIL · /api/memory antwortet nicht"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
|
|
||||||
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
|
|
||||||
echo "PASS · memory.provider=mc2-memory aktiv"
|
|
||||||
else
|
|
||||||
echo "FAIL · memory.provider nicht mehr gesetzt (Config vom Update überschrieben?)"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Hermes-Runtime-Patch-Traeger (20.07.2026): ein Update setzt den Quellbaum ---
|
# --- Hermes-Runtime-Patch-Traeger (20.07.2026): ein Update setzt den Quellbaum ---
|
||||||
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
||||||
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
||||||
@@ -49,14 +36,6 @@ else
|
|||||||
echo "WARN · hermes-patches/apply.py nicht gefunden ($PATCH_APPLY)"
|
echo "WARN · hermes-patches/apply.py nicht gefunden ($PATCH_APPLY)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ( cd "$HERMES/hermes-agent" && HERMES_HOME="$HERMES" ./venv/bin/python -c \
|
|
||||||
"import sys; sys.path.insert(0,'.'); from plugins.memory import load_memory_provider; p=load_memory_provider('mc2-memory'); assert p and p.name()=='mc2-memory'" \
|
|
||||||
>/dev/null 2>&1 ); then
|
|
||||||
echo "PASS · mc2-memory-Plugin lädt unter dem neuen Hermes"
|
|
||||||
else
|
|
||||||
echo "FAIL · mc2-memory-Plugin lädt nicht (MemoryProvider-ABC geändert?)"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Config-Drift-Wächter (Lehre aus v0.18, 02.07.2026): Hermes fällt bei unbekannten ---
|
# --- Config-Drift-Wächter (Lehre aus v0.18, 02.07.2026): Hermes fällt bei unbekannten ---
|
||||||
# --- Config-Werten STILL auf Defaults zurück (approvals.mode 'auto' -> manual -> alle ---
|
# --- Config-Werten STILL auf Defaults zurück (approvals.mode 'auto' -> manual -> alle ---
|
||||||
# --- Tools hingen in pending_approval). Solche Warnungen müssen den Job ROT machen. ---
|
# --- Tools hingen in pending_approval). Solche Warnungen müssen den Job ROT machen. ---
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ globalTTL: 0
|
|||||||
|
|
||||||
models:
|
models:
|
||||||
Qwen3.6-35B-A3B:
|
Qwen3.6-35B-A3B:
|
||||||
# parallel 2 + c 131072: 2 Slots à 65k (Slot 2 = Mem0-Lern-Extraktion, blockiert Voice-Turns
|
# parallel 2 + c 131072: 2 Slots à 65k (Slot 2 = Nebenlast/Hintergrund-Extraktion, blockiert Voice-Turns
|
||||||
# nicht mehr); KV Q8_0 macht das speicherneutral zu vorher (65k f16). Bench 2026-07-02:
|
# nicht mehr); KV Q8_0 macht das speicherneutral zu vorher (65k f16). Bench 2026-07-02:
|
||||||
# Q8_0 kostet 0 t/s, MTP n-max 3 = +26% vs. ohne Spec. (Q4_K-KV stand mal im Template,
|
# Q8_0 kostet 0 t/s, MTP n-max 3 = +26% vs. ohne Spec. (Q4_K-KV stand mal im Template,
|
||||||
# lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der
|
# lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der
|
||||||
@@ -78,7 +78,7 @@ models:
|
|||||||
out: [text]
|
out: [text]
|
||||||
context: 32768
|
context: 32768
|
||||||
Qwen3-Reranker-0.6B:
|
Qwen3-Reranker-0.6B:
|
||||||
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Mem0-Suchtreffer nach echter
|
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Gedächtnis-Suchtreffer nach echter
|
||||||
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
||||||
# Winzig (~0,7 GB) → in brains (verdrängungssicher); lädt in ~1-2 s, erste Anfrage wärmt.
|
# Winzig (~0,7 GB) → in brains (verdrängungssicher); lädt in ~1-2 s, erste Anfrage wärmt.
|
||||||
cmd: |
|
cmd: |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=MC2 Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config)
|
Description=MC2 Zustands-Backup (Hermes-Configs/Secrets + llama-swap config)
|
||||||
Documentation=file:%h/mission-control-v2/docs/BACKUP.md
|
Documentation=file:%h/mission-control-v2/docs/BACKUP.md
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# systemd-USER-Unit für den MC2-Steward — die Wächter-Loops als eigener Prozess (UMBAU v3 P2).
|
||||||
|
# Re-Warm-Wächter und Health-Sentry überleben damit jeden Steuerpult-Neustart, und der Sentry
|
||||||
|
# kann auch das Steuerpult SELBST + den mc2-gateway überwachen (MC_SENTRY_WATCH_MC2=1).
|
||||||
|
# Meldungen gehen per HTTP in den MC2-Briefkasten (MC_ANNOUNCE_HTTP); ist MC2 down, greift
|
||||||
|
# weiterhin der Telegram-Direktweg (notify.sh).
|
||||||
|
# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user.
|
||||||
|
#
|
||||||
|
# ‼️ NACHGETRAGEN 27.08.2026: Diese Unit lief auf der Box, fehlte aber im Repo — sie war weder
|
||||||
|
# versioniert noch vom Disaster-Recovery abgedeckt, obwohl backend/routers/system.py,
|
||||||
|
# services/maintenance.py (USER_SERVICES) und deploy/stack-postcheck.sh sie voraussetzen.
|
||||||
|
# Inhalt 1:1 von der laufenden Box übernommen (`systemctl --user cat mc2-steward`), nur die
|
||||||
|
# Description um den entfallenen Mem0-Dedupe-Loop gekürzt. Das Warm-Set-Drop-in liegt
|
||||||
|
# daneben in mc2-steward.service.d-warmset.conf — beim Ausrollen mitnehmen.
|
||||||
|
#
|
||||||
|
# Ablage: ~/.config/systemd/user/mc2-steward.service ; dann:
|
||||||
|
# systemctl --user daemon-reload && systemctl --user enable --now mc2-steward
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=MC2 Steward (Wächter: Re-Warm, Health-Sentry)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/mission-control-v2/backend
|
||||||
|
ExecStart=%h/mission-control-v2/backend/.venv/bin/python steward.py
|
||||||
|
Environment=PYTHONPATH=%h/mission-control-v2
|
||||||
|
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||||
|
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||||
|
Environment=MC_MODELS_DIR=/srv/models
|
||||||
|
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
|
||||||
|
Environment=MC_SENTRY_WATCH_MC2=1
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Drop-in für mc2-steward.service — Ablage auf der Box:
|
||||||
|
# ~/.config/systemd/user/mc2-steward.service.d/warmset.conf
|
||||||
|
#
|
||||||
|
# 26.07.2026 — Ko-Residenz ist nicht Warm-Pflicht. Die Gruppe `brains` enthaelt seit dem
|
||||||
|
# Umbau auch Modelle MIT TTL (Coder 90 min, Kritiker 30 min); ohne diese Liste hielte der
|
||||||
|
# Waechter sie faelschlich fuer dauerhaft faellig und arbeitete gegen die TTLs.
|
||||||
|
# ANFUEHRUNGSZEICHEN SIND PFLICHT: systemd trennt Environment= an Leerzeichen, ohne sie
|
||||||
|
# kommt nur das erste Modell an (live erlebt) — das Warm-Ziel waere still zu klein.
|
||||||
|
[Service]
|
||||||
|
Environment="MC_WARMSET=Qwen3-Embedding-0.6B Qwen3-Reranker-0.6B Qwen3.6-35B-A3B"
|
||||||
|
Environment=MC_WARMUP_RERANK=reranker
|
||||||
@@ -19,19 +19,16 @@ Environment=MC_PORT=9001
|
|||||||
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||||
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||||
Environment=MC_MODELS_DIR=/srv/models
|
Environment=MC_MODELS_DIR=/srv/models
|
||||||
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
|
|
||||||
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
|
||||||
# Gateway-Auszug (UMBAU v3 P1): /v1 roh an den eigenständigen mc2-gateway-Prozess
|
# Gateway-Auszug (UMBAU v3 P1): /v1 roh an den eigenständigen mc2-gateway-Prozess
|
||||||
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
||||||
# bedient /v1 wieder selbst.
|
# bedient /v1 wieder selbst.
|
||||||
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
|
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
|
||||||
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry, Mem0-Dedupe)
|
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry) laufen jetzt im
|
||||||
# laufen jetzt im eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese drei
|
# eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese zwei Zeilen entfernen
|
||||||
# Zeilen entfernen (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
# (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
||||||
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
|
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
|
||||||
Environment=MC_REWARM_ENABLED=0
|
Environment=MC_REWARM_ENABLED=0
|
||||||
Environment=MC_SENTRY_ENABLED=0
|
Environment=MC_SENTRY_ENABLED=0
|
||||||
Environment=MC_MEM_DEDUPE_ENABLED=0
|
|
||||||
# KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den
|
# KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den
|
||||||
# ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates
|
# ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates
|
||||||
# liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default
|
# liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default
|
||||||
|
|||||||
+6
-4
@@ -15,11 +15,13 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
||||||
DEST_DIR="$MODELS_DIR/mc2-backups"
|
DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||||
SERVICES="mem0-service mission-control-2 hermes-gateway"
|
# Alle User-Dienste, die von wiederhergestellter Config abhaengen. mc2-gateway (der
|
||||||
|
# /v1-Denkpfad) und mc2-steward (Waechter) fehlten hier bis 27.08.2026 — nach einem
|
||||||
|
# Restore liefen beide mit der ALTEN llama-swap-/Hermes-Config weiter.
|
||||||
|
SERVICES="mission-control-2 mc2-gateway mc2-steward hermes-gateway"
|
||||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||||
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||||
@@ -98,7 +100,6 @@ echo "--- Inhalt ---"; cat "$STAGE/MANIFEST.txt" 2>/dev/null || true; echo "----
|
|||||||
|
|
||||||
if [ "$DRY" = 1 ]; then
|
if [ "$DRY" = 1 ]; then
|
||||||
echo "[dry-run] würde zurücksetzen:"
|
echo "[dry-run] würde zurücksetzen:"
|
||||||
[ -d "$STAGE/mem0" ] && echo " • mem0 → $MEM0_DIR (wird ersetzt)"
|
|
||||||
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
||||||
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
||||||
@@ -119,7 +120,8 @@ bash "$SRC/deploy/backup.sh" || echo " (Pre-Restore-Backup fehlgeschlagen — f
|
|||||||
echo "→ Dienste stoppen…"
|
echo "→ Dienste stoppen…"
|
||||||
systemctl --user stop $SERVICES 2>/dev/null || true
|
systemctl --user stop $SERVICES 2>/dev/null || true
|
||||||
|
|
||||||
if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir -p "$MEM0_DIR"; cp -a "$STAGE/mem0/." "$MEM0_DIR/"; fi
|
# Alte Snapshots (vor 27.08.2026) tragen noch ein Gedaechtnis-Verzeichnis mit — es wird
|
||||||
|
# bewusst NICHT mehr zurueckgespielt, der zugehoerige Dienst existiert nicht mehr.
|
||||||
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
||||||
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
|
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
|
||||||
# Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed).
|
# Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed).
|
||||||
#
|
#
|
||||||
# Pendant zu hermes-postcheck.sh (prüft das Gehirn/Mem0); dieser prüft Router+Engine+MC2
|
# Pendant zu hermes-postcheck.sh (prüft das Gehirn); dieser prüft Router+Engine+MC2
|
||||||
# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert).
|
# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||||
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
|
|
||||||
BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste
|
BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste
|
||||||
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Mem0/Gedächtnis)
|
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Reranker-Stufe)
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
||||||
@@ -48,14 +47,14 @@ else
|
|||||||
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3b. Embedding-Modell (für Mem0/Gedächtnis) lädt und liefert einen Vektor?
|
# 3b. Embedding-Modell lädt und liefert einen Vektor?
|
||||||
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
||||||
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
||||||
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
||||||
else
|
else
|
||||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Mem0/Gedächtnis betroffen)"; fail=1
|
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
||||||
@@ -85,12 +84,8 @@ if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Mem0-Sidecar (Gedächtnis) erreichbar?
|
# (Schritt 5 „Gedächtnis-Sidecar erreichbar" entfiel am 27.08.2026: der Dienst auf :8765 ist
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
# seit der Ablösung im August tot, der Check konnte nur noch rot werden. docs/wissen/VERDIKTE.md)
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
|
||||||
else
|
|
||||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$fail" -eq 0 ]; then
|
if [ "$fail" -eq 0 ]; then
|
||||||
echo "=== STACK OK ✓ ==="
|
echo "=== STACK OK ✓ ==="
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Mem0, Voice) altern
|
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Voice) altern
|
||||||
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
||||||
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
||||||
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")
|
VENVS=("$HOME/.voice/venv:voice")
|
||||||
AUDIT_VENV="$HOME/.venv-audit"
|
AUDIT_VENV="$HOME/.venv-audit"
|
||||||
HERMES="$HOME/.local/bin/hermes"
|
HERMES="$HOME/.local/bin/hermes"
|
||||||
MONAT="$(date '+%Y-%m')"
|
MONAT="$(date '+%Y-%m')"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# systemd-USER-Unit für den MC2 Voice-Sidecar — lokales STT + gestuftes TTS.
|
||||||
|
#
|
||||||
|
# ‼️ NACHGETRAGEN 27.08.2026: Diese Unit lief auf der Box, fehlte aber im Repo — weder
|
||||||
|
# versioniert noch vom Disaster-Recovery abgedeckt, obwohl services/maintenance.py
|
||||||
|
# (USER_SERVICES) und routers/system.py sie voraussetzen. Inhalt 1:1 von der laufenden Box
|
||||||
|
# übernommen (`systemctl --user cat voice-service`).
|
||||||
|
#
|
||||||
|
# Ablage: ~/.config/systemd/user/voice-service.service ; dann:
|
||||||
|
# systemctl --user daemon-reload && systemctl --user enable --now voice-service
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=MC2 Voice Sidecar — lokales STT (faster-whisper) + gestuftes TTS (Piper/Chatterbox)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Eigenes Python-3.12-venv (~/.voice/venv) — torch/chatterbox/faster-whisper passen nicht ins
|
||||||
|
# 3.14-Backend-venv. Bind 127.0.0.1: nur lokal; MC2 proxyt nach außen (routers/voice.py).
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/mission-control-v2/voice_service
|
||||||
|
Environment=VOICE_PORT=8650
|
||||||
|
Environment=VOICE_STT_MODEL=medium
|
||||||
|
Environment=VOICE_STT_LANG=de
|
||||||
|
Environment=VOICE_PIPER_DIR=%h/.voice/voices
|
||||||
|
Environment=VOICE_PIPER_DEFAULT=de_DE-thorsten-medium
|
||||||
|
Environment=VOICE_CHATTERBOX_DEVICE=cpu
|
||||||
|
Environment=VOICE_CHATTERBOX_LANG=de
|
||||||
|
Environment=TOKENIZERS_PARALLELISM=false
|
||||||
|
ExecStart=%h/.voice/venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8650
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
||||||
#
|
#
|
||||||
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
||||||
# (Qwen3-Embedding-0.6B, für Mem0/Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
# (Qwen3-Embedding-0.6B). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||||
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
||||||
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
||||||
# anstoßen, sonst bleibt es kalt.
|
# anstoßen, sonst bleibt es kalt.
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ Config: `/etc/llama-swap/config.yaml` (sudo zum Schreiben).
|
|||||||
- **v0.20.4** (git-Install `~/.hermes/hermes-agent`), Config `~/.hermes/config.yaml`, Persona `~/.hermes/SOUL.md` = **Lucy**.
|
- **v0.20.4** (git-Install `~/.hermes/hermes-agent`), Config `~/.hermes/config.yaml`, Persona `~/.hermes/SOUL.md` = **Lucy**.
|
||||||
- **Bot Mode:** Unterstützt Multi-Bot-Roster (`Lucy` als Haupt-Agentin, `Coder` mit `Qwen3-Coder-Next`, `Debugger` mit `Muse-Glimmer-30B`).
|
- **Bot Mode:** Unterstützt Multi-Bot-Roster (`Lucy` als Haupt-Agentin, `Coder` mit `Qwen3-Coder-Next`, `Debugger` mit `Muse-Glimmer-30B`).
|
||||||
- `approvals.mode: smart`, `approvals.cron_mode: deny`.
|
- `approvals.mode: smart`, `approvals.cron_mode: deny`.
|
||||||
- MCP-Server (aus `~/mission-control-v2/mcp/`): mission-control-memory, mission-control-stack, hermes-pc-control, hermes-web-fetch, mcp_voice.
|
- MCP-Server (aus `~/mission-control-v2/mcp/`): mission-control-stack, hermes-pc-control, hermes-web-fetch, mcp_voice.
|
||||||
|
(`mission-control-memory` ist am 27.08.2026 entfallen — Hermes führt sein Gedächtnis selbst.)
|
||||||
|
|
||||||
## Automatik-Fahrplan (was nachts von allein läuft)
|
## Automatik-Fahrplan (was nachts von allein läuft)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as A,I as P,f as M,k as B,u as D,r as z,j as e,J as T,b as a,K as g,M as b,N as W,t as f,W as j,O as G,X as I,C as N,n as y,q as i}from"./index-CCbyojll.js";/**
|
import{c as A,I as P,f as M,k as B,u as D,r as z,j as e,J as T,b as a,K as g,M as b,N as W,t as f,W as j,O as G,X as I,C as N,n as y,q as i}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as W,a1 as H,a2 as F,u as G,k as T,r as b,j as e,R as _,b as y,L as m,C as E,_ as O,D as P,a0 as z,a3 as R,a4 as V,X as D,T as C,F as K,n as $,a5 as q,q as U}from"./index-CCbyojll.js";import{S,H as Q}from"./SectionLabel-BfFwUeZU.js";import{C as M}from"./chevron-down-SPeGIim5.js";/**
|
import{c as W,a1 as H,a2 as F,u as G,k as T,r as b,j as e,R as _,b as y,L as m,C as E,_ as O,D as P,a0 as z,a3 as R,a4 as V,X as D,T as C,F as K,n as $,a5 as q,q as U}from"./index-DNt66je4.js";import{S,H as Q}from"./SectionLabel-DKDWJ8hi.js";import{C as M}from"./chevron-down-BTSzJRag.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as d,aj as S,r as h,j as e,ak as M,L as p,a0 as z,a5 as C,al as D,am as A,b as B,an as Z,u as E,k as L,n as k,q}from"./index-CCbyojll.js";import{R as j}from"./rotate-ccw-DuKtbjZS.js";/**
|
import{c as d,aj as S,r as h,j as e,ak as M,L as p,a0 as z,a5 as C,al as D,am as A,b as B,an as Z,u as E,k as L,n as k,q}from"./index-DNt66je4.js";import{R as j}from"./rotate-ccw-CGv9dXzB.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
-26
File diff suppressed because one or more lines are too long
+26
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as $,j as e,b as S,a2 as ve,u as Se,k as De,r as d,n as z,a6 as Ke,_ as O,L as D,X as Q,a7 as xe,F as T,a3 as he,C as Ce,T as ze,a4 as Ee,a8 as pe,q as Ae,R as fe}from"./index-CCbyojll.js";import{S as Le,H as X}from"./SectionLabel-BfFwUeZU.js";import{C as ee}from"./code-xml-AZtn6xxf.js";import{L as Pe}from"./layers-Dpl2QlP2.js";import{C as U}from"./chevron-down-SPeGIim5.js";import{R as We}from"./rotate-ccw-DuKtbjZS.js";/**
|
import{c as $,j as e,b as S,a2 as ve,u as Se,k as De,r as d,n as z,a6 as Ke,_ as O,L as D,X as Q,a7 as xe,F as T,a3 as he,C as Ce,T as ze,a4 as Ee,a8 as pe,q as Ae,R as fe}from"./index-DNt66je4.js";import{S as Le,H as X}from"./SectionLabel-DKDWJ8hi.js";import{C as ee}from"./code-xml-BiSOeCkP.js";import{L as Pe}from"./layers-BakyhGhx.js";import{C as U}from"./chevron-down-BTSzJRag.js";import{R as We}from"./rotate-ccw-CGv9dXzB.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-CCbyojll.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-DNt66je4.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-CCbyojll.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-DNt66je4.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as E,u as he,a as qe,r as j,j as e,C as Y,L as Fe,b as k,d as Re,q as V,B as re,e as Ie,E as ze,f as le,g as Be,h as Ae,i as Pe,k as Ge,l as ee,X as me,H as be,T as se,s as Oe,m as Te,n as q,o as U,p as He,S as ge,t as Qe,P as Ue,v as Ve,w as We,x as Ze,y as Je}from"./index-CCbyojll.js";import{J as Xe}from"./JobsBar-Ckecoyqy.js";import{C as Ye}from"./code-xml-AZtn6xxf.js";import{R as et}from"./rotate-ccw-DuKtbjZS.js";import{C as ue,Z as ie,L as tt,a as rt}from"./zap-CRNNnxfs.js";import{C as De}from"./chevron-down-SPeGIim5.js";import{L as st}from"./layers-Dpl2QlP2.js";/**
|
import{c as E,u as he,a as qe,r as j,j as e,C as Y,L as Fe,b as k,d as Re,q as V,B as re,e as Ie,E as ze,f as le,g as Be,h as Ae,i as Pe,k as Ge,l as ee,X as me,H as be,T as se,s as Oe,m as Te,n as q,o as U,p as He,S as ge,t as Qe,P as Ue,v as Ve,w as We,x as Ze,y as Je}from"./index-DNt66je4.js";import{J as Xe}from"./JobsBar-DG8VDiAV.js";import{C as Ye}from"./code-xml-BiSOeCkP.js";import{R as et}from"./rotate-ccw-CGv9dXzB.js";import{C as ue,Z as ie,L as tt,a as rt}from"./zap-cdYTHDpR.js";import{C as De}from"./chevron-down-BTSzJRag.js";import{L as st}from"./layers-BakyhGhx.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as s,j as e}from"./index-CCbyojll.js";/**
|
import{c as s,j as e}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-CCbyojll.js";import{J as _}from"./JobsBar-Ckecoyqy.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-DNt66je4.js";import{J as _}from"./JobsBar-DG8VDiAV.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+17
-12
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as o}from"./index-CCbyojll.js";/**
|
import{c as o}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as e}from"./index-CCbyojll.js";/**
|
import{c as e}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
|
||||||
* @license lucide-react v0.460.0 - ISC
|
|
||||||
*
|
|
||||||
* This source code is licensed under the ISC license.
|
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
|
||||||
*/const o=a("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);export{o as F};
|
|
||||||
+39
-39
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
import{c as a}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as t}from"./index-CCbyojll.js";/**
|
import{c as t}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
import{c as a}from"./index-DNt66je4.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CCbyojll.js"></script>
|
<script type="module" crossorigin src="/assets/index-DNt66je4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dv1-nhwH.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-qQDuX0H0.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>␍
|
<div id="root"></div>␍
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Gauge } from "lucide-react"
|
|||||||
import { useVoiceTrace } from "@/lib/queries"
|
import { useVoiceTrace } from "@/lib/queries"
|
||||||
import type { VoiceTurn } from "@/lib/api"
|
import type { VoiceTurn } from "@/lib/api"
|
||||||
|
|
||||||
// Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Mem0 ist ein Unter-Detail
|
// Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Der Gedächtnis-Abruf läuft
|
||||||
// INNERHALB von „Hirn" und wird NICHT als eigenes Segment addiert (sonst Doppelzählung).
|
// Hermes-intern und ist für MC2 nicht messbar — er steckt in „Hirn".
|
||||||
const SEGMENTS = [
|
const SEGMENTS = [
|
||||||
{ key: "stt_ms", label: "STT", color: "#f59e0b" },
|
{ key: "stt_ms", label: "STT", color: "#f59e0b" },
|
||||||
{ key: "vision_ms", label: "Sehen", color: "#a78bfa" },
|
{ key: "vision_ms", label: "Sehen", color: "#a78bfa" },
|
||||||
@@ -39,7 +39,6 @@ function culprit(t: VoiceTurn): { label: string; color: string; ms: number } | n
|
|||||||
|
|
||||||
function TurnRow({ t, scale }: { t: VoiceTurn; scale: number }) {
|
function TurnRow({ t, scale }: { t: VoiceTurn; scale: number }) {
|
||||||
const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ")
|
const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ")
|
||||||
+ (t.mem0_ms != null ? ` (davon Gedächtnis ${fmt(t.mem0_ms)})` : "")
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(t.ts)}</span>
|
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(t.ts)}</span>
|
||||||
@@ -103,7 +102,6 @@ export function LatencyCard() {
|
|||||||
{latest && !latest.error && (
|
{latest && !latest.error && (
|
||||||
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
|
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
|
||||||
{SEGMENTS.filter((s) => (latest[s.key] as number | null) != null).map((s) => `${s.label} ${fmt(latest[s.key] as number)}`).join(" · ")}
|
{SEGMENTS.filter((s) => (latest[s.key] as number | null) != null).map((s) => `${s.label} ${fmt(latest[s.key] as number)}`).join(" · ")}
|
||||||
{latest.mem0_ms != null && <span className="text-muted-foreground/50"> · davon Gedächtnis {fmt(latest.mem0_ms)}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +126,7 @@ export function LatencyCard() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
|
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
|
||||||
„Hirn" = Zeit bis zum ersten Wort (Agent + Mem0-Suche + Modell); Tool-Runden stecken in „Antwort". Reine
|
„Hirn" = Zeit bis zum ersten Wort (Agent + Gedächtnis-Suche + Modell); Tool-Runden stecken in „Antwort". Reine
|
||||||
Telegram-Turns laufen an MC2 vorbei und erscheinen hier nicht.
|
Telegram-Turns laufen an MC2 vorbei und erscheinen hier nicht.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+6
-52
@@ -338,7 +338,6 @@ export interface ConnectResp {
|
|||||||
gateway_url: string
|
gateway_url: string
|
||||||
mc_url: string
|
mc_url: string
|
||||||
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
|
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
|
||||||
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectLine {
|
export interface ConnectLine {
|
||||||
@@ -347,20 +346,12 @@ export interface ConnectLine {
|
|||||||
}
|
}
|
||||||
export interface ConnectHealth {
|
export interface ConnectHealth {
|
||||||
gateway: ConnectLine
|
gateway: ConnectLine
|
||||||
memory: ConnectLine
|
memory: ConnectLine // Leitung 2 — Hermes-natives Gedächtnis (nur Status, nichts zu konfigurieren)
|
||||||
desktop_gateway: ConnectLine
|
desktop_gateway: ConnectLine
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Memory {
|
// Graph-Ansicht des Wissens-Vaults (GET /api/wissen/graph) — trägt aus historischen Gründen
|
||||||
id: string
|
// noch „Memory" im Namen; die Daten kommen aus dem Vault, nicht aus einem Gedächtnis-Dienst.
|
||||||
content: string
|
|
||||||
category: string
|
|
||||||
source: string
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
score?: number // Relevanz bei semantischer Suche (q gesetzt); sonst undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemoryGraphNode {
|
export interface MemoryGraphNode {
|
||||||
id: string
|
id: string
|
||||||
content: string
|
content: string
|
||||||
@@ -372,13 +363,6 @@ export interface MemoryGraph {
|
|||||||
edges: { source: string; target: string; weight: number }[]
|
edges: { source: string; target: string; weight: number }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DedupeResult {
|
|
||||||
groups: { keep: { id: string; content: string; category: string }; remove: { id: string; content: string }[] }[]
|
|
||||||
duplicate_count: number
|
|
||||||
removed: number
|
|
||||||
applied: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AgentStatus {
|
export interface AgentStatus {
|
||||||
gateway_url: string
|
gateway_url: string
|
||||||
box_console_url?: string
|
box_console_url?: string
|
||||||
@@ -443,7 +427,8 @@ export interface TokenStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
|
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
|
||||||
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
|
// stt · vision · hirn · gen. Der Gedächtnis-Abruf läuft seit der Mem0-Ablösung Hermes-intern
|
||||||
|
// (kein Rückruf an MC2) und steckt daher unsichtbar in hirn_ms.
|
||||||
export interface VoiceTurn {
|
export interface VoiceTurn {
|
||||||
id: string
|
id: string
|
||||||
ts: number // Epoch-Sekunden
|
ts: number // Epoch-Sekunden
|
||||||
@@ -452,9 +437,8 @@ export interface VoiceTurn {
|
|||||||
had_images: boolean
|
had_images: boolean
|
||||||
stt_ms: number | null
|
stt_ms: number | null
|
||||||
vision_ms: number | null
|
vision_ms: number | null
|
||||||
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
|
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Gedächtnis + LLM-TTFT)
|
||||||
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
|
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
|
||||||
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
|
|
||||||
total_ms: number
|
total_ms: number
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
@@ -583,36 +567,6 @@ export interface ChronikResp {
|
|||||||
items: ChronikItem[]
|
items: ChronikItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Eigenleben („Von allein": Skills + Vorschlags-Bilanz der Box) ────────────
|
|
||||||
export interface EigenlebenSkill {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
kategorie: string | null
|
|
||||||
aktiv: boolean | null // null = nicht ermittelbar (Dev-Modus)
|
|
||||||
beschreibung: string
|
|
||||||
version: string
|
|
||||||
autor: string
|
|
||||||
herkunft: "selbst" | "repo" | "bundled"
|
|
||||||
herkunft_label: string
|
|
||||||
genutzt: number
|
|
||||||
zuletzt_genutzt: string | null
|
|
||||||
erstellt: string | null
|
|
||||||
status: string
|
|
||||||
geaendert: number
|
|
||||||
}
|
|
||||||
export interface EigenlebenEintrag {
|
|
||||||
art: string // "angenommen" | "abgelehnt" | sonstiger Chronik-State
|
|
||||||
titel: string
|
|
||||||
detail: string
|
|
||||||
ts: number
|
|
||||||
}
|
|
||||||
export interface EigenlebenResp {
|
|
||||||
available: boolean
|
|
||||||
skills: EigenlebenSkill[]
|
|
||||||
selbst_erstellt: number
|
|
||||||
bilanz: { eintraege: EigenlebenEintrag[]; angenommen: number; abgelehnt: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Wissens-Vault (Traum-Notizen, read-only) ─────────────────────────────────
|
// ── Wissens-Vault (Traum-Notizen, read-only) ─────────────────────────────────
|
||||||
export interface WissenFile {
|
export interface WissenFile {
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
type ConnectHealth,
|
type ConnectHealth,
|
||||||
type DiscoverResp,
|
type DiscoverResp,
|
||||||
type DraftsResp,
|
type DraftsResp,
|
||||||
type EigenlebenResp,
|
|
||||||
type GroupsResp,
|
type GroupsResp,
|
||||||
type HermesBrainResp,
|
type HermesBrainResp,
|
||||||
type Health,
|
type Health,
|
||||||
@@ -21,7 +20,6 @@ import {
|
|||||||
type IdeenLogResp,
|
type IdeenLogResp,
|
||||||
type IdeenResp,
|
type IdeenResp,
|
||||||
type Job,
|
type Job,
|
||||||
type Memory,
|
|
||||||
type MemoryGraph,
|
type MemoryGraph,
|
||||||
type ModelsResp,
|
type ModelsResp,
|
||||||
type Reminder,
|
type Reminder,
|
||||||
@@ -54,13 +52,11 @@ export const qk = {
|
|||||||
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
||||||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||||||
connectHealth: ["connect-health"] as const,
|
connectHealth: ["connect-health"] as const,
|
||||||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
|
||||||
memoryGraph: ["memory-graph"] as const,
|
memoryGraph: ["memory-graph"] as const,
|
||||||
voiceTrace: ["voice-trace"] as const,
|
voiceTrace: ["voice-trace"] as const,
|
||||||
auftragsbuch: ["auftragsbuch"] as const,
|
auftragsbuch: ["auftragsbuch"] as const,
|
||||||
ideen: ["ideen"] as const,
|
ideen: ["ideen"] as const,
|
||||||
chronik: ["chronik"] as const,
|
chronik: ["chronik"] as const,
|
||||||
eigenleben: ["eigenleben"] as const,
|
|
||||||
wissen: ["wissen"] as const,
|
wissen: ["wissen"] as const,
|
||||||
zeitmaschine: ["zeitmaschine"] as const,
|
zeitmaschine: ["zeitmaschine"] as const,
|
||||||
reminders: ["reminders"] as const,
|
reminders: ["reminders"] as const,
|
||||||
@@ -125,13 +121,6 @@ export const useChronik = (limit = 150, refetchInterval: number = TAKT.gemuetlic
|
|||||||
export const useWissen = () =>
|
export const useWissen = () =>
|
||||||
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
||||||
|
|
||||||
export const useEigenleben = (refetchInterval: number = TAKT.traege) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: qk.eigenleben,
|
|
||||||
queryFn: () => api<EigenlebenResp>("/api/eigenleben"),
|
|
||||||
refetchInterval,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useZeitmaschine = (refetchInterval: number = TAKT.traege, enabled = true) =>
|
export const useZeitmaschine = (refetchInterval: number = TAKT.traege, enabled = true) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.zeitmaschine,
|
queryKey: qk.zeitmaschine,
|
||||||
@@ -248,18 +237,6 @@ export const useConnectHealth = () =>
|
|||||||
refetchInterval: 15000,
|
refetchInterval: 15000,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: qk.memory(opts?.q, opts?.category),
|
|
||||||
queryFn: () => {
|
|
||||||
const p = new URLSearchParams()
|
|
||||||
if (opts?.q) p.set("q", opts.q)
|
|
||||||
if (opts?.category) p.set("category", opts.category)
|
|
||||||
return api<Memory[]>(`/api/memory?${p}`)
|
|
||||||
},
|
|
||||||
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
||||||
export function invalidate(qc: QueryClient, ...keys: (readonly unknown[])[]) {
|
export function invalidate(qc: QueryClient, ...keys: (readonly unknown[])[]) {
|
||||||
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
Check, Copy, Cpu, Brain, Globe, FolderOpen, Laptop, ArrowRight,
|
Check, Copy, Cpu, Brain, Globe, Laptop, ArrowRight,
|
||||||
Info, CircleCheck, CircleX, Loader2, ChevronDown, ChevronRight,
|
Info, CircleCheck, CircleX, Loader2, ChevronDown, ChevronRight,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useConnect, useConnectHealth } from "@/lib/queries"
|
import { useConnect, useConnectHealth } from "@/lib/queries"
|
||||||
@@ -82,14 +82,12 @@ function defaultHost(): string {
|
|||||||
|
|
||||||
export function ConnectView() {
|
export function ConnectView() {
|
||||||
const [host, setHost] = useState(localStorage.getItem("mc_host") || defaultHost())
|
const [host, setHost] = useState(localStorage.getItem("mc_host") || defaultHost())
|
||||||
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
|
|
||||||
// Legacy-Tools (roher /v1-Zugang) leben im Ausklapper — Hermes Desktop ist der Hauptweg.
|
// Legacy-Tools (roher /v1-Zugang) leben im Ausklapper — Hermes Desktop ist der Hauptweg.
|
||||||
const [active, setActive] = useState("kilo")
|
const [active, setActive] = useState("kilo")
|
||||||
const [sonstigeOffen, setSonstigeOffen] = useState(false)
|
const [sonstigeOffen, setSonstigeOffen] = useState(false)
|
||||||
const [copied, setCopied] = useState<"desktop" | "model" | "memory" | null>(null)
|
const [copied, setCopied] = useState<"desktop" | "model" | null>(null)
|
||||||
|
|
||||||
const params = new URLSearchParams({ host })
|
const params = new URLSearchParams({ host })
|
||||||
if (mcpPath) params.set("mcp_path", mcpPath)
|
|
||||||
const { data, error: dataErr } = useConnect(params.toString())
|
const { data, error: dataErr } = useConnect(params.toString())
|
||||||
const { data: health, isLoading: healthLoading } = useConnectHealth()
|
const { data: health, isLoading: healthLoading } = useConnectHealth()
|
||||||
const error = dataErr ? String(dataErr) : ""
|
const error = dataErr ? String(dataErr) : ""
|
||||||
@@ -99,15 +97,10 @@ export function ConnectView() {
|
|||||||
if (v) localStorage.setItem("mc_host", v)
|
if (v) localStorage.setItem("mc_host", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMcpPath(v: string) {
|
|
||||||
setMcpPath(v)
|
|
||||||
localStorage.setItem("mc_mcp_path", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
const desktop = data?.tools["hermes_desktop"]
|
const desktop = data?.tools["hermes_desktop"]
|
||||||
const tool = data?.tools[active]
|
const tool = data?.tools[active]
|
||||||
|
|
||||||
async function copy(which: "desktop" | "model" | "memory", snippet?: string) {
|
async function copy(which: "desktop" | "model", snippet?: string) {
|
||||||
if (!snippet) return
|
if (!snippet) return
|
||||||
await navigator.clipboard.writeText(snippet)
|
await navigator.clipboard.writeText(snippet)
|
||||||
setCopied(which)
|
setCopied(which)
|
||||||
@@ -123,7 +116,7 @@ export function ConnectView() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<span className="text-foreground">Hermes Desktop</span> ist die Tür zur Box — einmal verbinden, fertig.
|
<span className="text-foreground">Hermes Desktop</span> ist die Tür zur Box — einmal verbinden, fertig.
|
||||||
Für Spezialfälle gibt es darunter die rohen Leitungen (Gateway /v1 + Gedächtnis-MCP) für sonstige Tools.
|
Für Spezialfälle gibt es darunter den rohen Gateway-Zugang ( /v1) für sonstige Tools.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -165,7 +158,7 @@ export function ConnectView() {
|
|||||||
<StatusBadge line={health?.memory} loading={healthLoading} />
|
<StatusBadge line={health?.memory} loading={healthLoading} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[11px] font-mono text-violet-300/80">
|
<div className="mt-1 text-[11px] font-mono text-violet-300/80">
|
||||||
MCP · mcp_memory.py · separat
|
Hermes-nativ · im Agenten · nichts einzurichten
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,9 +180,9 @@ export function ConnectView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3.5 text-[11px] text-muted-foreground leading-relaxed">
|
<p className="mt-3.5 text-[11px] text-muted-foreground leading-relaxed">
|
||||||
Der Gateway liefert <span className="text-foreground">nur das LLM</span>. Das geteilte Gedächtnis läuft über einen
|
Der Gateway liefert <span className="text-foreground">nur das LLM</span>. Das Gedächtnis führt
|
||||||
<span className="text-foreground"> eigenen MCP-Server</span> und das Desktop-Gateway bietet die Agenten-Schnittstelle —
|
<span className="text-foreground"> Hermes selbst</span> (nichts einzurichten — die Kachel zeigt nur, ob es lebt), und das
|
||||||
alle drei Leitungen werden unabhängig eingerichtet. IDEs wählen die <span className="text-foreground">echten Modelle</span>
|
Desktop-Gateway bietet die Agenten-Schnittstelle. IDEs wählen die <span className="text-foreground">echten Modelle</span>
|
||||||
{" "}(coder · heavy · vision); daneben gibt es die virtuellen Lanes <span className="text-foreground">chat</span> und
|
{" "}(coder · heavy · vision); daneben gibt es die virtuellen Lanes <span className="text-foreground">chat</span> und
|
||||||
{" "}<span className="text-foreground">coding</span>, die je Anfrage selbst das passende Modell wählen (Alltags-Chat, simple Tools).
|
{" "}<span className="text-foreground">coding</span>, die je Anfrage selbst das passende Modell wählen (Alltags-Chat, simple Tools).
|
||||||
</p>
|
</p>
|
||||||
@@ -260,12 +253,12 @@ export function ConnectView() {
|
|||||||
className="flex w-full items-center gap-2 p-4 text-sm font-space font-bold text-foreground cursor-pointer hover:text-primary transition-colors"
|
className="flex w-full items-center gap-2 p-4 text-sm font-space font-bold text-foreground cursor-pointer hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
{sonstigeOffen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
{sonstigeOffen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||||
Sonstige Tools (roher /v1-Zugang + Gedächtnis-MCP)
|
Sonstige Tools (roher /v1-Zugang)
|
||||||
<span className="text-[10px] font-medium text-muted-foreground normal-case">Kilo Code · Claude Code · Eigenbau</span>
|
<span className="text-[10px] font-medium text-muted-foreground normal-case">Kilo Code · Claude Code · Eigenbau</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{sonstigeOffen && (
|
{sonstigeOffen && (
|
||||||
<div className="grid gap-5 lg:grid-cols-2 p-4 pt-0">
|
<div className="grid gap-5 p-4 pt-0">
|
||||||
{/* Leitung 1 — Modell anbinden */}
|
{/* Leitung 1 — Modell anbinden */}
|
||||||
<div className="space-y-3 rounded-2xl border border-border/60 border-t-2 border-t-primary/70 bg-card/30 p-4">
|
<div className="space-y-3 rounded-2xl border border-border/60 border-t-2 border-t-primary/70 bg-card/30 p-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -312,51 +305,6 @@ export function ConnectView() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Leitung 2 — Gedächtnis anbinden */}
|
|
||||||
<div className="space-y-3 rounded-2xl border border-border/60 border-t-2 border-t-violet-500/70 bg-card/30 p-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-space font-bold text-foreground">
|
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-violet-500/15 text-violet-300 text-[11px]">2</span>
|
|
||||||
Gedächtnis anbinden
|
|
||||||
<span className="text-[10px] font-medium text-muted-foreground normal-case">optional</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] text-muted-foreground mt-0.5">Ein MCP-Block — gilt zusätzlich für <em>jedes</em> Tool aus Schritt 1.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-2.5 p-3 rounded-xl bg-violet-500/5 border border-violet-500/20 text-[11px] text-muted-foreground leading-relaxed">
|
|
||||||
<Info className="h-4 w-4 text-violet-400 shrink-0 mt-0.5" />
|
|
||||||
<span>{data.memory.note}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Der Pfad wirkt NUR auf dieses Snippet — deshalb wohnt das Feld hier,
|
|
||||||
nicht mehr prominent in der Kopfzeile („Wann brauche ich das?"). */}
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
|
||||||
<FolderOpen className="h-3.5 w-3.5 text-violet-400" /> Wo liegt mcp_memory.py auf DIESEM PC?
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={mcpPath}
|
|
||||||
onChange={(e) => saveMcpPath(e.target.value)}
|
|
||||||
aria-label="Lokaler MCP-Scriptpfad"
|
|
||||||
spellCheck={false}
|
|
||||||
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
|
|
||||||
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
|
|
||||||
/>
|
|
||||||
<p className="text-[10px] text-muted-foreground/70">
|
|
||||||
Nur nötig, wenn du das Gedächtnis in einem PC-Tool anschließt — das Script läuft lokal
|
|
||||||
und spricht mit der Box. Der Pfad landet direkt im Snippet unten.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CodeWindow
|
|
||||||
tool={data.memory}
|
|
||||||
fileName="mcp.json"
|
|
||||||
accent="violet"
|
|
||||||
copied={copied === "memory"}
|
|
||||||
onCopy={() => copy("memory", data.memory.snippet)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -345,10 +345,12 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Stack>
|
<Stack>
|
||||||
Dein Gedächtnis (Tab <strong>Gedächtnis</strong>) ist <strong>Mem0</strong>-basiert: auto-lernend &
|
Dein Gedächtnis führt <strong>Hermes</strong> selbst — auto-lernend und semantisch. Der Agent lernt im
|
||||||
semantisch, <strong>graph-first</strong> dargestellt, mit 4-Typen-Taxonomie
|
Hintergrund nach jedem Turn, egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
|
||||||
(<Pill>Identität</Pill> · <Pill>Wissen</Pill> · <Pill>Regeln</Pill> · <Pill>Ereignisse</Pill>). Der Agent
|
<br />Deshalb hat das Steuerpult <strong>keinen eigenen Gedächtnis-Tab</strong>: bis August 2026 lag das
|
||||||
lernt im Hintergrund nach jedem Turn — egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
|
Gedächtnis in einem MC2-Sidecar, seit dessen Ablösung ist Hermes die einzige Quelle — angesehen und
|
||||||
|
gepflegt wird es dort, nicht hier. Unter <strong>Wissen</strong> findest du weiterhin die Graph-Sicht
|
||||||
|
auf Lucys Wissens-Vault.
|
||||||
<br /><strong>Gotcha:</strong> ein gelöschter Fakt kann wieder auftauchen, solange die Original-Nachricht noch im Verlauf steht (additive Extraktion).
|
<br /><strong>Gotcha:</strong> ein gelöschter Fakt kann wieder auftauchen, solange die Original-Nachricht noch im Verlauf steht (additive Extraktion).
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
"""
|
|
||||||
MC2 Memory — Hermes-Memory-Provider, der ans geteilte **Mem0-Gedächtnis** hängt (über MC2 :9001,
|
|
||||||
das den Mem0-Sidecar :8765 proxyt). Macht das Gedächtnis für Hermes **hands-off**:
|
|
||||||
|
|
||||||
- `sync_turn` → nach jedem Turn werden die Turn-Messages an `/api/memory/learn` geschickt;
|
|
||||||
Mem0 EXTRAHIERT dauerhafte Fakten selbst (infer=True), dedupliziert semantisch. Nicht-blockierend
|
|
||||||
(Hintergrund-Worker), damit der Agent nie auf die LLM-Extraktion wartet.
|
|
||||||
- `prefetch` → vor jedem Turn semantische Suche gegen die Nutzer-Message; relevante Fakten werden
|
|
||||||
als Kontext eingeblendet (automatischer Recall).
|
|
||||||
|
|
||||||
**Context-only:** `get_tool_schemas()` liefert `[]` → der Agent bekommt KEINE Memory-Tools. Damit
|
|
||||||
entfällt das Tool-Loop-Risiko, wegen dem das native Memory-Toolset abgeschaltet ist
|
|
||||||
(`agent.disabled_toolsets: [..., memory]` bleibt). Single Source of Truth bleibt der MC2-Sidecar
|
|
||||||
(NoThink-Fix + alleiniger Chroma-Besitzer) — dieser Provider ist nur ein dünner HTTP-Client.
|
|
||||||
|
|
||||||
Aktivierung (in ~/.hermes/config.yaml):
|
|
||||||
memory:
|
|
||||||
memory_enabled: true
|
|
||||||
provider: mc2-memory
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import queue
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from agent.memory_provider import MemoryProvider
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
MC_URL = os.environ.get("MC_URL", "http://127.0.0.1:9001").rstrip("/")
|
|
||||||
MC_TOKEN = os.environ.get("MC_TOKEN", "")
|
|
||||||
RECALL_LIMIT = int(os.environ.get("MC2_MEMORY_RECALL_LIMIT", "5"))
|
|
||||||
# 0.5 statt 0.3: nur starke Treffer einblenden → weniger Rausch-Fakten UND ein stabilerer
|
|
||||||
# Prompt-Prefix (oft leer), damit --cache-reuse über Turns greift.
|
|
||||||
RECALL_MIN_SCORE = float(os.environ.get("MC2_MEMORY_RECALL_MIN_SCORE", "0.5"))
|
|
||||||
# 25 statt 12: triviale Kurz-Turns (Begrüßung/Quittung) gar nicht erst zum Lernen schicken.
|
|
||||||
MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "25")) # zu Kurzes nicht lernen
|
|
||||||
# Anti-Memory-Poisoning (Stufe 0 Security): aus Turns mit untrusted Bildschirm-/Web-Inhalt NICHT
|
|
||||||
# auto-lernen. Lucys Bildschirm-Sicht prependet diesen Marker an die User-Message (voice.py); auf dem
|
|
||||||
# Schirm sichtbarer Text könnte Injection-Anweisungen tragen, die sonst DAUERHAFT ins Gedächtnis
|
|
||||||
# wandern und Tage später ungefragt wiederverwendet werden.
|
|
||||||
SKIP_UNTRUSTED = os.environ.get("MC2_MEMORY_SKIP_UNTRUSTED", "1") not in ("0", "false", "False", "no")
|
|
||||||
UNTRUSTED_MARKERS = ("[Bildschirm-Sicht",)
|
|
||||||
# Lern-Buendelung (Phase 2, 19.07.2026): Die LLM-Extraktion lief bisher nach JEDEM Turn und
|
|
||||||
# konkurrierte dabei mit Lucys Hirn um die Slots (gleiches Modell, --parallel 2). Jetzt werden
|
|
||||||
# Turns gesammelt und als EIN Gespraechs-Block extrahiert: nach BATCH_TURNS Turns oder wenn
|
|
||||||
# BATCH_IDLE_S Sekunden Ruhe war. Ein Batch = ein Extraktionslauf statt N.
|
|
||||||
BATCH_TURNS = max(1, int(os.environ.get("MC2_MEMORY_BATCH_TURNS", "4")))
|
|
||||||
BATCH_IDLE_S = float(os.environ.get("MC2_MEMORY_BATCH_IDLE", "180"))
|
|
||||||
|
|
||||||
# Triviale Turns, die keinen dauerhaften Fakt tragen (reine Begrüßung/Quittung, Aufwärm-/Test-
|
|
||||||
# Kommandos) → gar nicht erst zum Auto-Lernen schicken. Spart Extraktions-Läufe und verhindert
|
|
||||||
# Müll-Fakten aus Meta-/Ablauf-Turns.
|
|
||||||
_TRIVIAL_TURN_RE = re.compile(
|
|
||||||
r"^\s*(?:(?:hi|hallo|hey|moin|servus|guten\s+(?:morgen|tag|abend)|danke\w*|"
|
|
||||||
r"ok(?:ay)?|alles\s+klar|passt|super|top|cool|nice|ja|nein|jo|jup|jap)[\s.,!?]*)+$"
|
|
||||||
r"|^\s*(?:aufw(?:ä|ae)rmen|/?test)\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_trivial_turn(text: str) -> bool:
|
|
||||||
return bool(_TRIVIAL_TURN_RE.search(text or ""))
|
|
||||||
|
|
||||||
|
|
||||||
def _headers() -> dict:
|
|
||||||
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
|
|
||||||
|
|
||||||
|
|
||||||
class MC2MemoryProvider(MemoryProvider):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._session_id = ""
|
|
||||||
self._agent_context = "primary"
|
|
||||||
self._q: queue.Queue[list] = queue.Queue(maxsize=200)
|
|
||||||
self._worker: threading.Thread | None = None
|
|
||||||
self._stop = threading.Event()
|
|
||||||
self._recall_cache: dict[str, str] = {}
|
|
||||||
# Turn-Puffer der Lern-Buendelung (siehe BATCH_TURNS/BATCH_IDLE_S).
|
|
||||||
self._buf: list[list[dict[str, str]]] = []
|
|
||||||
self._buf_lock = threading.Lock()
|
|
||||||
self._buf_last_add = 0.0
|
|
||||||
|
|
||||||
# -- Identität / Verfügbarkeit -------------------------------------------
|
|
||||||
def name(self) -> str:
|
|
||||||
return "mc2-memory"
|
|
||||||
|
|
||||||
def is_available(self) -> bool:
|
|
||||||
try:
|
|
||||||
r = httpx.get(f"{MC_URL}/api/health", timeout=3.0)
|
|
||||||
return r.status_code == 200
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def initialize(self, session_id: str, **kwargs) -> None:
|
|
||||||
self._session_id = session_id
|
|
||||||
# Nur im primären Agent-Kontext lernen (cron/subagent-Systemprompts nicht einlernen).
|
|
||||||
self._agent_context = kwargs.get("agent_context", "primary")
|
|
||||||
if self._worker is None or not self._worker.is_alive():
|
|
||||||
self._stop.clear()
|
|
||||||
self._worker = threading.Thread(target=self._run, name="mc2-memory-learn", daemon=True)
|
|
||||||
self._worker.start()
|
|
||||||
|
|
||||||
def system_prompt_block(self) -> str:
|
|
||||||
return (
|
|
||||||
"Du hast ein dauerhaftes, geteiltes Langzeitgedächtnis (MC2/Mem0). Relevante Fakten "
|
|
||||||
"werden dir vor einem Turn automatisch eingeblendet; neue dauerhafte Fakten über den "
|
|
||||||
"Nutzer und das Projekt werden nach dem Turn automatisch gelernt — du musst dafür "
|
|
||||||
"nichts tun."
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- Recall (vor dem Turn) -----------------------------------------------
|
|
||||||
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
|
||||||
q = (query or "").strip()
|
|
||||||
if len(q) < 3:
|
|
||||||
return ""
|
|
||||||
if q in self._recall_cache:
|
|
||||||
return self._recall_cache[q]
|
|
||||||
try:
|
|
||||||
r = httpx.get(f"{MC_URL}/api/memory", params={"q": q}, headers=_headers(), timeout=5.0)
|
|
||||||
r.raise_for_status()
|
|
||||||
items = r.json()
|
|
||||||
except Exception as exc: # nie den Turn blockieren/abbrechen
|
|
||||||
log.debug("mc2-memory prefetch failed: %s", exc)
|
|
||||||
return ""
|
|
||||||
facts = [
|
|
||||||
i for i in items
|
|
||||||
if i.get("score") is None or float(i.get("score", 0)) >= RECALL_MIN_SCORE
|
|
||||||
][:RECALL_LIMIT]
|
|
||||||
out = ""
|
|
||||||
if facts:
|
|
||||||
lines = "\n".join(f"- {f.get('content', '')}" for f in facts)
|
|
||||||
out = f"Relevante Fakten aus dem Langzeitgedächtnis:\n{lines}"
|
|
||||||
# Cache klein halten
|
|
||||||
if len(self._recall_cache) > 64:
|
|
||||||
self._recall_cache.clear()
|
|
||||||
self._recall_cache[q] = out
|
|
||||||
return out
|
|
||||||
|
|
||||||
# -- Lernen (nach dem Turn) ----------------------------------------------
|
|
||||||
def sync_turn(self, user_content: str, assistant_content: str, *,
|
|
||||||
session_id: str = "", messages: list[dict[str, Any]] | None = None) -> None:
|
|
||||||
if self._agent_context != "primary":
|
|
||||||
return
|
|
||||||
u = (user_content or "").strip()
|
|
||||||
if len(u) < MIN_USER_LEN:
|
|
||||||
return
|
|
||||||
if _is_trivial_turn(u):
|
|
||||||
log.debug("mc2-memory: trivialer Turn (Begrüßung/Quittung/Aufwärmen) — Auto-Lernen übersprungen")
|
|
||||||
return
|
|
||||||
if SKIP_UNTRUSTED and any(m in u for m in UNTRUSTED_MARKERS):
|
|
||||||
log.debug("mc2-memory: Turn mit untrusted Bildschirm-/Web-Inhalt — Auto-Lernen übersprungen")
|
|
||||||
return
|
|
||||||
msgs: list[dict[str, str]] = [{"role": "user", "content": u}]
|
|
||||||
a = (assistant_content or "").strip()
|
|
||||||
# Hermes ≥0.18 liefert optional den vollen Turn-Kontext (`messages`, inkl. Tool-Calls).
|
|
||||||
# Bewusst NUR die Tool-NAMEN mitlernen ("hat X per Tool Y geprüft") — Tool-ERGEBNISSE
|
|
||||||
# sind untrusted (Web-Inhalte!) und wären ein Poisoning-Vektor am Junk-Guard vorbei.
|
|
||||||
if messages:
|
|
||||||
tools = []
|
|
||||||
for m in messages:
|
|
||||||
for tc in (m.get("tool_calls") or []):
|
|
||||||
name = ((tc.get("function") or {}).get("name") or "").strip()
|
|
||||||
if name and name not in tools:
|
|
||||||
tools.append(name)
|
|
||||||
if tools:
|
|
||||||
a = (a + f"\n[genutzte Tools: {', '.join(tools[:6])}]").strip()
|
|
||||||
if a:
|
|
||||||
msgs.append({"role": "assistant", "content": a[:4000]})
|
|
||||||
with self._buf_lock:
|
|
||||||
self._buf.append(msgs)
|
|
||||||
self._buf_last_add = time.time()
|
|
||||||
full = len(self._buf) >= BATCH_TURNS
|
|
||||||
if full:
|
|
||||||
self._drain_buffer()
|
|
||||||
# Neue Fakten können gelandet sein → Recall-Cache invalidieren.
|
|
||||||
self._recall_cache.clear()
|
|
||||||
|
|
||||||
def _drain_buffer(self) -> None:
|
|
||||||
"""Gepufferte Turns als EINEN Gespraechs-Block in die Lern-Queue geben."""
|
|
||||||
with self._buf_lock:
|
|
||||||
if not self._buf:
|
|
||||||
return
|
|
||||||
merged: list[dict[str, str]] = []
|
|
||||||
for turn in self._buf:
|
|
||||||
merged.extend(turn)
|
|
||||||
self._buf.clear()
|
|
||||||
try:
|
|
||||||
self._q.put_nowait(merged)
|
|
||||||
except queue.Full:
|
|
||||||
log.debug("mc2-memory learn queue full — Batch übersprungen")
|
|
||||||
|
|
||||||
def _post_learn(self, msgs: list) -> None:
|
|
||||||
try:
|
|
||||||
httpx.post(f"{MC_URL}/api/memory/learn",
|
|
||||||
json={"messages": msgs, "source": "hermes"},
|
|
||||||
headers=_headers(), timeout=90.0)
|
|
||||||
except Exception as exc:
|
|
||||||
log.debug("mc2-memory learn POST failed: %s", exc)
|
|
||||||
|
|
||||||
def _run(self) -> None:
|
|
||||||
while not self._stop.is_set():
|
|
||||||
try:
|
|
||||||
msgs = self._q.get(timeout=5.0)
|
|
||||||
except queue.Empty:
|
|
||||||
# Idle-Flush: Ruhe im Gespraech → angesammelte Turns jetzt lernen.
|
|
||||||
with self._buf_lock:
|
|
||||||
idle = (self._buf and
|
|
||||||
time.time() - self._buf_last_add >= BATCH_IDLE_S)
|
|
||||||
if idle:
|
|
||||||
self._drain_buffer()
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
self._post_learn(msgs)
|
|
||||||
finally:
|
|
||||||
self._q.task_done()
|
|
||||||
|
|
||||||
def _flush(self) -> None:
|
|
||||||
"""Offene Turns garantiert rausschreiben — bei Session-Ende/CLI-Exit, wo der
|
|
||||||
Prozess sofort beendet wird (daemon-Worker würde sonst mitten im POST sterben)."""
|
|
||||||
self._drain_buffer()
|
|
||||||
self._stop.set()
|
|
||||||
if self._worker and self._worker.is_alive():
|
|
||||||
self._worker.join(timeout=95.0) # laufenden Worker-POST zu Ende lassen
|
|
||||||
while True: # vom Worker nicht mehr abgeholte Turns
|
|
||||||
try:
|
|
||||||
msgs = self._q.get_nowait()
|
|
||||||
except queue.Empty:
|
|
||||||
break
|
|
||||||
self._post_learn(msgs)
|
|
||||||
|
|
||||||
# -- Context-only: keine Agent-Tools → kein Tool-Loop --------------------
|
|
||||||
def get_tool_schemas(self) -> list[dict[str, Any]]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def on_session_end(self, messages: list[dict[str, Any]]) -> None:
|
|
||||||
self._flush()
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
self._flush()
|
|
||||||
|
|
||||||
|
|
||||||
def register(ctx) -> None:
|
|
||||||
ctx.register_memory_provider(MC2MemoryProvider())
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
name: mc2-memory
|
|
||||||
version: 1.0.0
|
|
||||||
description: "MC2 Memory — context-only Provider, der Hermes ans geteilte Mem0-Gedächtnis (MC2-Sidecar) hängt: automatisches Lernen nach jedem Turn + semantischer Recall davor. Keine Agent-Tools (kein Tool-Loop)."
|
|
||||||
pip_dependencies:
|
|
||||||
- httpx
|
|
||||||
+3
-3
@@ -216,7 +216,7 @@ def system_status() -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_services() -> str:
|
def list_services() -> str:
|
||||||
"""Erreichbarkeit aller Stack-Dienste (Engine, Gateway, Hermes, Mem0, Voice)."""
|
"""Erreichbarkeit aller Stack-Dienste (Engine, Gateway, Hermes, Voice)."""
|
||||||
data = _get("/api/system/services")
|
data = _get("/api/system/services")
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
f"{'✅' if s['ok'] else '❌'} {s['name']} ({s['url']})"
|
f"{'✅' if s['ok'] else '❌'} {s['name']} ({s['url']})"
|
||||||
@@ -227,7 +227,7 @@ def list_services() -> str:
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def restart_service(service: str) -> str:
|
def restart_service(service: str) -> str:
|
||||||
"""Startet einen erlaubten Dienst neu. Erlaubt: llama-swap (Engine) | mission-control-2 |
|
"""Startet einen erlaubten Dienst neu. Erlaubt: llama-swap (Engine) | mission-control-2 |
|
||||||
hermes-gateway | hermes-terminal | mem0-service | voice-service. Der Engine-Neustart
|
hermes-gateway | hermes-terminal | voice-service. Der Engine-Neustart
|
||||||
(llama-swap = System-Dienst) kann das Box-Passwort verlangen, wenn kein NOPASSWD gesetzt ist."""
|
(llama-swap = System-Dienst) kann das Box-Passwort verlangen, wenn kein NOPASSWD gesetzt ist."""
|
||||||
res = _post("/api/system/restart", {"service": service})
|
res = _post("/api/system/restart", {"service": service})
|
||||||
return f"Restart {service}: {'ok' if res.get('ok') else 'Fehler — ' + res.get('err', '')}"
|
return f"Restart {service}: {'ok' if res.get('ok') else 'Fehler — ' + res.get('err', '')}"
|
||||||
@@ -235,7 +235,7 @@ def restart_service(service: str) -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def backup_now() -> str:
|
def backup_now() -> str:
|
||||||
"""Erstellt SOFORT ein Voll-Backup (mem0 + Configs + Secrets). Nutze dies vor riskanten
|
"""Erstellt SOFORT ein Voll-Backup (Hermes-Configs + Secrets + llama-swap). Nutze dies vor riskanten
|
||||||
Änderungen oder wenn der Nutzer ein Backup will."""
|
Änderungen oder wenn der Nutzer ein Backup will."""
|
||||||
r = _post("/api/system/backup")
|
r = _post("/api/system/backup")
|
||||||
return f"Backup erstellt: {json.dumps(r, ensure_ascii=False)}"
|
return f"Backup erstellt: {json.dumps(r, ensure_ascii=False)}"
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Mission Control 2.0 — Memory MCP Server (geteiltes Gedächtnis für ALLE Tools).
|
|
||||||
|
|
||||||
Stdio-MCP-Server. Läuft als Subprocess von Cline/Claude Code/OpenCode/Hermes.
|
|
||||||
Ruft die /api/memory-Endpunkte von Mission Control via HTTP auf.
|
|
||||||
|
|
||||||
Tool-Beschreibungen bewusst als GUARDS formuliert (konditional, nicht imperativ),
|
|
||||||
damit kleine Modelle (z.B. Hermes 14B) nicht in Aufruf-Schleifen laufen.
|
|
||||||
|
|
||||||
Env: MC_URL (default http://192.168.178.151:9001), MC_TOKEN (optional).
|
|
||||||
Install (auf dem Rechner des Tools): pip install mcp httpx
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
|
||||||
|
|
||||||
MC_URL = os.environ.get("MC_URL", "http://192.168.178.151:9001").rstrip("/")
|
|
||||||
MC_TOKEN = os.environ.get("MC_TOKEN", "")
|
|
||||||
|
|
||||||
mcp = FastMCP("mission-control-memory")
|
|
||||||
|
|
||||||
|
|
||||||
def _h() -> dict:
|
|
||||||
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
|
|
||||||
|
|
||||||
|
|
||||||
def _get(path: str, **params):
|
|
||||||
r = httpx.get(f"{MC_URL}{path}", headers=_h(), params=params, timeout=10)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
def _post(path: str, data: dict):
|
|
||||||
r = httpx.post(f"{MC_URL}{path}", headers=_h(), json=data, timeout=10)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
def _put(path: str, data: dict):
|
|
||||||
r = httpx.put(f"{MC_URL}{path}", headers=_h(), json=data, timeout=10)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
def _delete(path: str):
|
|
||||||
r = httpx.delete(f"{MC_URL}{path}", headers=_h(), timeout=10)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def get_memories(category: str = "") -> str:
|
|
||||||
"""Lädt gespeicherte Fakten/Entscheidungen aus dem geteilten Gedächtnis.
|
|
||||||
Nutze dies EINMAL am Session-Beginn, wenn du Projekt-Kontext brauchst — nicht wiederholt.
|
|
||||||
category: identity | knowledge | rules | events | (leer = alle)"""
|
|
||||||
items = _get("/api/memory", **({"category": category} if category else {}))
|
|
||||||
if not items:
|
|
||||||
return "Keine Memories gespeichert."
|
|
||||||
icon = {"identity": "👤", "knowledge": "🔵", "rules": "📋", "events": "🕒"}
|
|
||||||
return "\n".join(
|
|
||||||
f"{icon.get(m['category'], '·')} [{m['category']}] {m['content']} (ID: {m['id'][:8]})"
|
|
||||||
for m in items
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def search_memories(q: str) -> str:
|
|
||||||
"""Sucht SEMANTISCH (nach Bedeutung, nicht nur Stichwort) in den Fakten. Nutze dies
|
|
||||||
VOR add_memory (Dubletten-Check) oder wenn du eine konkrete frühere Entscheidung suchst.
|
|
||||||
Treffer sind nach Relevanz sortiert."""
|
|
||||||
items = _get("/api/memory", q=q)
|
|
||||||
if not items:
|
|
||||||
return f"Keine Treffer für '{q}'."
|
|
||||||
def _line(m):
|
|
||||||
sc = f" {m['score']:.2f}" if isinstance(m.get("score"), (int, float)) else ""
|
|
||||||
return f"[{m['category']}{sc}] {m['content']} (ID: {m['id'][:8]})"
|
|
||||||
return "\n".join(_line(m) for m in items)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def add_memory(content: str, category: str = "knowledge", source: str = "agent") -> str:
|
|
||||||
"""Speichert EINEN dauerhaften Fakt im geteilten Gedächtnis (alle Tools sehen ihn).
|
|
||||||
Nur aufrufen, WENN gerade etwas Dauerhaftes entstanden ist (Konvention, Architektur-
|
|
||||||
Entscheidung, Tech-Version, Nutzer-Präferenz) UND es noch nicht existiert (vorher
|
|
||||||
search_memories!). Knapp & atomar. Existiert ein passender Eintrag → update_memory.
|
|
||||||
category: identity (Nutzer/Vorlieben) | knowledge (Fakten/Stack) | rules (Regeln/Konventionen) | events (Ereignisse/Entscheidungen)"""
|
|
||||||
m = _post("/api/memory", {"content": content, "category": category, "source": source})
|
|
||||||
return f"Gespeichert (ID: {m['id'][:8]}): {content}"
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def update_memory(memory_id: str, content: str = "", category: str = "") -> str:
|
|
||||||
"""Aktualisiert einen bestehenden Eintrag (nur gesetzte Felder)."""
|
|
||||||
data = {k: v for k, v in (("content", content), ("category", category)) if v}
|
|
||||||
if not data:
|
|
||||||
return "Nichts zu aktualisieren."
|
|
||||||
m = _put(f"/api/memory/{memory_id}", data)
|
|
||||||
return f"Aktualisiert: {m['content']}"
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def delete_memory(memory_id: str) -> str:
|
|
||||||
"""Löscht einen veralteten/falschen Eintrag anhand seiner ID."""
|
|
||||||
_delete(f"/api/memory/{memory_id}")
|
|
||||||
return f"Eintrag {memory_id[:8]} gelöscht."
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
def learn(text: str) -> str:
|
|
||||||
"""Lässt das Gedächtnis AUTOMATISCH aus einem Gesprächsausschnitt lernen: reiche
|
|
||||||
rohe Nutzer-Aussagen/Turns durch — das Gedächtnis EXTRAHIERT die dauerhaften Fakten
|
|
||||||
selbst und mischt sie ein (ohne Dubletten). Nutze dies, wenn der Nutzer beiläufig etwas
|
|
||||||
Dauerhaftes über sich/seine Vorlieben/das Projekt erwähnt hat — du musst nicht selbst
|
|
||||||
den Fakt formulieren. Für einen bereits fertig formulierten Einzel-Fakt → add_memory."""
|
|
||||||
res = _post("/api/memory/learn", {"text": text})
|
|
||||||
items = res.get("results", []) if isinstance(res, dict) else []
|
|
||||||
learned = [r for r in items if r.get("event") in (None, "ADD", "UPDATE")]
|
|
||||||
if not learned:
|
|
||||||
return "Nichts Neues gelernt (keine dauerhaften Fakten erkannt)."
|
|
||||||
return "Gelernt:\n" + "\n".join(f"· {r.get('content')}" for r in learned)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
mcp.run()
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Mem0-Sidecar für Mission Control 2.0 — das auto-lernende, semantische Gedächtnis.
|
|
||||||
|
|
||||||
WARUM ein eigener Dienst? Mem0 + chromadb laufen nur unter Python 3.12 (`~/.mem0/venv`),
|
|
||||||
das MC2-Backend aber unter Python 3.14 (kann mem0 nicht importieren). Darum kapselt dieser
|
|
||||||
schlanke FastAPI-Dienst die Mem0-Memory-Instanz und exponiert sie auf localhost. MC2
|
|
||||||
(`backend/services/memory.py`) spricht ihn per HTTP an — die `/api/memory`-API-Form bleibt
|
|
||||||
nach außen unverändert (UI + MCP-Server kompatibel).
|
|
||||||
|
|
||||||
Aufbau:
|
|
||||||
- Vektor-Store : Chroma embedded (kein Docker auf der Box) — Pfad MEM0_CHROMA_PATH.
|
|
||||||
- Embeddings : llama.cpp `embed`-Rolle über llama-swap (/v1/embeddings, 1024 Dim).
|
|
||||||
- LLM-Extraktion: lokales `fast`-Hirn (Qwen3.6) über llama-swap (/v1/chat/completions).
|
|
||||||
Thinking wird abgeschaltet (NoThinkLLM), sonst bricht die JSON-Extraktion ab.
|
|
||||||
|
|
||||||
Läuft als systemd-User-Dienst (deploy/mem0-service.service) im `~/.mem0/venv`.
|
|
||||||
Bind: 127.0.0.1 (nur lokal; MC2 proxyt nach außen).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
from mem0 import Memory
|
|
||||||
from mem0.configs.llms.openai import OpenAIConfig
|
|
||||||
from mem0.llms.openai import OpenAILLM
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
log = logging.getLogger("mem0_service")
|
|
||||||
|
|
||||||
# --- Konfiguration (alles über Env überschreibbar; Defaults = Box-Stand) ----------
|
|
||||||
USER_ID = os.environ.get("MEM0_USER_ID", "mission-control") # ein geteiltes Gehirn
|
|
||||||
EMBED_URL = os.environ.get("MEM0_EMBED_URL", "http://127.0.0.1:8080/v1")
|
|
||||||
LLM_URL = os.environ.get("MEM0_LLM_URL", "http://127.0.0.1:8080/v1")
|
|
||||||
EMBED_MODEL = os.environ.get("MEM0_EMBED_MODEL", "embed")
|
|
||||||
LLM_MODEL = os.environ.get("MEM0_LLM_MODEL", "fast")
|
|
||||||
EMBED_DIMS = int(os.environ.get("MEM0_EMBED_DIMS", "1024"))
|
|
||||||
CHROMA_PATH = os.environ.get("MEM0_CHROMA_PATH", "/srv/models/mem0/chroma")
|
|
||||||
HISTORY_DB = os.environ.get("MEM0_HISTORY_DB", "/srv/models/mem0/history.db")
|
|
||||||
COLLECTION = os.environ.get("MEM0_COLLECTION", "mc2")
|
|
||||||
API_KEY = os.environ.get("MEM0_API_KEY", "sk-local") # llama.cpp ignoriert den Key
|
|
||||||
LLM_MAX_TOKENS = int(os.environ.get("MEM0_LLM_MAX_TOKENS", "2048"))
|
|
||||||
SEARCH_TOP_K = int(os.environ.get("MEM0_SEARCH_TOP_K", "50"))
|
|
||||||
|
|
||||||
# --- Rerank-Zweitstufe (07.07.2026): Die Vektor-Suche findet ÄHNLICHES, der Reranker ordnet
|
|
||||||
# nach ECHTER Relevanz (Qwen3-Reranker via llama-swap /v1/rerank). Es ändert sich NUR die
|
|
||||||
# Reihenfolge: `score` bleibt der Vektor-Score (RECALL_MIN_SCORE im Hermes-Plugin bleibt
|
|
||||||
# kalibriert), Konsumenten, die vorne abschneiden (RECALL_LIMIT), bekommen automatisch die
|
|
||||||
# relevantesten Fakten. Jeder Fehler → lautlos Vektor-Reihenfolge (Gedächtnis bricht NIE).
|
|
||||||
RERANK_ENABLED = os.environ.get("MC_RERANK_ENABLED", "1") != "0"
|
|
||||||
RERANK_URL = os.environ.get("MC_RERANK_URL", "http://127.0.0.1:8080/v1/rerank")
|
|
||||||
RERANK_MODEL = os.environ.get("MC_RERANK_MODEL", "Qwen3-Reranker-0.6B")
|
|
||||||
RERANK_TIMEOUT = float(os.environ.get("MC_RERANK_TIMEOUT", "8"))
|
|
||||||
RERANK_CANDIDATES = int(os.environ.get("MC_RERANK_CANDIDATES", "20")) # nur die Top-N neu ordnen (Latenz)
|
|
||||||
|
|
||||||
# Kategorien = fundierte Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (4).
|
|
||||||
# Mem0-OSS kann nicht nativ klassifizieren (Cloud-Feature) → wir lassen das Hirn beim Lernen
|
|
||||||
# einordnen (classify_facts). Die Beschreibungen steuern die Treffsicherheit der Auto-Zuordnung.
|
|
||||||
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
|
||||||
DEFAULT_CATEGORY = "knowledge"
|
|
||||||
CATEGORY_DESCRIPTIONS = {
|
|
||||||
"identity": "Identität & Vorlieben des Nutzers: wer er ist (Name, Rolle), wie er angesprochen werden "
|
|
||||||
"will, persönliche Vorlieben und seine Arbeitsweise.",
|
|
||||||
"knowledge": "Wissen & Fakten: stabile Fakten über Stack, Projekte, Tools, Infrastruktur und Umgebung.",
|
|
||||||
"rules": "Regeln & Konventionen: verbindliche Anweisungen und Workflows — wie etwas gemacht werden "
|
|
||||||
"soll, Do's und Don'ts.",
|
|
||||||
"events": "Ereignisse & Entscheidungen: was passiert ist, getroffene Entscheidungen, zeitgebundene Vorgänge.",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NoThinkLLM(OpenAILLM):
|
|
||||||
"""`fast` (Qwen3.6) ist ein Thinking-Modell. Unter response_format=json_object
|
|
||||||
verbrennt das Reasoning Tokens und schneidet die JSON-Fakten ab (leeres Ergebnis).
|
|
||||||
`enable_thinking=false` (chat_template_kwargs) → deterministische, schnelle Extraktion."""
|
|
||||||
|
|
||||||
def generate_response(self, messages, response_format=None, tools=None,
|
|
||||||
tool_choice="auto", **kwargs):
|
|
||||||
eb = dict(kwargs.pop("extra_body", {}) or {})
|
|
||||||
eb.setdefault("chat_template_kwargs", {"enable_thinking": False})
|
|
||||||
return super().generate_response(
|
|
||||||
messages, response_format=response_format, tools=tools,
|
|
||||||
tool_choice=tool_choice, extra_body=eb, **kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_memory() -> Memory:
|
|
||||||
os.environ.setdefault("OPENAI_API_KEY", API_KEY)
|
|
||||||
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
||||||
os.makedirs(CHROMA_PATH, exist_ok=True)
|
|
||||||
os.makedirs(os.path.dirname(HISTORY_DB), exist_ok=True)
|
|
||||||
cfg = {
|
|
||||||
"vector_store": {"provider": "chroma", "config": {
|
|
||||||
"collection_name": COLLECTION, "path": CHROMA_PATH}},
|
|
||||||
"embedder": {"provider": "openai", "config": {
|
|
||||||
"model": EMBED_MODEL, "embedding_dims": EMBED_DIMS,
|
|
||||||
"openai_base_url": EMBED_URL, "api_key": API_KEY}},
|
|
||||||
"llm": {"provider": "openai", "config": {
|
|
||||||
"model": LLM_MODEL, "openai_base_url": LLM_URL, "api_key": API_KEY,
|
|
||||||
"temperature": 0.1, "max_tokens": LLM_MAX_TOKENS}},
|
|
||||||
"history_db_path": HISTORY_DB,
|
|
||||||
# Sprache zwingend Deutsch erzwingen — mem0s englischer Extraktions-Prompt zieht sonst
|
|
||||||
# Richtung Englisch. Eigennamen/Fachbegriffe bleiben unangetastet.
|
|
||||||
"custom_instructions": (
|
|
||||||
"RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN "
|
|
||||||
"NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile "
|
|
||||||
"Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. "
|
|
||||||
"NIEMALS FAKTEN ÜBER DEN ASSISTENTEN SELBST (ZWINGEND): Extrahiere KEINE Aussagen über den "
|
|
||||||
"Assistenten/die KI (Hermes, Lucy), seine Fähigkeiten, seinen Zustand, seine Tools/MCP-Server, "
|
|
||||||
"seine Cronjobs, Commit-/Versionsstände oder welche Modelle er nutzt. Das Subjekt eines Fakts "
|
|
||||||
"ist IMMER der Nutzer oder sein Projekt — niemals 'der Assistent', 'Hermes' oder 'Lucy'. "
|
|
||||||
"KEINE ZEITGEBUNDENEN ZUSTÄNDE: Ignoriere transiente Momentaufnahmen (Fehler-/Job-Läufe, "
|
|
||||||
"Commit-Rückstand, Versions-Updates, 'gerade dabei'-/'hochgefahren'-/'aufgewärmt'-Status, "
|
|
||||||
"Test- und Onboarding-Fragen). IGNORIERE VOLLSTÄNDIG und gib KEINE Fakten zurück bei: "
|
|
||||||
"Tagesnachrichten und Welt-/Politik-/Börsen-/Promi-Ereignissen, Wetter, allgemeinem Weltwissen "
|
|
||||||
"ohne Bezug zum Nutzer, vom Assistenten erzeugten Zusammenfassungen oder Recherche-Ergebnissen, "
|
|
||||||
"Smalltalk, sowie Meta-Aussagen über den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', "
|
|
||||||
"'der Nutzer möchte Y prüfen', 'der Assistent tat Z'). Speichere nur, was in Wochen noch "
|
|
||||||
"nützlich ist; im Zweifel NICHT speichern. "
|
|
||||||
"SPRACHE (ZWINGEND): Formuliere ALLE Fakten auf DEUTSCH — unabhängig von der "
|
|
||||||
"Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und "
|
|
||||||
"Produktnamen sowie etablierte Fachbegriffe (z.B. PowerShell-Push, Deploy, Qwen3.6, Neovim, "
|
|
||||||
"Vulkan) bleiben unverändert. Schreibe jeden Fakt knapp und atomar — genau eine Aussage."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
m = Memory.from_config(cfg)
|
|
||||||
# LLM gegen die Thinking-freie Variante tauschen (gleiche Verbindung).
|
|
||||||
m.llm = NoThinkLLM(OpenAIConfig(
|
|
||||||
model=LLM_MODEL, openai_base_url=LLM_URL, api_key=API_KEY,
|
|
||||||
temperature=0.1, max_tokens=LLM_MAX_TOKENS,
|
|
||||||
))
|
|
||||||
return m
|
|
||||||
|
|
||||||
|
|
||||||
_mem: Memory | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def mem() -> Memory:
|
|
||||||
global _mem
|
|
||||||
if _mem is None:
|
|
||||||
_mem = build_memory()
|
|
||||||
return _mem
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(_app: FastAPI):
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
try:
|
|
||||||
mem() # Memory beim Start aufbauen (Chroma öffnen, Modelle anpingen)
|
|
||||||
log.info("Mem0 bereit (chroma=%s, embed=%s, llm=%s)", CHROMA_PATH, EMBED_MODEL, LLM_MODEL)
|
|
||||||
except Exception:
|
|
||||||
log.exception("Mem0-Init fehlgeschlagen (Dienst läuft, /health meldet down)")
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="MC2 Mem0 Sidecar", lifespan=lifespan)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Mapping Mem0 <-> MC2-API-Form -----------------------------------------------
|
|
||||||
def _to_item(r: dict, score=None) -> dict:
|
|
||||||
meta = r.get("metadata") or {}
|
|
||||||
out = {
|
|
||||||
"id": r.get("id"),
|
|
||||||
"content": r.get("memory", ""),
|
|
||||||
"category": meta.get("category") or "stable",
|
|
||||||
"bereich": meta.get("bereich") or "",
|
|
||||||
"source": meta.get("source") or "auto",
|
|
||||||
"created_at": r.get("created_at") or "",
|
|
||||||
"updated_at": r.get("updated_at") or r.get("created_at") or "",
|
|
||||||
}
|
|
||||||
if score is not None:
|
|
||||||
try:
|
|
||||||
out["score"] = round(float(score), 4)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
class MemIn(BaseModel):
|
|
||||||
content: str
|
|
||||||
category: str = DEFAULT_CATEGORY
|
|
||||||
source: str = "manual"
|
|
||||||
|
|
||||||
|
|
||||||
class MemUp(BaseModel):
|
|
||||||
content: str | None = None
|
|
||||||
category: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class LearnIn(BaseModel):
|
|
||||||
text: str | None = None
|
|
||||||
messages: list[dict] | None = None
|
|
||||||
source: str = "auto"
|
|
||||||
category: str = DEFAULT_CATEGORY
|
|
||||||
classify: bool = True # Hirn ordnet die gelernten Fakten automatisch ein
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
def health() -> dict:
|
|
||||||
try:
|
|
||||||
mem().get_all(filters={"user_id": USER_ID}, top_k=1)
|
|
||||||
return {"ok": True}
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(503, f"mem0 nicht bereit: {exc}")
|
|
||||||
|
|
||||||
|
|
||||||
def _rerank(query: str, items: list[dict]) -> list[dict]:
|
|
||||||
"""Suchtreffer nach echter Relevanz umordnen; bei jedem Fehler unverändert zurück."""
|
|
||||||
if not (RERANK_ENABLED and len(items) > 1):
|
|
||||||
return items
|
|
||||||
try:
|
|
||||||
import httpx
|
|
||||||
r = httpx.post(RERANK_URL, json={
|
|
||||||
"model": RERANK_MODEL,
|
|
||||||
"query": query,
|
|
||||||
"documents": [i.get("content", "") for i in items],
|
|
||||||
"top_n": len(items),
|
|
||||||
}, timeout=RERANK_TIMEOUT)
|
|
||||||
r.raise_for_status()
|
|
||||||
results = r.json().get("results") or []
|
|
||||||
ranked = []
|
|
||||||
for s in sorted(results, key=lambda x: x.get("relevance_score", 0.0), reverse=True):
|
|
||||||
idx = s.get("index")
|
|
||||||
if isinstance(idx, int) and 0 <= idx < len(items):
|
|
||||||
item = dict(items[idx])
|
|
||||||
item["rerank_score"] = round(float(s.get("relevance_score", 0.0)), 4)
|
|
||||||
ranked.append(item)
|
|
||||||
# Nur übernehmen, wenn der Reranker ALLE Kandidaten bewertet hat (sonst Verlustgefahr).
|
|
||||||
return ranked if len(ranked) == len(items) else items
|
|
||||||
except Exception as exc:
|
|
||||||
log.debug("rerank übersprungen: %s", exc)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/memory")
|
|
||||||
def list_memory(q: str = "", category: str = "", bereich: str = "") -> list[dict]:
|
|
||||||
"""q gesetzt → semantische Suche (mit Relevanz-Score). Sonst → alle Fakten.
|
|
||||||
bereich='alltag'|'projekt' filtert (Fakten OHNE Bereichs-Feld laufen immer mit,
|
|
||||||
damit Alt-Bestand nie verschwindet)."""
|
|
||||||
if q:
|
|
||||||
res = mem().search(q, filters={"user_id": USER_ID}, top_k=SEARCH_TOP_K)
|
|
||||||
items = [_to_item(r, r.get("score")) for r in res.get("results", [])]
|
|
||||||
# Zweitstufe: die vorderen Kandidaten nach echter Relevanz ordnen (Rest bleibt hinten dran).
|
|
||||||
items = _rerank(q, items[:RERANK_CANDIDATES]) + items[RERANK_CANDIDATES:]
|
|
||||||
else:
|
|
||||||
res = mem().get_all(filters={"user_id": USER_ID}, top_k=1000)
|
|
||||||
items = [_to_item(r) for r in res.get("results", [])]
|
|
||||||
items.sort(key=lambda i: i.get("created_at", ""), reverse=True)
|
|
||||||
if category:
|
|
||||||
items = [i for i in items if i["category"] == category]
|
|
||||||
if bereich in ("alltag", "projekt"):
|
|
||||||
items = [i for i in items if i.get("bereich") in (bereich, "")]
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/memory", status_code=201)
|
|
||||||
def add_memory(body: MemIn) -> dict:
|
|
||||||
"""Manueller/agentischer Einzel-Fakt → VERBATIM speichern (infer=False, keine
|
|
||||||
LLM-Umformung). Auto-Lernen aus Gesprächen läuft über /learn (infer=True)."""
|
|
||||||
content = body.content.strip()
|
|
||||||
if not content:
|
|
||||||
raise HTTPException(400, "content leer")
|
|
||||||
res = mem().add(
|
|
||||||
[{"role": "user", "content": content}],
|
|
||||||
user_id=USER_ID, infer=False,
|
|
||||||
metadata={"category": body.category, "source": body.source},
|
|
||||||
)
|
|
||||||
results = res.get("results", [])
|
|
||||||
if not results:
|
|
||||||
raise HTTPException(500, "Mem0 hat nichts gespeichert")
|
|
||||||
new_id = results[0]["id"]
|
|
||||||
full = mem().get(new_id) or {}
|
|
||||||
return _to_item(full) if full else _to_item({"id": new_id, "memory": content,
|
|
||||||
"metadata": {"category": body.category, "source": body.source}})
|
|
||||||
|
|
||||||
|
|
||||||
@app.put("/memory/{mid}")
|
|
||||||
def update_memory(mid: str, body: MemUp) -> dict:
|
|
||||||
existing = mem().get(mid)
|
|
||||||
if not existing:
|
|
||||||
raise HTTPException(404, "Eintrag nicht gefunden")
|
|
||||||
cur = _to_item(existing)
|
|
||||||
new_content = body.content.strip() if body.content is not None else cur["content"]
|
|
||||||
new_cat = body.category if body.category is not None else cur["category"]
|
|
||||||
mem().update(mid, data=new_content,
|
|
||||||
metadata={"category": new_cat, "source": cur["source"]})
|
|
||||||
full = mem().get(mid) or {}
|
|
||||||
return _to_item(full)
|
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/memory/{mid}")
|
|
||||||
def delete_memory(mid: str) -> dict:
|
|
||||||
if not mem().get(mid):
|
|
||||||
raise HTTPException(404, "Eintrag nicht gefunden")
|
|
||||||
mem().delete(mid)
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/graph")
|
|
||||||
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
|
||||||
"""Fakten als Graph: Knoten = Fakten, Kanten = semantische Ähnlichkeit (Kosinus
|
|
||||||
der gespeicherten Embeddings, je Knoten die top_k Nachbarn ≥ min_score). Für die
|
|
||||||
Obsidian-artige Gedächtnis-Visualisierung im UI."""
|
|
||||||
items = mem().get_all(filters={"user_id": USER_ID}, top_k=2000).get("results", [])
|
|
||||||
nodes = [_to_item(r) for r in items]
|
|
||||||
node_ids = [n["id"] for n in nodes]
|
|
||||||
idx = {nid: i for i, nid in enumerate(node_ids)}
|
|
||||||
|
|
||||||
# Embeddings direkt aus der Chroma-Collection ziehen (kein Re-Embedding).
|
|
||||||
col = mem().vector_store.collection
|
|
||||||
raw = col.get(include=["embeddings"])
|
|
||||||
raw_ids = raw.get("ids") or []
|
|
||||||
raw_embs = raw.get("embeddings")
|
|
||||||
edges: list[dict] = []
|
|
||||||
vecs = [None] * len(node_ids)
|
|
||||||
have = 0
|
|
||||||
if raw_embs is not None:
|
|
||||||
for rid, emb in zip(raw_ids, raw_embs):
|
|
||||||
if rid in idx and emb is not None:
|
|
||||||
vecs[idx[rid]] = emb
|
|
||||||
have += 1
|
|
||||||
|
|
||||||
if have >= 2:
|
|
||||||
import numpy as np
|
|
||||||
present = [i for i, v in enumerate(vecs) if v is not None]
|
|
||||||
M = np.array([vecs[i] for i in present], dtype=float)
|
|
||||||
norms = np.linalg.norm(M, axis=1, keepdims=True)
|
|
||||||
norms[norms == 0] = 1.0
|
|
||||||
Mn = M / norms
|
|
||||||
sim = Mn @ Mn.T
|
|
||||||
seen: set[tuple[int, int]] = set()
|
|
||||||
for a in range(len(present)):
|
|
||||||
order = np.argsort(-sim[a])
|
|
||||||
cnt = 0
|
|
||||||
for b in order:
|
|
||||||
if b == a:
|
|
||||||
continue
|
|
||||||
s = float(sim[a][b])
|
|
||||||
if s < min_score:
|
|
||||||
break
|
|
||||||
i, j = present[a], present[b]
|
|
||||||
key = (min(i, j), max(i, j))
|
|
||||||
if key not in seen:
|
|
||||||
seen.add(key)
|
|
||||||
edges.append({"source": node_ids[i], "target": node_ids[j],
|
|
||||||
"weight": round(s, 3)})
|
|
||||||
cnt += 1
|
|
||||||
if cnt >= top_k:
|
|
||||||
break
|
|
||||||
return {"nodes": nodes, "edges": edges}
|
|
||||||
|
|
||||||
|
|
||||||
def classify_facts(facts: list[tuple[str, str]]) -> dict:
|
|
||||||
"""Ordnet jeden Fakt per LLM einer Kategorie UND einem Bereich zu →
|
|
||||||
{id: {"category": <slug>, "bereich": "alltag"|"projekt"}}.
|
|
||||||
|
|
||||||
Bereich (19.07.2026, Trennung der Welten): "alltag" = der Nutzer als Person
|
|
||||||
(Vorlieben, Leben, Arbeitsweise, Entscheidungen), "projekt" = Technik-/Code-/
|
|
||||||
Infrastruktur-Stände. Grundlage für einen späteren Recall-Filter, damit
|
|
||||||
Projekt-Schutt nicht in Alltags-Gespräche streut — OHNE zweites Gedächtnis.
|
|
||||||
Nutzt dasselbe (Thinking-freie) Hirn wie die Extraktion. Fehler = leeres Mapping."""
|
|
||||||
if not facts:
|
|
||||||
return {}
|
|
||||||
import json as _json
|
|
||||||
import re as _re
|
|
||||||
cat_lines = "\n".join(f"- {k}: {v}" for k, v in CATEGORY_DESCRIPTIONS.items())
|
|
||||||
system = (
|
|
||||||
"Du bist ein präziser Klassifikator für ein Langzeitgedächtnis. Ordne jeden Fakt GENAU EINER "
|
|
||||||
"Kategorie zu. Verfügbare Kategorien (nur diese Slugs verwenden):\n" + cat_lines +
|
|
||||||
"\nOrdne zusätzlich jeden Fakt GENAU EINEM Bereich zu: 'alltag' (der Nutzer als Person: "
|
|
||||||
"Vorlieben, Leben, Anrede, Arbeitsweise, grundsätzliche Entscheidungen) oder 'projekt' "
|
|
||||||
"(Technik: Code, Modelle, Server, Infrastruktur, Projekt- und Fehlerstände). "
|
|
||||||
"Antworte NUR als JSON-Objekt der Form "
|
|
||||||
"{\"<id>\": {\"kategorie\": \"<slug>\", \"bereich\": \"alltag|projekt\"}, …}."
|
|
||||||
)
|
|
||||||
body = "\n".join(f"{fid}: {txt}" for fid, txt in facts)
|
|
||||||
try:
|
|
||||||
resp = mem().llm.generate_response(
|
|
||||||
messages=[{"role": "system", "content": system},
|
|
||||||
{"role": "user", "content": "Fakten:\n" + body}],
|
|
||||||
response_format={"type": "json_object"},
|
|
||||||
)
|
|
||||||
m = _re.search(r"\{.*\}", resp or "", _re.DOTALL)
|
|
||||||
data = _json.loads(m.group(0)) if m else {}
|
|
||||||
out: dict = {}
|
|
||||||
for k, v in data.items():
|
|
||||||
if isinstance(v, str): # Alt-Form {"id": "slug"} weiter akzeptieren
|
|
||||||
if v in CATEGORIES:
|
|
||||||
out[str(k)] = {"category": v, "bereich": ""}
|
|
||||||
elif isinstance(v, dict):
|
|
||||||
cat = v.get("kategorie") or v.get("category") or ""
|
|
||||||
ber = v.get("bereich") or ""
|
|
||||||
if cat in CATEGORIES or ber in ("alltag", "projekt"):
|
|
||||||
out[str(k)] = {"category": cat if cat in CATEGORIES else DEFAULT_CATEGORY,
|
|
||||||
"bereich": ber if ber in ("alltag", "projekt") else ""}
|
|
||||||
return out
|
|
||||||
except Exception:
|
|
||||||
log.exception("Auto-Klassifikation fehlgeschlagen")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
# Deterministischer Guard: mem0-OSS extrahiert trotz custom_instructions hartnäckig Fakten ÜBER
|
|
||||||
# den Assistenten selbst (Hermes/Lucy) und transiente Zustände (Commit-Rückstand, Cronjob-Läufe,
|
|
||||||
# Onboarding-/Test-Fragen). Diese Muster werden nach der Extraktion verworfen (der Fakt wird wieder
|
|
||||||
# gelöscht) — reine Sicherungsschicht, unabhängig vom Extraktions-LLM.
|
|
||||||
_JUNK_FACT_PATTERNS = [
|
|
||||||
r"^\s*(?:der|die|das)\s+assistent", # "Der Assistent ..." (Selbstbeschreibung)
|
|
||||||
r"\bassistent(?:in)?\s+(?:hermes|lucy)\b", # "... Assistent Hermes/Lucy ..."
|
|
||||||
r"\bhermes[- ]agent\b",
|
|
||||||
r"commits?\s+hinter", # Versions-Rückstand (transient)
|
|
||||||
r"\bcronjob\b",
|
|
||||||
r"beim\s+letzten\s+(?:lauf|mal)",
|
|
||||||
r"onboarding[-\s]?frage|\btest-?frage\b",
|
|
||||||
r"hat\s+den\s+assistenten\s+gebeten",
|
|
||||||
r"auf\s+betriebstemperatur|hochgefahren",
|
|
||||||
r"aktualisiert\s+sein\s+ged(?:ä|ae)chtnis", # Selbst-Meta über das Gedächtnis
|
|
||||||
]
|
|
||||||
_JUNK_FACT_RE = re.compile("|".join(_JUNK_FACT_PATTERNS), re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_junk_fact(text: str) -> bool:
|
|
||||||
"""True, wenn der extrahierte Fakt Assistenten-Meta oder ein transienter Zustand ist."""
|
|
||||||
return bool(_JUNK_FACT_RE.search(text or ""))
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/learn")
|
|
||||||
def learn(body: LearnIn) -> dict:
|
|
||||||
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
|
|
||||||
(infer=True). Anschließend ordnet das Hirn jeden neuen Fakt automatisch einer Kategorie zu."""
|
|
||||||
msgs = body.messages
|
|
||||||
if not msgs and body.text:
|
|
||||||
msgs = [{"role": "user", "content": body.text}]
|
|
||||||
if not msgs:
|
|
||||||
raise HTTPException(400, "text oder messages erforderlich")
|
|
||||||
res = mem().add(
|
|
||||||
msgs, user_id=USER_ID, infer=True,
|
|
||||||
metadata={"category": body.category, "source": body.source},
|
|
||||||
)
|
|
||||||
results = res.get("results", [])
|
|
||||||
|
|
||||||
# Guard: frisch extrahierte Assistenten-Meta/transiente Fakten sofort wieder verwerfen.
|
|
||||||
kept = []
|
|
||||||
for r in results:
|
|
||||||
if r.get("event") in ("ADD", "UPDATE") and _is_junk_fact(r.get("memory", "")):
|
|
||||||
try:
|
|
||||||
mem().delete(r["id"])
|
|
||||||
except Exception:
|
|
||||||
log.debug("Junk-Fakt-Löschung fehlgeschlagen für %s", r.get("id"))
|
|
||||||
log.info("mem0 guard: Junk-Fakt verworfen: %s", (r.get("memory") or "")[:80])
|
|
||||||
continue
|
|
||||||
kept.append(r)
|
|
||||||
results = kept
|
|
||||||
|
|
||||||
cats: dict = {}
|
|
||||||
if body.classify:
|
|
||||||
to_classify = [(r["id"], r.get("memory", "")) for r in results
|
|
||||||
if r.get("id") and r.get("memory") and r.get("event") in ("ADD", "UPDATE")]
|
|
||||||
cats = classify_facts(to_classify)
|
|
||||||
for r in results:
|
|
||||||
info = cats.get(r.get("id"))
|
|
||||||
if info:
|
|
||||||
meta = {"category": info["category"], "source": body.source}
|
|
||||||
if info.get("bereich"):
|
|
||||||
meta["bereich"] = info["bereich"]
|
|
||||||
try:
|
|
||||||
mem().update(r["id"], data=r.get("memory", ""), metadata=meta)
|
|
||||||
except Exception:
|
|
||||||
log.debug("Kategorie-Update fehlgeschlagen für %s", r.get("id"))
|
|
||||||
|
|
||||||
return {"results": [
|
|
||||||
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event"),
|
|
||||||
"category": (cats.get(r.get("id")) or {}).get("category", body.category),
|
|
||||||
"bereich": (cats.get(r.get("id")) or {}).get("bereich", "")}
|
|
||||||
for r in results
|
|
||||||
]}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
uvicorn.run(app, host="127.0.0.1", port=int(os.environ.get("MEM0_PORT", "8765")))
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Einmalige Migration: alte SQLite-Fakten (`memories`-Tabelle aus dem v1/v2-Memory)
|
|
||||||
→ Mem0. Läuft im ~/.mem0/venv. Idempotent über eine Marker-Datei.
|
|
||||||
|
|
||||||
Jeder Alt-Eintrag wird VERBATIM übernommen (infer=False), Kategorie/Source als
|
|
||||||
Metadaten. Beim aktuellen Box-Stand ist die DB leer → No-Op, aber für
|
|
||||||
Reproduzierbarkeit/künftige Migrationen vorhanden.
|
|
||||||
|
|
||||||
Aufruf: ~/.mem0/venv/bin/python migrate.py [pfad/zur/mc2-memory.db]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from app import USER_ID, mem # nutzt dieselbe Mem0-Instanz/Config
|
|
||||||
|
|
||||||
DEFAULT_DB = os.environ.get("MC_MEMORY_DB", "/srv/models/mc2-memory.db")
|
|
||||||
MARKER = os.environ.get("MEM0_MIGRATION_MARKER", "/srv/models/mem0/.migrated_sqlite")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
db_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_DB
|
|
||||||
if os.path.exists(MARKER):
|
|
||||||
print(f"Migration bereits erledigt (Marker {MARKER}).")
|
|
||||||
return 0
|
|
||||||
if not os.path.exists(db_path):
|
|
||||||
print(f"Keine Alt-DB unter {db_path} — nichts zu migrieren.")
|
|
||||||
os.makedirs(os.path.dirname(MARKER), exist_ok=True)
|
|
||||||
open(MARKER, "w").close()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
conn = sqlite3.connect(db_path)
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
try:
|
|
||||||
rows = conn.execute(
|
|
||||||
"SELECT content, category, source FROM memories ORDER BY created_at"
|
|
||||||
).fetchall()
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
rows = []
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
m = mem()
|
|
||||||
n = 0
|
|
||||||
for r in rows:
|
|
||||||
content = (r["content"] or "").strip()
|
|
||||||
if not content:
|
|
||||||
continue
|
|
||||||
m.add([{"role": "user", "content": content}], user_id=USER_ID, infer=False,
|
|
||||||
metadata={"category": r["category"] or "stable",
|
|
||||||
"source": r["source"] or "migrated"})
|
|
||||||
n += 1
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(MARKER), exist_ok=True)
|
|
||||||
open(MARKER, "w").close()
|
|
||||||
print(f"Migriert: {n} Einträge aus {db_path} → Mem0.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Mem0-Sidecar-Deps im ~/.mem0/venv (Python 3.12). BEWUSST GEPINNT — Upgrades nur
|
|
||||||
# absichtlich, weil der Sidecar mem0-INTERNA nutzt (NoThink-LLM-Swap, Roh-Zugriff auf die
|
|
||||||
# Chroma-Collection für den Graphen, Annahmen übers Result-Format). Ein unkontrolliertes
|
|
||||||
# mem0/chromadb-Upgrade kann das still brechen.
|
|
||||||
# Upgrade-Prozedur (Backup → Pin anheben → smoke_test.py): siehe docs/UPGRADE.md.
|
|
||||||
mem0ai==2.0.8
|
|
||||||
chromadb==1.5.9
|
|
||||||
fastapi==0.138.1
|
|
||||||
uvicorn[standard]==0.49.0
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Smoke-Test für den Mem0-Sidecar — VOR und NACH jedem mem0/chromadb-Upgrade laufen lassen.
|
|
||||||
Prüft genau die Pfade, die mem0-INTERNA nutzen (LLM-Extraktion/NoThink, Auto-Einordnung,
|
|
||||||
Embeddings/Suche, Roh-Chroma-Graph). Nutzt eine WEGWERF-Collection unter /tmp — das echte
|
|
||||||
Gedächtnis (/srv/models/mem0) wird NICHT angefasst.
|
|
||||||
|
|
||||||
Aufruf: ~/.mem0/venv/bin/python ~/mission-control-v2/mem0_service/smoke_test.py
|
|
||||||
Exit 0 = alles grün, sonst 1.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import time
|
|
||||||
|
|
||||||
# WICHTIG: Env vor dem Import von app setzen (app liest die Pfade beim Modul-Load).
|
|
||||||
_TMP = tempfile.mkdtemp(prefix="mem0_smoke_")
|
|
||||||
os.environ["MEM0_CHROMA_PATH"] = os.path.join(_TMP, "chroma")
|
|
||||||
os.environ["MEM0_HISTORY_DB"] = os.path.join(_TMP, "history.db")
|
|
||||||
os.environ["MEM0_COLLECTION"] = f"smoke_{int(time.time())}"
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
import app
|
|
||||||
|
|
||||||
_fails: list[str] = []
|
|
||||||
|
|
||||||
|
|
||||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
|
||||||
print(("PASS" if cond else "FAIL"), "—", name, ("" if cond else f":: {detail}"))
|
|
||||||
if not cond:
|
|
||||||
_fails.append(name)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
app.mem() # Memory bauen (öffnet Chroma, pingt Modelle)
|
|
||||||
|
|
||||||
# 1) Verbatim add + Rücklesen
|
|
||||||
r = app.add_memory(app.MemIn(content="Smoke: der Himmel ist blau.", category="knowledge", source="smoke"))
|
|
||||||
check("verbatim add + get", bool(r.get("id")) and "blau" in r.get("content", ""), str(r)[:160])
|
|
||||||
|
|
||||||
# 2) Embeddings / semantische Suche (Bedeutung, nicht Wort)
|
|
||||||
res = app.list_memory(q="Welche Farbe hat der Himmel?")
|
|
||||||
check("semantische Suche", any("blau" in i.get("content", "") for i in res), str(res)[:160])
|
|
||||||
|
|
||||||
# 3) Auto-Lernen: LLM-Extraktion (NoThink) + Deutsch erzwungen
|
|
||||||
lr = app.learn(app.LearnIn(text="Ich heiße Max und programmiere am liebsten in Go.", source="smoke"))
|
|
||||||
items = lr.get("results", [])
|
|
||||||
check("Auto-Lernen extrahiert Fakten", len(items) > 0, str(lr)[:160])
|
|
||||||
joined = " ".join((i.get("content") or "") for i in items).lower()
|
|
||||||
check("Extraktion auf Deutsch", any(w in joined for w in ("heißt", "programmiert", "mag", "bevorzugt", "name", "nutzt")), joined[:160])
|
|
||||||
|
|
||||||
# 4) Auto-Einordnung in die 4 gültigen Kategorien
|
|
||||||
cats = {i.get("category") for i in items if i.get("category")}
|
|
||||||
check("Auto-Einordnung gültige Kategorie", bool(cats) and cats <= set(app.CATEGORIES), str(cats))
|
|
||||||
|
|
||||||
# 5) Graph aus den Roh-Embeddings (Chroma-Interna)
|
|
||||||
g = app.graph()
|
|
||||||
check("Graph liefert Knoten", len(g.get("nodes", [])) >= 1, str({k: len(v) for k, v in g.items()}))
|
|
||||||
|
|
||||||
print("\n" + ("ALLE TESTS GRÜN ✓" if not _fails else f"FEHLGESCHLAGEN: {_fails}"))
|
|
||||||
return 0 if not _fails else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
code = main()
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(_TMP, ignore_errors=True)
|
|
||||||
sys.exit(code)
|
|
||||||
Reference in New Issue
Block a user