chore: sync local workspace state

This commit is contained in:
2026-05-30 00:38:57 +02:00
parent 20098a3253
commit e6e8eba8f6
8 changed files with 5626 additions and 68 deletions

View File

@@ -134,11 +134,19 @@ def _node_size(access_count: int) -> float:
def generate_graph_html(store: EngramStore, output_path: str) -> str:
"""Generiert interaktive HTML-Graph-Visualisierung."""
engrams = store.get_all()
# store.get_all() defaults to 1000; paginate so the graph can include all nodes.
engrams = []
offset = 0
while True:
batch = store.get_all(limit=2000, offset=offset)
if not batch:
break
engrams.extend(batch)
offset += len(batch)
nodes = []
edges = []
node_ids = set()
node_ids = set(str(e.id) for e in engrams)
for eg in engrams:
eid = str(eg.id)
@@ -156,22 +164,19 @@ def generate_graph_html(store: EngramStore, output_path: str) -> str:
"size": size,
"confidence": conf,
"confirmed": eg.correctness.confirmed,
"verdict": getattr(eg.correctness, "verdict", "unknown"),
"source": eg.metadata.get("source", "?"),
"tags": tags,
}
})
node_ids.add(eid)
# Add edges after all nodes are known (otherwise early nodes miss links).
for eg in engrams:
eid = str(eg.id)
for lid in eg.links:
lid_s = str(lid)
if lid_s in node_ids:
edges.append({
"data": {
"id": f"{eid}_{lid_s}",
"source": eid,
"target": lid_s,
}
})
edges.append({"data": {"id": f"{eid}_{lid_s}", "source": eid, "target": lid_s}})
elements = {"nodes": nodes, "edges": edges}
html = _HTML_TEMPLATE.format(elements_json=json.dumps(elements, ensure_ascii=False))

View File

@@ -12,6 +12,7 @@ import os
import sys
import json
import traceback
import re
from pathlib import Path
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
@@ -48,6 +49,45 @@ except Exception:
# --- Konfiguration ---
BRAIN_DB = Path(__file__).parent.parent / "data" / "brain.sqlite"
_UUID_RE = re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.I)
_SHORT_ID_RE = re.compile(r"\b[0-9a-f]{8}\b", re.I)
def _detect_feedback(content: str) -> dict | None:
"""
Heuristik: erkennt kurze Korrektur-/Feedback-Nachrichten in Chats.
Returns:
{"kind":"confirm"|"reject"|"stop", "target": <id or short-id or None>, "raw": <normalized>}
"""
if not isinstance(content, str):
return None
raw = content.strip()
if not raw:
return None
norm = raw.lower().strip()
target = None
m = _UUID_RE.search(raw)
if m:
target = m.group(0)
else:
m2 = _SHORT_ID_RE.search(raw)
if m2:
target = m2.group(0)
if norm in {"stop", "stopp", "halt"}:
return {"kind": "stop", "target": target, "raw": norm}
if norm in {"nein", "no", "falsch", "wrong"}:
return {"kind": "reject", "target": target, "raw": norm}
if norm in {"ja", "yes", "richtig", "korrekt", "stimmt"}:
return {"kind": "confirm", "target": target, "raw": norm}
if norm.startswith(("korrigiert", "korrektur", "correction")):
if "richtig" in norm or "korrekt" in norm:
return {"kind": "confirm", "target": target, "raw": norm}
return {"kind": "reject", "target": target, "raw": norm}
return None
def get_brain() -> EngramStore:
"""Gibt initialisierten Brain-Store."""
@@ -77,14 +117,43 @@ def save_session_learned(
)
"""
store = get_brain()
tags = tags or []
fb = _detect_feedback(content) if source == "session" else None
fb_target_id: str | None = None
if fb:
tags = list(dict.fromkeys(tags + ["feedback", fb["kind"]]))
confidence = min(confidence, 0.2)
grounding = Grounding.ASSUMPTION
if session_id:
recent = store.get_latest_by_session_id(session_id, limit=10, exclude_tags=["feedback"])
if recent:
fb_target_id = str(recent[0].id)
eg = Engram.create(
content=content,
source=source,
tags=tags or [],
tags=tags,
session_id=session_id,
confidence=confidence,
grounding=grounding,
)
if fb and fb_target_id:
target = store.get(fb_target_id)
if target:
try:
# Link both ways for graphing/traceability
eg.links.append(target.id)
if eg.id not in target.links:
target.links.append(eg.id)
# Lock target so auto-review does not keep "re-deciding" after a correction signal.
target.metadata["predict_locked"] = True
target.metadata["predict_locked_reason"] = f"feedback:{fb['raw']}"
target.metadata["predict_locked_at"] = datetime.now(timezone.utc).isoformat()
store.save(target)
except Exception:
pass
store.save(eg)
return eg

View File

@@ -136,6 +136,32 @@ class EngramStore:
).fetchall()
return [self._row_to_engram(r) for r in rows]
def get_latest_by_session_id(
self,
session_id: str,
*,
limit: int = 5,
exclude_tags: Optional[List[str]] = None,
) -> List[Engram]:
"""
Lädt die neuesten Engramme für eine OpenClaw-Session-ID.
Hinweis: session_id liegt im `metadata_json`; wir nutzen eine robuste
LIKE-Suche, damit auch Legacy-Records gefunden werden.
"""
if not session_id:
return []
rows = self._conn.execute(
"SELECT * FROM engrams WHERE metadata_json LIKE ? ORDER BY created_at DESC LIMIT ?",
(f'%"session_id": "{session_id}"%', limit),
).fetchall()
engrams = [self._row_to_engram(r) for r in rows]
if exclude_tags:
ex = set(t for t in exclude_tags if isinstance(t, str))
if ex:
engrams = [e for e in engrams if not (ex & set(e.metadata.get("tags", []) or []))]
return engrams
def delete(self, engram_id: str) -> bool:
"""Löscht ein Engramm und alle Verknüpfungen."""
rowid = self._conn.execute(