diff --git a/chat_autosave.py b/chat_autosave.py new file mode 100644 index 0000000..6af1cb9 --- /dev/null +++ b/chat_autosave.py @@ -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'") diff --git a/cron_tasks/backup_secondbrain.py b/cron_tasks/backup_secondbrain.py new file mode 100644 index 0000000..67758c2 --- /dev/null +++ b/cron_tasks/backup_secondbrain.py @@ -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()) diff --git a/cron_tasks/brain_rules.py b/cron_tasks/brain_rules.py new file mode 100644 index 0000000..f4d0f98 --- /dev/null +++ b/cron_tasks/brain_rules.py @@ -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}") diff --git a/cron_tasks/heartbeat_secondbrain.py b/cron_tasks/heartbeat_secondbrain.py new file mode 100644 index 0000000..3612562 --- /dev/null +++ b/cron_tasks/heartbeat_secondbrain.py @@ -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()) diff --git a/openclaw_cron_wrapper.py b/openclaw_cron_wrapper.py new file mode 100644 index 0000000..8a25a84 --- /dev/null +++ b/openclaw_cron_wrapper.py @@ -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())