diff --git a/src/app_dashboard.py b/src/app_dashboard.py index ff00f22..98b7b96 100644 --- a/src/app_dashboard.py +++ b/src/app_dashboard.py @@ -1,6 +1,6 @@ """ 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 @@ -9,102 +9,139 @@ 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" -def _store(): - return EngramStore(_DB_PATH) - - -def _chroma(): - p = Path(_DB_PATH).parent / "chroma" - return ChromaStore(str(p)) +@st.cache_resource +class _LazyDB: + """Lazy-Initialisierung damit st.secrets erst bei Bedarf gelesen wird.""" + _store = None + _chroma = None + + @staticmethod + def store(): + if _LazyDB._store is None: + db = str(_DEFAULT_DB) + try: + db = st.secrets.get("db_path", str(_DEFAULT_DB)) + except Exception: + pass + _LazyDB._store = EngramStore(db) + return _LazyDB._store + + @staticmethod + def chroma(): + if _LazyDB._chroma is None: + p = Path(str(_DEFAULT_DB)).parent / "chroma" + _LazyDB._chroma = ChromaStore(str(p)) + return _LazyDB._chroma +@st.cache_resource def _retriever(): - return Retriever(_store(), _chroma()) + return Retriever(_LazyDB.store(), _LazyDB.chroma()) +@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(_LazyDB.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.0") + +page = st.sidebar.radio("Seite", ["Übersicht", "Engramme", "Suche", "Graph", "Heal-Log", "Neural Scorer"]) if page == "Übersicht": - store = _store() - engrams = store.get_all() + store = _LazyDB.store() + engrams = store.get_all(limit=1000) 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.success("Fixed!") elif page == "Engramme": - store = _store() + store = _LazyDB.store() st.subheader("Alle Engramme") 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") + st.subheader("Hybrid Search (Semantic + Keyword)") query = st.text_input("Query") - mode = st.radio("Modus", ["Hybrid", "Keyword", "Semantic"]) + 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) for r in results: eg = r["engram"] with st.container(): @@ -113,62 +150,68 @@ elif page == "Suche": c1, c2 = st.columns(2) if c1.button("✅ Confirm", key=f"sc_{eg.id}"): eg.correctness.confirm("user") - store = _store() - store.save(eg) - st.success("Confirmed") + _LazyDB.store().save(eg) + c1.success("Confirmed") if c2.button("❌ Reject", key=f"sr_{eg.id}"): eg.correctness.reject("user") - store = _store() - store.save(eg) - st.warning("Rejected") + _LazyDB.store().save(eg) + c2.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"): + path = generate_graph_html(_LazyDB.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() + store = _LazyDB.store() + 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..."): + 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}**") diff --git a/src/chroma_store.py b/src/chroma_store.py index d3dbb65..6518cb8 100644 --- a/src/chroma_store.py +++ b/src/chroma_store.py @@ -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."""