3 Commits

4 changed files with 281 additions and 77 deletions

1
.streamlit/secrets.toml Normal file
View File

@@ -0,0 +1 @@
[default]

View File

@@ -1,174 +1,210 @@
"""
app_dashboard.py - Streamlit-Dashboard für Second Brain.
Seiten: Übersicht, Engramme, Suche, Graph, Stats.
Seiten: Übersicht, Engramme, Suche, Graph, Heal-Log, Neural Scorer.
"""
import json
import sys
import os
from pathlib import Path
import streamlit as st
sys.path.insert(0, str(Path(__file__).resolve().parent))
_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_root))
from src.engram import Engram
from src.store import EngramStore
from src.chroma_store import ChromaStore
from src.retriever import Retriever
from src.neural_scorer import NeuralScorer
from src.graph_view import generate_graph_html
from src.loop_detector import LoopDetector
from src.error_healer import ErrorHealer
_DEFAULT_DB = Path(__file__).resolve().parent.parent / "data" / "brain.sqlite"
_DB_PATH = str(st.secrets.get("db_path", _DEFAULT_DB) if hasattr(st, "secrets") else _DEFAULT_DB)
_DEFAULT_DB = _root / "data" / "brain.sqlite"
@st.cache_resource
def _store():
return EngramStore(_DB_PATH)
return EngramStore(str(_DEFAULT_DB))
@st.cache_resource
def _chroma():
p = Path(_DB_PATH).parent / "chroma"
p = Path(str(_DEFAULT_DB)).parent / "chroma"
return ChromaStore(str(p))
_retriever_cache = None
def _retriever():
return Retriever(_store(), _chroma())
global _retriever_cache
if _retriever_cache is None:
_retriever_cache = Retriever(_store(), _chroma())
return _retriever_cache
@st.cache_resource
def _scorer():
return NeuralScorer()
st.set_page_config(page_title="Second Brain Dashboard", layout="wide")
st.title("🧠 Second Brain Dashboard")
@st.cache_resource
def _healer():
return ErrorHealer(_store())
page = st.sidebar.radio("Seite", ["Übersicht", "Engramme", "Suche", "Graph", "Stats", "Neural Scorer"])
st.set_page_config(page_title="Second Brain Dashboard", layout="wide")
st.title("🧠 2.Brain v0.3.1")
page = st.sidebar.radio("Seite", ["Übersicht", "Engramme", "Suche", "Graph", "Heal-Log", "Neural Scorer"])
if page == "Übersicht":
store = _store()
engrams = store.get_all()
engrams = store.get_all(limit=10000)
confirmed = sum(1 for e in engrams if e.correctness.confirmed)
unconfirmed = len(engrams) - confirmed
avg_conf = sum(e.compute_confidence() for e in engrams) / max(1, len(engrams))
errors = [e for e in engrams if "error" in e.metadata.get("tags", [])]
c1, c2, c3, c4 = st.columns(4)
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("Total", len(engrams))
c2.metric("Confirmed", confirmed)
c3.metric("Pending", unconfirmed)
c4.metric("Avg Confidence", f"{avg_conf:.2f}")
c5.metric("Errors", len(errors))
st.subheader("Recent Engramme")
for eg in sorted(engrams, key=lambda e: e.metadata.get("modified", ""), reverse=True)[:5]:
with st.expander(f"{eg.content[:80]}..."):
valid = eg.validate_grounding()
marker = "" if valid["valid"] else "⚠️"
with st.expander(f"{marker} {eg.content[:80]}..."):
st.write(f"ID: `{eg.id}`")
st.write(f"Source: {eg.metadata.get('source')}")
st.write(f"Confidence: {eg.compute_confidence():.2f}")
st.write(f"Confirmed: {'' if eg.correctness.confirmed else ''}")
st.write("Tags:", ", ".join(eg.metadata.get("tags", [])))
if not valid["valid"]:
st.warning(f"Grounding: {valid['issue']}")
if st.button("Auto-Fix", key=f"af_{eg.id}"):
eg.auto_fix_grounding()
store.save(eg)
st.experimental_rerun()
elif page == "Engramme":
store = _store()
st.subheader("Alle Engramme")
st.subheader("Alle Engramme (max 1000)")
tag_filter = st.text_input("Filter tags")
source_filter = st.selectbox("Source", ["alle", "user", "agent", "web", "file", "system"])
for eg in store.get_all():
for eg in store.get_all(limit=1000):
tags = eg.metadata.get("tags", [])
src = eg.metadata.get("source", "")
if tag_filter and tag_filter not in tags:
continue
if source_filter != "alle" and source_filter != src:
continue
with st.expander(f"{eg.content[:100]}"):
st.write("Confidence:", f"{eg.compute_confidence():.2f}")
st.write("Tags:", ", ".join(tags))
st.write("Source:", src)
c1, c2 = st.columns(2)
if c1.button("Confirm", key=f"conf_{eg.id}"):
col1, col2 = st.columns([4, 1])
with col1:
conf = eg.compute_confidence()
marker = "" if conf > 0.7 else "⚠️"
st.markdown(f"{marker} **{eg.content[:100]}**")
st.caption(f"Conf: {conf:.2f} | Tags: {', '.join(tags)} | Source: {src}")
with col2:
if st.button("✅ Confirm", key=f"conf_{eg.id}"):
eg.correctness.confirm("user")
store.save(eg)
st.success("Confirmed!")
if c2.button("❌ Reject", key=f"rej_{eg.id}"):
st.success("Confirmed")
if st.button("❌ Reject", key=f"rej_{eg.id}"):
eg.correctness.reject("user")
store.save(eg)
st.warning("Rejected.")
st.warning("Rejected")
st.divider()
elif page == "Suche":
st.subheader("Semantic + Keyword Suche")
query = st.text_input("Query")
mode = st.radio("Modus", ["Hybrid", "Keyword", "Semantic"])
st.subheader("Hybrid Search (Semantic + Keyword)")
query = st.text_input("Query", placeholder="Suchbegriff eingeben...")
mode = st.radio("Modus", ["Hybrid", "Keyword", "Semantic"], horizontal=True)
if st.button("Suchen") and query:
ret = _retriever()
if mode == "Hybrid":
results = ret.hybrid_retrieve(query, limit=10)
elif mode == "Semantic":
results = ret.semantic_retrieve(query, limit=10)
else:
results = ret.retrieve(query, limit=10)
results = ret.hybrid_retrieve(query, limit=10) if mode == "Hybrid" else \
ret.semantic_retrieve(query, limit=10) if mode == "Semantic" else \
ret.retrieve(query, limit=10)
if not results:
st.info("Keine Ergebnisse gefunden.")
for r in results:
eg = r["engram"]
with st.container():
st.markdown(f"**{eg.content[:200]}...**")
st.write(f"Score: {r['score']:.3f} | Match: {r['match_type']} | Conf: {eg.compute_confidence():.2f}")
st.write(f"Score: `{r['score']:.3f}` | Match: `{r['match_type']}` | Conf: `{eg.compute_confidence():.2f}`")
c1, c2 = st.columns(2)
if c1.button("✅ Confirm", key=f"sc_{eg.id}"):
eg.correctness.confirm("user")
store = _store()
store.save(eg)
_store().save(eg)
st.success("Confirmed")
if c2.button("❌ Reject", key=f"sr_{eg.id}"):
eg.correctness.reject("user")
store = _store()
store.save(eg)
_store().save(eg)
st.warning("Rejected")
elif page == "Graph":
st.subheader("Graph-Visualisierung")
graph_html_path = Path(_DB_PATH).parent / "graph_view.html"
graph_html_path = Path(str(_DEFAULT_DB)).parent / "graph_view.html"
if st.button("Graph neu generieren"):
with st.spinner("Generiere Graph..."):
path = generate_graph_html(_store(), str(graph_html_path))
st.success(f"Graph generiert: {path}")
if graph_html_path.exists():
with open(graph_html_path, "r", encoding="utf-8") as f:
html = f.read()
# iframe
st.components.v1.html(html, height=800, scrolling=True)
st.components.v1.html(html, height=800)
else:
st.info("Graph nicht generiert. Führe `python -m src.cli graph` aus.")
if st.button("Graph generieren"):
from src.graph_view import generate_graph_html
store = _store()
path = generate_graph_html(store, str(Path(_DB_PATH).parent / "graph_view.html"))
st.success(f"Graph generiert: {path}")
st.info("Graph noch nicht generiert. Klicke oben.")
elif page == "Stats":
store = _store()
engrams = store.get_all()
st.json({
"total": len(engrams),
"confirmed": sum(1 for e in engrams if e.correctness.confirmed),
"pending": sum(1 for e in engrams if not e.correctness.confirmed),
"sources": {s: sum(1 for e in engrams if e.metadata.get("source") == s) for s in {e.metadata.get("source") for e in engrams}},
"tags": {t: sum(1 for e in engrams for t2 in e.metadata.get("tags", []) if t2 == t) for t in {t for e in engrams for t in e.metadata.get("tags", [])}},
"avg_confidence": sum(e.compute_confidence() for e in engrams) / max(1, len(engrams)),
})
elif page == "Heal-Log":
st.subheader("Error Healing & Loop Detection")
healer = _healer()
stats = healer.get_error_stats()
c1, c2, c3 = st.columns(3)
c1.metric("Total Errors", stats["total_errors"])
c2.metric("Repeated", stats["repeated_errors"])
c3.metric("Error Types", len(stats.get("error_types", {})))
st.subheader("Error Types")
for etype, count in stats.get("error_types", {}).items():
st.write(f"- **{etype}**: {count}")
st.subheader("Loop-Checker")
q = st.text_input("Query")
r = st.text_input("Response")
if st.button("Check Loop") and q and r:
detector = LoopDetector()
result = detector.check(q, r)
st.json(result)
if result["loop_detected"]:
st.error(result["suggestion"])
elif page == "Neural Scorer":
st.subheader("Neural Scorer Training")
scorer = _scorer()
store = _store()
engrams = store.get_all()
engrams = store.get_all(limit=10000)
labeled = [e for e in engrams if e.correctness.confirmed or e.correctness.rejections > 0]
st.write(f"Labelled Engramme: {len(labeled)}")
st.write(f"Labelled Engramme: **{len(labeled)}**")
if st.button("Train Neural Scorer"):
if len(labeled) < 2:
st.error("Mindestens 2 labelierte Engramme nötig (confirm + reject).")
else:
result = scorer.train(labeled, epochs=30)
with st.spinner("Training läuft..."):
result = scorer.train(labeled, epochs=30)
st.json(result)
st.success("Training abgeschlossen!")
if st.button("Predict All"):
for eg in engrams[:10]:
for eg in engrams[:20]:
pred = scorer.predict(eg)
st.write(f"{eg.content[:60]}... → {pred:.3f}")
st.write(f"{eg.content[:50]}... → **{pred:.3f}**")

View File

@@ -31,19 +31,21 @@ class ChromaStore:
)
def _build_metadata(self, engram: Engram) -> Dict[str, Any]:
"""Serialisierte Metadaten für ChromaDB (nur primitives)."""
meta = engram.metadata.copy()
# ChromaDB akzeptiert nur Listen/Strings/Numbers/Bools
tags = meta.pop("tags", [])
if isinstance(tags, list):
meta["tags"] = ",".join(str(t) for t in tags)
meta.setdefault("source", "agent")
meta.setdefault("confidence", 0.5)
meta.setdefault("correctness", "unconfirmed")
# Hierarchy als JSON-String
if "hierarchy" in meta:
meta["hierarchy"] = json.dumps(meta["hierarchy"])
return meta
"""Serialisierte Metadaten für ChromaDB (nur primitiv/scalar/Str)."""
m = engram.metadata
safe: Dict[str, Any] = {}
# Nur explizit erlaubte Felder übernehmen
safe["source"] = str(m.get("source", "agent"))
safe["confidence"] = float(m.get("confidence", 0.5))
safe["grounding"] = int(m.get("grounding", 1))
tags = m.get("tags", [])
safe["tags"] = ",".join(str(t) for t in tags) if isinstance(tags, list) else str(tags)
safe["created"] = str(m.get("created", ""))
safe["modified"] = str(m.get("modified", ""))
safe["access_count"] = int(m.get("access_count", 0))
safe["correctness"] = "confirmed" if engram.correctness.confirmed else "unconfirmed"
safe["content"] = str(engram.content)[:500] # Chroma akzeptiert kurze Strings besser
return safe
def add(self, engram: Engram, embedding: Optional[List[float]] = None) -> None:
"""Engramm mit Embedding zur Vektor-DB hinzufügen."""

165
src/proactive_search.py Normal file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
proactive_search.py - Proaktive Websuche für Second Brain.
Sucht relevante Themen, speichert Ergebnisse als Engramme.
Stoppt wenn neue Aufgaben erkannt werden.
"""
import sys
import json
from pathlib import Path
from datetime import datetime, timezone, timedelta
from typing import List, Dict, Any, Optional
sys.path.insert(0, str(Path(__file__).resolve().parent))
from src.store import EngramStore
from src.engram import Engram, Grounding
from src.retriever import Retriever
from src.embedder import encode
from src.chroma_store import ChromaStore
DB_PATH = Path(__file__).resolve().parent.parent / "data" / "brain.sqlite"
CHROMA_PATH = Path(__file__).resolve().parent.parent / "data" / "chroma"
# Themen die relevant sind für den Benutzer
INTEREST_TOPICS = [
"OpenClaw AI Agent",
"Künstliche Intelligenz Trends 2025",
"Second Brain Memory System",
"Automation DIY Projects",
"Smart Home IoT",
"Raspberry Pi Projects",
"Deutschland Tech News",
"AI Agent Frameworks",
"Workflow Automation",
]
def get_store():
return EngramStore(str(DB_PATH))
def load_state() -> Dict[str, Any]:
"""Lädt den Such-Zustand."""
state_path = Path(__file__).resolve().parent.parent / "data" / "search_state.json"
if state_path.exists():
with open(state_path, "r", encoding="utf-8") as f:
return json.load(f)
return {
"last_search": None,
"searched_topics": [],
"new_tasks_detected": False,
"paused_until": None,
}
def save_state(state: Dict[str, Any]):
state_path = Path(__file__).resolve().parent.parent / "data" / "search_state.json"
with open(state_path, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False)
def check_for_new_tasks(store: EngramStore) -> bool:
"""Prüft ob in letzten 2h neue Aufgaben-Artige Engramme erstellt wurden."""
now = datetime.now(timezone.utc)
recent = now - timedelta(hours=2)
egs = store.get_all(limit=1000)
for eg in egs:
created_str = eg.metadata.get("created", "")
if not created_str:
continue
try:
eg_time = datetime.fromisoformat(created_str)
if eg_time.tzinfo is None:
eg_time = eg_time.replace(tzinfo=timezone.utc)
if eg_time > recent:
tags = eg.metadata.get("tags", [])
if "task" in tags or "aufgabe" in tags or "todo" in tags:
return True
except Exception:
pass
return False
def try_web_search(topic: str) -> Optional[List[Dict[str, str]]]:
"""Web-Suche via OpenClaw."""
try:
import subprocess
result = subprocess.run(
["python3", "-c", f"""
import sys
sys.path.insert(0, '/root/.openclaw/workspace/second-brain/src')
from src.retriever import Retriever
from src.store import EngramStore
store = EngramStore('data/brain.sqlite')
ret = Retriever(store)
results = ret.retrieve('{topic}')
print('FOUND ' + str(len(results)))
"""],
capture_output=True,
text=True,
timeout=30,
cwd="/root/.openclaw/workspace/second-brain",
)
# Actually do web search
print(f"[search] Would search: {topic}")
return None # Placeholder: real search would be here
except Exception as e:
print(f"[search] Error: {e}")
return None
def run_proactive_search():
"""Haupt-Funktion für proaktive Suche."""
store = get_store()
state = load_state()
now = datetime.now(timezone.utc)
# Check: Neue Aufgaben?
if check_for_new_tasks(store):
state["new_tasks_detected"] = True
state["paused_until"] = (now + timedelta(hours=4)).isoformat()
save_state(state)
print("🛑 Neue Aufgaben erkannt. Suche pausiert für 4h.")
return
# Check: Pausiert?
if state.get("paused_until"):
paused = datetime.fromisoformat(state["paused_until"])
if now < paused:
print(f"⏸️ Suche pausiert bis {state['paused_until']}")
return
else:
state["paused_until"] = None
state["new_tasks_detected"] = False
# Thema auswählen (Round-Robin)
searched = set(state.get("searched_topics", []))
remaining = [t for t in INTEREST_TOPICS if t not in searched]
if not remaining:
remaining = INTEREST_TOPICS
searched = set()
topic = remaining[0]
print(f"🔍 Suche: {topic}")
# Als Engramm speichern (als "Suchanfrage", nicht als Faktum)
eg = Engram.create(
content=f"Proaktive Web-Suche: {topic}\nStatus: Geplant",
source="agent",
tags=["proactive", "search", "planned"],
confidence=0.3,
grounding=Grounding.ASSUMPTION,
)
store.save(eg)
state["last_search"] = now.isoformat()
state["searched_topics"] = list(searched | {topic})
save_state(state)
print(f"✅ Such-Engramm gespeichert: {eg.id}")
if __name__ == "__main__":
run_proactive_search()