feat(systemd): Dashboard-Service, brain_rules, 18 Engramme bewertet, Cron persistent
Neu: - systemd: secondbrain-dashboard.service (Port 8501, autostart) - cron_rules.py: Auto-Confirm ab 3x, Archiv nach 30d - cron_tasks/: heartbeat + backup + brain_rules (persistent) - openclaw_cron_wrapper.py: subprocess-Isolation (kein SessionTakeover) - chat_autosave.py: Auto-Save von Chat + Kontext-Anreicherung Daten: - 18 unbestätigte Engramme bewertet: - 14x CONFIRMED (Fakten/Definitionen korrekt) - 3x ARCHIVIERT (historisch, nicht aktuell) - 1x CONFIRMED (Regel 73624013) - 0 offene unbestätigte Closes Gitea-Issue: #9
This commit is contained in:
119
chat_autosave.py
Normal file
119
chat_autosave.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Chat-Auto-Save: Wertvolle User-Nachrichten → Engramm.
|
||||||
|
Wird am Ende jeder Main-Session-Antwort aufgerufen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BRAIN_DIR = Path("/root/.openclaw/workspace/second-brain")
|
||||||
|
sys.path.insert(0, str(BRAIN_DIR))
|
||||||
|
|
||||||
|
from src.engram import Engram, Grounding
|
||||||
|
from src.store import EngramStore
|
||||||
|
|
||||||
|
DB_PATH = BRAIN_DIR / "data" / "brain.sqlite"
|
||||||
|
|
||||||
|
|
||||||
|
def _hash(text: str) -> str:
|
||||||
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
|
||||||
|
|
||||||
|
|
||||||
|
def is_fluff(content: str) -> bool:
|
||||||
|
"""Prüft ob Inhalt nur Floskel ist."""
|
||||||
|
lower = content.lower().strip().rstrip(".?!")
|
||||||
|
short_fluff = [
|
||||||
|
"hallo", "hi", "hey", "guten tag", "guten morgen", "guten abend",
|
||||||
|
"danke", "ok", "okay", "ja", "nein", "bitte", "gerne", "tschüss",
|
||||||
|
"bis später", "bis morgen", "alles klar", "in ordnung",
|
||||||
|
]
|
||||||
|
if lower in short_fluff:
|
||||||
|
return True
|
||||||
|
if len(content) < 10 and all(c in " ?,!.;:-" for c in content):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_if_worthy(content: str, source: str = "user", tags: list = None,
|
||||||
|
confidence: float = 0.7, session_id: str = None,
|
||||||
|
reasoning: str = None) -> dict:
|
||||||
|
"""
|
||||||
|
Speichert Nachricht als Engramm wenn sie Wert hat.
|
||||||
|
Wird in jeder Antwort aufgerufen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if is_fluff(content):
|
||||||
|
return {"saved": False, "reason": "fluff"}
|
||||||
|
|
||||||
|
store = EngramStore(str(DB_PATH))
|
||||||
|
content_hash = _hash(content)
|
||||||
|
recent = store.get_all(limit=200)
|
||||||
|
for eg in recent:
|
||||||
|
if _hash(eg.content) == content_hash:
|
||||||
|
return {"saved": False, "reason": "duplicate", "id": str(eg.id)}
|
||||||
|
|
||||||
|
eg = Engram.create(
|
||||||
|
content=content,
|
||||||
|
source=source,
|
||||||
|
tags=tags or ["auto-save", "chat"],
|
||||||
|
session_id=session_id,
|
||||||
|
confidence=confidence,
|
||||||
|
grounding=Grounding.ASSUMPTION,
|
||||||
|
)
|
||||||
|
store.save(eg)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"saved": True,
|
||||||
|
"id": str(eg.id),
|
||||||
|
"confidence": eg.compute_confidence(),
|
||||||
|
"first8": str(eg.id)[:8],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_prompt(topic: str, limit: int = 3) -> str:
|
||||||
|
"""
|
||||||
|
Holt relevante bestätigte Engramme für Kontext-Anreicherung.
|
||||||
|
Wird VOR jeder Antwort aufgerufen.
|
||||||
|
"""
|
||||||
|
store = EngramStore(str(DB_PATH))
|
||||||
|
recent = store.get_all(limit=100)
|
||||||
|
|
||||||
|
# Einfache Text-Suche (kein FTS wegen Satzzeichen)
|
||||||
|
topic_lower = topic.lower()
|
||||||
|
matches = []
|
||||||
|
for eg in recent:
|
||||||
|
if eg.correctness.confirmed and topic_lower in eg.content.lower():
|
||||||
|
matches.append(eg)
|
||||||
|
elif len(matches) < limit and any(t in topic_lower for t in [t.lower() for t in eg.metadata.get("tags", [])]):
|
||||||
|
matches.append(eg)
|
||||||
|
if len(matches) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = ["\n📚 Relevantes Wissen:"]
|
||||||
|
for eg in matches[:limit]:
|
||||||
|
lines.append(f" • [{eg.compute_confidence():.0%}] {eg.content[:120]}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def check_pending(session_id: str = None) -> list:
|
||||||
|
"""Gibt unbestätigte Engramme zurück."""
|
||||||
|
store = EngramStore(str(DB_PATH))
|
||||||
|
egs = store.get_all(limit=50)
|
||||||
|
pending = [eg for eg in egs if not eg.correctness.confirmed]
|
||||||
|
return pending
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
result = save_if_worthy(sys.argv[1])
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
else:
|
||||||
|
print("Usage: python3 chat_autosave.py 'Nachricht'")
|
||||||
22
cron_tasks/backup_secondbrain.py
Normal file
22
cron_tasks/backup_secondbrain.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Backup-Task für Second Brain - isoliert, persistent."""
|
||||||
|
import json, os, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
BRAIN_DIR = Path("/root/.openclaw/workspace/second-brain")
|
||||||
|
sys.path.insert(0, str(BRAIN_DIR))
|
||||||
|
from src.store import EngramStore
|
||||||
|
|
||||||
|
def main():
|
||||||
|
brain_db = os.environ.get("BRAIN_DB", str(BRAIN_DIR / "data" / "brain.sqlite"))
|
||||||
|
store = EngramStore(brain_db)
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_path = Path(brain_db).parent / f"backup_{ts}.jsonl"
|
||||||
|
count = store.export_jsonl(str(backup_path))
|
||||||
|
result = {"timestamp": datetime.now(timezone.utc).isoformat(), "backup_path": str(backup_path), "count": count, "success": True}
|
||||||
|
print(f"BACKUP: {count} Engramme -> {backup_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
53
cron_tasks/brain_rules.py
Normal file
53
cron_tasks/brain_rules.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Brain-Regeln - Automatische Bestaetigungs- und Archivierungslogik.
|
||||||
|
Wird von Cron und Agent aufgerufen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/root/.openclaw/workspace/second-brain")
|
||||||
|
|
||||||
|
from src.engram import Engram, Grounding
|
||||||
|
from src.store import EngramStore
|
||||||
|
|
||||||
|
DB = "/root/.openclaw/workspace/second-brain/data/brain.sqlite"
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rules():
|
||||||
|
store = EngramStore(DB)
|
||||||
|
egs = store.get_all(limit=1000)
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
for eg in egs:
|
||||||
|
conf = eg.compute_confidence()
|
||||||
|
age_days = eg._age_days(eg.metadata.get("created", ""))
|
||||||
|
correct = eg.correctness
|
||||||
|
|
||||||
|
# Regel 1: Triple-Confirm → Auto-Verifiziert
|
||||||
|
if not correct.confirmed and correct.confirmations >= 3:
|
||||||
|
correct.confirmed = True
|
||||||
|
correct.confirmations += 1
|
||||||
|
store.save(eg)
|
||||||
|
actions.append(f"Auto-Confirm: {str(eg.id)[:8]} (3x confirmed)")
|
||||||
|
|
||||||
|
# Regel 2: Lang unbestaetigt → ASSUMPTION Tag
|
||||||
|
if age_days > 30 and not correct.confirmed and "archiviert" not in eg.metadata.get("tags", []):
|
||||||
|
eg.metadata.setdefault("tags", []).append("archiviert")
|
||||||
|
eg.metadata["archivgrund"] = f"Unbestaetigt seit {age_days} Tagen"
|
||||||
|
store.save(eg)
|
||||||
|
actions.append(f"Archiviert: {str(eg.id)[:8]} (Alter {age_days}d)")
|
||||||
|
|
||||||
|
# Regel 3: Rejected mit 2+ Rejections → loeschen (Sanft: Tag statt rm)
|
||||||
|
if correct.rejections >= 2:
|
||||||
|
eg.metadata.setdefault("tags", []).append("deleted")
|
||||||
|
store.save(eg)
|
||||||
|
actions.append(f"Deleted-Tag: {str(eg.id)[:8]} ({correct.rejections}x rejected)")
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
actions = apply_rules()
|
||||||
|
print("Brain-Regeln angewendet:")
|
||||||
|
for a in actions or ["Keine Aktionen noetig"]:
|
||||||
|
print(f" {a}")
|
||||||
47
cron_tasks/heartbeat_secondbrain.py
Normal file
47
cron_tasks/heartbeat_secondbrain.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Heartbeat-Task für Second Brain - isoliert, persistent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
BRAIN_DIR = Path("/root/.openclaw/workspace/second-brain")
|
||||||
|
sys.path.insert(0, str(BRAIN_DIR))
|
||||||
|
|
||||||
|
from src.engram import Engram, Grounding
|
||||||
|
from src.store import EngramStore
|
||||||
|
|
||||||
|
def main():
|
||||||
|
output_file = os.environ.get("CRON_OUTPUT_FILE", "/tmp/heartbeat_result.json")
|
||||||
|
brain_db = os.environ.get("BRAIN_DB", str(BRAIN_DIR / "data" / "brain.sqlite"))
|
||||||
|
store = EngramStore(brain_db)
|
||||||
|
|
||||||
|
egs = store.get_all(limit=50)
|
||||||
|
unconfirmed = [eg for eg in egs if not eg.correctness.confirmed and eg.compute_confidence() > 0.5][:5]
|
||||||
|
errors = store.search_tag("error", limit=5)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"total_engrams": len(egs),
|
||||||
|
"unconfirmed_count": len(unconfirmed),
|
||||||
|
"error_count": len(errors),
|
||||||
|
"has_action": bool(unconfirmed) or len(errors) >= 3,
|
||||||
|
"message": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if unconfirmed:
|
||||||
|
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfirmed])
|
||||||
|
result["message"] = f"🧠 Unbestätigte Engramme:\n{contents}"
|
||||||
|
elif len(errors) >= 3:
|
||||||
|
result["message"] = f"⚠️ {len(errors)} Fehler-Engramme gespeichert."
|
||||||
|
|
||||||
|
Path(output_file).write_text(json.dumps(result, indent=2))
|
||||||
|
print(f"HEARTBEAT: {result['unconfirmed_count']} unconfirmed, {result['error_count']} errors")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
102
openclaw_cron_wrapper.py
Normal file
102
openclaw_cron_wrapper.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
OpenClaw Cron Isolation Wrapper - Updatesicherer Workaround.
|
||||||
|
Persistent: Tasks und Logs liegen im Workspace, nicht in /tmp.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# --- Konfiguration (persistent) ---
|
||||||
|
WORKSPACE = Path("/root/.openclaw/workspace")
|
||||||
|
CRON_TASKS_DIR = WORKSPACE / "cron_tasks"
|
||||||
|
LOG_FILE = WORKSPACE / "cron_wrapper.log"
|
||||||
|
BRAIN_DIR = WORKSPACE / "second-brain"
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
line = f"[{ts}] {msg}\n"
|
||||||
|
with open(LOG_FILE, "a") as f:
|
||||||
|
f.write(line)
|
||||||
|
print(line.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def run_isolated(task_name: str, task_args: dict = None) -> dict:
|
||||||
|
"""
|
||||||
|
Führt einen Task in echt isolierter Umgebung aus.
|
||||||
|
Kein Zugriff auf Session-Files, nur stdout/stderr.
|
||||||
|
"""
|
||||||
|
CRON_TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
task_script = CRON_TASKS_DIR / f"{task_name}.py"
|
||||||
|
if not task_script.exists():
|
||||||
|
return {"success": False, "error": f"Task nicht gefunden: {task_script}"}
|
||||||
|
|
||||||
|
# Temp-Verzeichnis für Output (flüchtig ist OK, Ergebnis kommt via stdout)
|
||||||
|
temp_dir = tempfile.mkdtemp(prefix=f"cron_{task_name}_")
|
||||||
|
output_file = Path(temp_dir) / "output.json"
|
||||||
|
|
||||||
|
# Saubere Env: Keine OpenClaw-Session-Variablen
|
||||||
|
env = os.environ.copy()
|
||||||
|
for key in list(env.keys()):
|
||||||
|
if "OPENCLAW" in key.upper() or "SESSION" in key.upper():
|
||||||
|
del env[key]
|
||||||
|
|
||||||
|
env["CRON_TASK_NAME"] = task_name
|
||||||
|
env["CRON_OUTPUT_FILE"] = str(output_file)
|
||||||
|
env["BRAIN_DB"] = str(BRAIN_DIR / "data" / "brain.sqlite")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(task_script)] + ([json.dumps(task_args)] if task_args else []),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300,
|
||||||
|
cwd=str(temp_dir),
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = result.stdout.strip()
|
||||||
|
stderr = result.stderr.strip()
|
||||||
|
|
||||||
|
output_data = {}
|
||||||
|
if output_file.exists():
|
||||||
|
try:
|
||||||
|
output_data = json.loads(output_file.read_text())
|
||||||
|
except Exception:
|
||||||
|
output_data = {"raw": output_file.read_text()}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": result.returncode == 0,
|
||||||
|
"returncode": result.returncode,
|
||||||
|
"stdout": stdout[-2000:] if stdout else "",
|
||||||
|
"stderr": stderr[-1000:] if stderr else "",
|
||||||
|
"output": output_data,
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {"success": False, "error": "Timeout nach 300s"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description="OpenClaw Cron Isolation Wrapper")
|
||||||
|
parser.add_argument("task", help="Task-Name aus cron_tasks/")
|
||||||
|
parser.add_argument("--args", help="JSON-Args für den Task")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
task_args = json.loads(args.args) if args.args else None
|
||||||
|
result = run_isolated(args.task, task_args)
|
||||||
|
|
||||||
|
print(json.dumps(result, indent=2, default=str))
|
||||||
|
return 0 if result["success"] else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user