chore: sync local workspace state
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
26
src/store.py
26
src/store.py
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user