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."""
|
"""Second Brain - Gedächtnissystem für OpenClaw."""
|
||||||
|
|
||||||
|
try:
|
||||||
from .engram import Engram, Grounding, Correctness, ReviewEntry
|
from .engram import Engram, Grounding, Correctness, ReviewEntry
|
||||||
from .store import EngramStore
|
from .store import EngramStore
|
||||||
from .retriever import Retriever
|
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"
|
__version__ = "0.1.0"
|
||||||
__all__ = ["Engram", "Grounding", "Correctness", "ReviewEntry", "EngramStore", "Retriever"]
|
__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 http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from .store import EngramStore
|
# Insert project root so `python3 src/dashboard.py` works without `-m`
|
||||||
from .engram import Engram, Grounding
|
project_root = str(Path(__file__).parent.parent)
|
||||||
from .retriever import Retriever
|
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"
|
DB_PATH = Path(__file__).parent.parent / "data" / "brain.sqlite"
|
||||||
HTML_PATH = Path(__file__).parent.parent / "data" / "dashboard.html"
|
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:
|
def generate_dashboard() -> str:
|
||||||
"""Generiert HTML-Dashboard aus aktuellem Brain-Stand."""
|
"""Generiert HTML-Dashboard aus aktuellem Brain-Stand."""
|
||||||
store = EngramStore(str(DB_PATH))
|
store = EngramStore(str(DB_PATH))
|
||||||
|
egs = store.get_all(limit=100)
|
||||||
|
|
||||||
|
# Stats: mit Retriever (venv) oder manuell berechnen
|
||||||
|
if Retriever:
|
||||||
ret = Retriever(store)
|
ret = Retriever(store)
|
||||||
stats = ret.stats()
|
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
|
# Farbe nach Confidence
|
||||||
def color(conf):
|
def color(conf):
|
||||||
@@ -48,95 +79,81 @@ def generate_dashboard() -> str:
|
|||||||
except:
|
except:
|
||||||
return "UNKNOWN"
|
return "UNKNOWN"
|
||||||
|
|
||||||
# Liste der Engramme
|
# Karten-Ansicht für Mobil
|
||||||
rows = []
|
card_rows = []
|
||||||
for eg in egs:
|
for eg in egs:
|
||||||
conf = eg.compute_confidence()
|
conf = eg.compute_confidence()
|
||||||
rows.append(f"""
|
card_rows.append(f"""
|
||||||
<tr>
|
<div class="card">
|
||||||
<td><span style="color:{color(conf)};font-size:1.2em">{marker(conf)}</span></td>
|
<div class="card-header">
|
||||||
<td><code>{str(eg.id)[:8]}</code></td>
|
<span>{marker(conf)} {str(eg.id)[:8]}</span>
|
||||||
<td>{eg.content[:100]}{'...' if len(eg.content) > 100 else ''}</td>
|
<span class="badge" style="background:rgba({int((1-conf)*200+55)},{int(conf*200+55)},100,0.3)">{conf:.2f}</span>
|
||||||
<td><span class="badge">{grounding_name(eg.metadata.get('grounding', 0))}</span></td>
|
</div>
|
||||||
<td><span class="badge">{eg.metadata.get('source', '?')}</span></td>
|
<div class="card-content">{eg.content[:150]}{'...' if len(eg.content) > 150 else ''}</div>
|
||||||
<td>{conf:.2f}</td>
|
<div class="card-meta">
|
||||||
<td>{eg.correctness.confirmations}/{eg.correctness.rejections}</td>
|
<span class="badge">{grounding_name(eg.metadata.get('grounding', 0))}</span>
|
||||||
<td>{eg.metadata.get('access_count', 0)}</td>
|
<span class="badge">{eg.metadata.get('source', '?')}</span>
|
||||||
<td>{', '.join(eg.metadata.get('tags', [])[:3])}</td>
|
<span class="badge">✓{eg.correctness.confirmations}/✗{eg.correctness.rejections}</span>
|
||||||
</tr>
|
{' '.join([f'<span class="badge">{t}</span>' for t in eg.metadata.get('tags', [])[:3]])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
html = f"""<!DOCTYPE html>
|
html = f"""<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>🧠 Second Brain Dashboard</title>
|
<title>🧠 Second Brain</title>
|
||||||
<style>
|
<style>
|
||||||
:root {{ --bg: #1a1a2e; --card: #16213e; --text: #eee; --accent: #0f4c75; }}
|
: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; }}
|
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 10px; font-size: 1.8em; }}
|
h1 {{ margin: 0 0 8px; font-size: 1.3em; text-align: center; }}
|
||||||
.stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 25px; }}
|
.stats {{ display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 15px; }}
|
||||||
.stat-card {{ background: var(--card); border-radius: 12px; padding: 15px; text-align: center; }}
|
.stat-card {{ background: var(--card); border-radius: 10px; padding: 10px; text-align: center; }}
|
||||||
.stat-card .num {{ font-size: 2em; font-weight: bold; color: #4fc3f7; }}
|
.stat-card .num {{ font-size: 1.5em; font-weight: bold; color: #4fc3f7; }}
|
||||||
.stat-card .lbl {{ font-size: 0.85em; opacity: 0.7; }}
|
.stat-card .lbl {{ font-size: 0.75em; opacity: 0.7; }}
|
||||||
table {{ width: 100%; border-collapse: collapse; background: var(--card); border-radius: 12px; overflow: hidden; }}
|
.search {{ margin-bottom: 10px; }}
|
||||||
th {{ text-align: left; padding: 10px; background: var(--accent); font-size: 0.85em; text-transform: uppercase; }}
|
.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; }}
|
||||||
td {{ padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.9em; vertical-align: top; }}
|
.card {{ background: var(--card); border-radius: 12px; padding: 12px; margin-bottom: 10px; }}
|
||||||
tr:hover td {{ background: rgba(255,255,255,0.03); }}
|
.card-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
|
||||||
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 12px; background: rgba(79,195,247,0.2); font-size: 0.75em; }}
|
.card-id {{ font-size: 0.75em; opacity: 0.6; }}
|
||||||
.search {{ margin-bottom: 20px; }}
|
.card-content {{ font-size: 0.9em; line-height: 1.4; margin-bottom: 8px; word-break: break-word; }}
|
||||||
.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; }}
|
.card-meta {{ display: flex; flex-wrap: wrap; gap: 6px; font-size: 0.75em; }}
|
||||||
.refresh {{ position: fixed; top: 20px; right: 20px; background: #2ecc71; color: #000; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }}
|
.badge {{ display: inline-block; padding: 3px 8px; border-radius: 12px; background: rgba(79,195,247,0.2); }}
|
||||||
.footer {{ margin-top: 30px; text-align: center; opacity: 0.5; font-size: 0.8em; }}
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>🧠 Second Brain Dashboard</h1>
|
<h1>🧠 Second Brain</h1>
|
||||||
<button class="refresh" onclick="location.reload()">🔄 Aktualisieren</button>
|
<button class="refresh" onclick="location.reload()">↻</button>
|
||||||
|
|
||||||
<div class="stats">
|
<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['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['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">{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['errors']}</div><div class="lbl">Fehler</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>
|
</div>
|
||||||
|
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<input type="text" placeholder="🔍 Suche nach Engrammen..." id="searchInput" onkeyup="filterTable()">
|
<input type="text" placeholder="🔍 Suche..." id="searchInput" onkeyup="filterCards()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="engramTable">
|
<div id="cards">
|
||||||
<thead>
|
{''.join(card_rows)}
|
||||||
<tr>
|
</div>
|
||||||
<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 class="footer">Generated {__import__('datetime').datetime.now().isoformat()}</div>
|
<div class="footer">Generated {__import__('datetime').datetime.now().strftime('%H:%M')}</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function filterTable() {{
|
function filterCards() {{
|
||||||
var input = document.getElementById('searchInput');
|
var input = document.getElementById('searchInput');
|
||||||
var filter = input.value.toLowerCase();
|
var filter = input.value.toLowerCase();
|
||||||
var table = document.getElementById('engramTable');
|
var cards = document.getElementById('cards').getElementsByClassName('card');
|
||||||
var rows = table.getElementsByTagName('tr');
|
for (var i=0; i<cards.length; i++) {{
|
||||||
for (var i=1; i<rows.length; i++) {{
|
var txt = cards[i].textContent || cards[i].innerText;
|
||||||
var txt = rows[i].textContent || rows[i].innerText;
|
cards[i].style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
|
||||||
rows[i].style.display = txt.toLowerCase().indexOf(filter) > -1 ? '' : 'none';
|
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,10 +16,27 @@ from pathlib import Path
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, List, Dict, Any
|
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
|
# Second Brain Import
|
||||||
from .store import EngramStore
|
from src.engram import Engram, Grounding
|
||||||
from .engram import Engram, Grounding
|
from src.store import EngramStore
|
||||||
from .retriever import Retriever
|
|
||||||
|
# Retriever: optional (braucht chromadb)
|
||||||
|
try:
|
||||||
|
from src.retriever import Retriever
|
||||||
|
except ImportError:
|
||||||
|
Retriever = None
|
||||||
|
|
||||||
|
|
||||||
# --- Konfiguration ---
|
# --- Konfiguration ---
|
||||||
@@ -75,11 +92,6 @@ def heartbeat_check() -> Optional[str]:
|
|||||||
Rückgabe: Nachricht für den User, oder None wenn nichts zu tun.
|
Rückgabe: Nachricht für den User, oder None wenn nichts zu tun.
|
||||||
"""
|
"""
|
||||||
store = get_brain()
|
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
|
# Prüfe auf wichtige unbestätigte Engramme
|
||||||
egs = store.get_all(limit=50)
|
egs = store.get_all(limit=50)
|
||||||
@@ -89,8 +101,7 @@ def heartbeat_check() -> Optional[str]:
|
|||||||
][:5]
|
][:5]
|
||||||
|
|
||||||
if unconfirmed:
|
if unconfirmed:
|
||||||
ids = ", ".join([str(eg.id)[:8] for eg in unconfirmed])
|
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfirmed])
|
||||||
contents = "\n".join([f" - {eg.content[:80]}" for eg in unconfimed])
|
|
||||||
return (
|
return (
|
||||||
f"🧠 Second Brain Heartbeat\n"
|
f"🧠 Second Brain Heartbeat\n"
|
||||||
f"Unbestätigte Engramme mit gutem Confidence-Score:\n{contents}\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
|
# memory_context in das Prompt einbauen
|
||||||
"""
|
"""
|
||||||
store = get_brain()
|
store = get_brain()
|
||||||
|
|
||||||
|
# Versuche Retriever (mit Embeddings), fallback auf einfache Textsuche
|
||||||
|
if Retriever:
|
||||||
ret = Retriever(store)
|
ret = Retriever(store)
|
||||||
results = ret.retrieve(topic, limit=limit, min_confidence=0.3)
|
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:
|
if not results:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user