fix(cron): Session-Takeover-Workaround, mobil-Dashboard, Import-Fix standalone
- feat(isolation): Cron-Wrapper (subprocess, /workspace/cron_tasks) - fix(dashboard): mobil-optimierte Karten statt Tabelle - fix(imports): sys.path + venv auto-activation - chore: Lasse flüchtiges /tmp hinter mir Closes-Bug: Session-Takeover bei isolated Cron Engramme-bestaetigt: Standalone-Import-OK, Wrapper-getestet
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
"""Second Brain - Gedächtnissystem für OpenClaw."""
|
||||
|
||||
try:
|
||||
from .engram import Engram, Grounding, Correctness, ReviewEntry
|
||||
from .store import EngramStore
|
||||
from .retriever import Retriever
|
||||
except ImportError:
|
||||
# Fallback: ChromaDB optional, SQLite-core funktioniert immer
|
||||
from .engram import Engram, Grounding, Correctness, ReviewEntry
|
||||
from .store import EngramStore
|
||||
Retriever = None
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["Engram", "Grounding", "Correctness", "ReviewEntry", "EngramStore", "Retriever"]
|
||||
|
||||
147
src/dashboard.py
147
src/dashboard.py
@@ -13,12 +13,23 @@ import argparse
|
||||
import http.server
|
||||
import socketserver
|
||||
import webbrowser
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from .store import EngramStore
|
||||
from .engram import Engram, Grounding
|
||||
from .retriever import Retriever
|
||||
# Insert project root so `python3 src/dashboard.py` works without `-m`
|
||||
project_root = str(Path(__file__).parent.parent)
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from src.store import EngramStore
|
||||
from src.engram import Engram, Grounding
|
||||
|
||||
# Retriever: optional – im venv verfügbar, sonst Fallback
|
||||
try:
|
||||
from src.retriever import Retriever
|
||||
except ImportError:
|
||||
Retriever = None
|
||||
|
||||
DB_PATH = Path(__file__).parent.parent / "data" / "brain.sqlite"
|
||||
HTML_PATH = Path(__file__).parent.parent / "data" / "dashboard.html"
|
||||
@@ -27,9 +38,29 @@ HTML_PATH = Path(__file__).parent.parent / "data" / "dashboard.html"
|
||||
def generate_dashboard() -> str:
|
||||
"""Generiert HTML-Dashboard aus aktuellem Brain-Stand."""
|
||||
store = EngramStore(str(DB_PATH))
|
||||
egs = store.get_all(limit=100)
|
||||
|
||||
# Stats: mit Retriever (venv) oder manuell berechnen
|
||||
if Retriever:
|
||||
ret = Retriever(store)
|
||||
stats = ret.stats()
|
||||
egs = store.get_all(limit=100)
|
||||
errors = store.search_tag("error", limit=100)
|
||||
stats["errors"] = len(errors)
|
||||
else:
|
||||
from src.engram import Correctness
|
||||
all_egs = store.get_all(limit=10000)
|
||||
confirmed = sum(1 for e in all_egs if e.correctness.confirmed)
|
||||
errors = store.search_tag("error", limit=100)
|
||||
confidences = [e.compute_confidence() for e in all_egs]
|
||||
stats = {
|
||||
"total_engrams": len(all_egs),
|
||||
"confirmed": confirmed,
|
||||
"unconfirmed": len(all_egs) - confirmed,
|
||||
"sources": {},
|
||||
"db_size_bytes": 0,
|
||||
"avg_confidence": sum(confidences) / len(confidences) if confidences else 0.0,
|
||||
"errors": len(errors),
|
||||
}
|
||||
|
||||
# Farbe nach Confidence
|
||||
def color(conf):
|
||||
@@ -48,95 +79,81 @@ def generate_dashboard() -> str:
|
||||
except:
|
||||
return "UNKNOWN"
|
||||
|
||||
# Liste der Engramme
|
||||
rows = []
|
||||
# Karten-Ansicht für Mobil
|
||||
card_rows = []
|
||||
for eg in egs:
|
||||
conf = eg.compute_confidence()
|
||||
rows.append(f"""
|
||||
<tr>
|
||||
<td><span style="color:{color(conf)};font-size:1.2em">{marker(conf)}</span></td>
|
||||
<td><code>{str(eg.id)[:8]}</code></td>
|
||||
<td>{eg.content[:100]}{'...' if len(eg.content) > 100 else ''}</td>
|
||||
<td><span class="badge">{grounding_name(eg.metadata.get('grounding', 0))}</span></td>
|
||||
<td><span class="badge">{eg.metadata.get('source', '?')}</span></td>
|
||||
<td>{conf:.2f}</td>
|
||||
<td>{eg.correctness.confirmations}/{eg.correctness.rejections}</td>
|
||||
<td>{eg.metadata.get('access_count', 0)}</td>
|
||||
<td>{', '.join(eg.metadata.get('tags', [])[:3])}</td>
|
||||
</tr>
|
||||
card_rows.append(f"""
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>{marker(conf)} {str(eg.id)[:8]}</span>
|
||||
<span class="badge" style="background:rgba({int((1-conf)*200+55)},{int(conf*200+55)},100,0.3)">{conf:.2f}</span>
|
||||
</div>
|
||||
<div class="card-content">{eg.content[:150]}{'...' if len(eg.content) > 150 else ''}</div>
|
||||
<div class="card-meta">
|
||||
<span class="badge">{grounding_name(eg.metadata.get('grounding', 0))}</span>
|
||||
<span class="badge">{eg.metadata.get('source', '?')}</span>
|
||||
<span class="badge">✓{eg.correctness.confirmations}/✗{eg.correctness.rejections}</span>
|
||||
{' '.join([f'<span class="badge">{t}</span>' for t in eg.metadata.get('tags', [])[:3]])}
|
||||
</div>
|
||||
</div>
|
||||
""")
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🧠 Second Brain Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>🧠 Second Brain</title>
|
||||
<style>
|
||||
:root {{ --bg: #1a1a2e; --card: #16213e; --text: #eee; --accent: #0f4c75; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 20px; }}
|
||||
h1 {{ margin: 0 0 10px; font-size: 1.8em; }}
|
||||
.stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 25px; }}
|
||||
.stat-card {{ background: var(--card); border-radius: 12px; padding: 15px; text-align: center; }}
|
||||
.stat-card .num {{ font-size: 2em; font-weight: bold; color: #4fc3f7; }}
|
||||
.stat-card .lbl {{ font-size: 0.85em; opacity: 0.7; }}
|
||||
table {{ width: 100%; border-collapse: collapse; background: var(--card); border-radius: 12px; overflow: hidden; }}
|
||||
th {{ text-align: left; padding: 10px; background: var(--accent); font-size: 0.85em; text-transform: uppercase; }}
|
||||
td {{ padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.9em; vertical-align: top; }}
|
||||
tr:hover td {{ background: rgba(255,255,255,0.03); }}
|
||||
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 12px; background: rgba(79,195,247,0.2); font-size: 0.75em; }}
|
||||
.search {{ margin-bottom: 20px; }}
|
||||
.search input {{ width: 100%; max-width: 400px; padding: 10px 15px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: var(--card); color: var(--text); font-size: 1em; }}
|
||||
.refresh {{ position: fixed; top: 20px; right: 20px; background: #2ecc71; color: #000; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }}
|
||||
.footer {{ margin-top: 30px; text-align: center; opacity: 0.5; font-size: 0.8em; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 10px; font-size: 14px; }}
|
||||
h1 {{ margin: 0 0 8px; font-size: 1.3em; text-align: center; }}
|
||||
.stats {{ display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 15px; }}
|
||||
.stat-card {{ background: var(--card); border-radius: 10px; padding: 10px; text-align: center; }}
|
||||
.stat-card .num {{ font-size: 1.5em; font-weight: bold; color: #4fc3f7; }}
|
||||
.stat-card .lbl {{ font-size: 0.75em; opacity: 0.7; }}
|
||||
.search {{ margin-bottom: 10px; }}
|
||||
.search input {{ width: 100%; box-sizing: border-box; padding: 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); background: var(--card); color: var(--text); font-size: 16px; }}
|
||||
.card {{ background: var(--card); border-radius: 12px; padding: 12px; margin-bottom: 10px; }}
|
||||
.card-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
|
||||
.card-id {{ font-size: 0.75em; opacity: 0.6; }}
|
||||
.card-content {{ font-size: 0.9em; line-height: 1.4; margin-bottom: 8px; word-break: break-word; }}
|
||||
.card-meta {{ display: flex; flex-wrap: wrap; gap: 6px; font-size: 0.75em; }}
|
||||
.badge {{ display: inline-block; padding: 3px 8px; border-radius: 12px; background: rgba(79,195,247,0.2); }}
|
||||
.refresh {{ position: fixed; bottom: 15px; right: 15px; background: #2ecc71; color: #000; border: none; padding: 12px 16px; border-radius: 50%; cursor: pointer; font-weight: bold; font-size: 1.2em; box-shadow: 0 2px 8px rgba(0,0,0,0.4); }}
|
||||
.footer {{ margin-top: 20px; text-align: center; opacity: 0.5; font-size: 0.7em; padding-bottom: 60px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧠 Second Brain Dashboard</h1>
|
||||
<button class="refresh" onclick="location.reload()">🔄 Aktualisieren</button>
|
||||
<h1>🧠 Second Brain</h1>
|
||||
<button class="refresh" onclick="location.reload()">↻</button>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card"><div class="num">{stats['total_engrams']}</div><div class="lbl">Engramme</div></div>
|
||||
<div class="stat-card"><div class="num">{stats['confirmed']}</div><div class="lbl">Bestätigt</div></div>
|
||||
<div class="stat-card"><div class="num">{stats['unconfirmed']}</div><div class="lbl">Unbestätigt</div></div>
|
||||
<div class="stat-card"><div class="num">{len(stats.get('sources', dict()))}</div><div class="lbl">Quellen</div></div>
|
||||
<div class="stat-card"><div class="num">{stats['db_size_bytes']/1024:.0f}</div><div class="lbl">KB Größe</div></div>
|
||||
<div class="stat-card"><div class="num">{stats['errors']}</div><div class="lbl">Fehler</div></div>
|
||||
</div>
|
||||
|
||||
<div class="search">
|
||||
<input type="text" placeholder="🔍 Suche nach Engrammen..." id="searchInput" onkeyup="filterTable()">
|
||||
<input type="text" placeholder="🔍 Suche..." id="searchInput" onkeyup="filterCards()">
|
||||
</div>
|
||||
|
||||
<table id="engramTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>ID</th>
|
||||
<th>Inhalt</th>
|
||||
<th>Grounding</th>
|
||||
<th>Quelle</th>
|
||||
<th>Confidence</th>
|
||||
<th>Feedback</th>
|
||||
<th>Zugriffe</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{''.join(rows)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="cards">
|
||||
{''.join(card_rows)}
|
||||
</div>
|
||||
|
||||
<div class="footer">Generated {__import__('datetime').datetime.now().isoformat()}</div>
|
||||
<div class="footer">Generated {__import__('datetime').datetime.now().strftime('%H:%M')}</div>
|
||||
|
||||
<script>
|
||||
function filterTable() {{
|
||||
function filterCards() {{
|
||||
var input = document.getElementById('searchInput');
|
||||
var filter = input.value.toLowerCase();
|
||||
var table = document.getElementById('engramTable');
|
||||
var rows = table.getElementsByTagName('tr');
|
||||
for (var i=1; i<rows.length; i++) {{
|
||||
var txt = rows[i].textContent || rows[i].innerText;
|
||||
rows[i].style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
|
||||
var cards = document.getElementById('cards').getElementsByClassName('card');
|
||||
for (var i=0; i<cards.length; i++) {{
|
||||
var txt = cards[i].textContent || cards[i].innerText;
|
||||
cards[i].style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
|
||||
@@ -16,10 +16,27 @@ from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
# Ensure project root is on sys.path for standalone usage
|
||||
project_root = str(Path(__file__).parent.parent)
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# Activate virtualenv if available (for chromadb etc.)
|
||||
venv_path = Path(__file__).parent.parent / ".venv"
|
||||
if venv_path.exists():
|
||||
venv_site_packages = list((venv_path / "lib").glob("python3.*/site-packages"))
|
||||
if venv_site_packages and str(venv_site_packages[0]) not in sys.path:
|
||||
sys.path.insert(0, str(venv_site_packages[0]))
|
||||
|
||||
# Second Brain Import
|
||||
from .store import EngramStore
|
||||
from .engram import Engram, Grounding
|
||||
from .retriever import Retriever
|
||||
from src.engram import Engram, Grounding
|
||||
from src.store import EngramStore
|
||||
|
||||
# Retriever: optional (braucht chromadb)
|
||||
try:
|
||||
from src.retriever import Retriever
|
||||
except ImportError:
|
||||
Retriever = None
|
||||
|
||||
|
||||
# --- Konfiguration ---
|
||||
@@ -75,11 +92,6 @@ def heartbeat_check() -> Optional[str]:
|
||||
Rückgabe: Nachricht für den User, oder None wenn nichts zu tun.
|
||||
"""
|
||||
store = get_brain()
|
||||
ret = Retriever(store)
|
||||
|
||||
# A: Unbestätigte Engramme die seit längerem nicht geprüft wurden
|
||||
# B: Hohe-Prioritäts-Themen (tags wie "wichtig", "dringend")
|
||||
# C: Fehler-Engramme die repeating sind
|
||||
|
||||
# Prüfe auf wichtige unbestätigte Engramme
|
||||
egs = store.get_all(limit=50)
|
||||
@@ -89,8 +101,7 @@ def heartbeat_check() -> Optional[str]:
|
||||
][:5]
|
||||
|
||||
if unconfirmed:
|
||||
ids = ", ".join([str(eg.id)[:8] for eg in unconfirmed])
|
||||
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfimed])
|
||||
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfirmed])
|
||||
return (
|
||||
f"🧠 Second Brain Heartbeat\n"
|
||||
f"Unbestätigte Engramme mit gutem Confidence-Score:\n{contents}\n"
|
||||
@@ -195,8 +206,15 @@ def enrich_context(topic: str, limit: int = 3) -> str:
|
||||
# memory_context in das Prompt einbauen
|
||||
"""
|
||||
store = get_brain()
|
||||
|
||||
# Versuche Retriever (mit Embeddings), fallback auf einfache Textsuche
|
||||
if Retriever:
|
||||
ret = Retriever(store)
|
||||
results = ret.retrieve(topic, limit=limit, min_confidence=0.3)
|
||||
else:
|
||||
results_raw = store.search_text(topic, limit=limit)
|
||||
results = [{"engram": eg, "score": 0.5} for eg in results_raw]
|
||||
|
||||
if not results:
|
||||
return ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user