315 lines
10 KiB
Python
315 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Second Brain CLI - direkte Nutzung ohne externe Abhängigkeiten.
|
|
|
|
Usage:
|
|
python -m src.cli add "Faktum" --tag wichtig --source user
|
|
python -m src.cli search "Faktum"
|
|
python -m src.cli show <id>
|
|
python -m src.cli confirm <id>
|
|
python -m src.cli reject <id>
|
|
python -m src.cli list
|
|
python -m src.cli stats
|
|
python -m src.cli export backup.jsonl
|
|
python -m src.cli graph
|
|
python -m src.cli heal
|
|
python -m src.cli neural-train
|
|
python -m src.cli loop-check "query" "response"
|
|
python -m src.cli dashboard
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from .store import EngramStore
|
|
from .engram import Engram, Grounding
|
|
from .retriever import Retriever
|
|
from .chroma_store import ChromaStore
|
|
from .graph_view import generate_graph_html
|
|
from .neural_scorer import NeuralScorer
|
|
from .loop_detector import LoopDetector
|
|
from .error_healer import ErrorHealer
|
|
|
|
DB_PATH = Path(__file__).parent.parent / "data" / "brain.sqlite"
|
|
CHROMA_PATH = Path(__file__).parent.parent / "data" / "chroma"
|
|
|
|
|
|
def get_store():
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
return EngramStore(str(DB_PATH))
|
|
|
|
|
|
def get_chroma():
|
|
return ChromaStore(str(CHROMA_PATH))
|
|
|
|
|
|
def cmd_add(args):
|
|
store = get_store()
|
|
eg = Engram.create(
|
|
content=" ".join(args.content),
|
|
source=args.source,
|
|
tags=args.tag,
|
|
grounding=Grounding[args.grounding] if args.grounding else Grounding.ASSUMPTION,
|
|
)
|
|
# Grounding-Regel prüfen (Issue #8)
|
|
validation = eg.validate_grounding()
|
|
if not validation["valid"] and args.auto_fix:
|
|
eg.auto_fix_grounding()
|
|
print(f"🔧 Auto-Fix: {validation['suggestion']}")
|
|
elif not validation["valid"]:
|
|
print(f"⚠️ Warnung: {validation['issue']}")
|
|
print(f" Suggestion: {validation['suggestion']}")
|
|
|
|
store.save(eg)
|
|
print(f"Created: {eg.id}\n Content: {eg.content[:100]}\n Confidence: {eg.compute_confidence():.2f}")
|
|
|
|
|
|
def cmd_search(args):
|
|
store = get_store()
|
|
chroma = get_chroma()
|
|
ret = Retriever(store, chroma)
|
|
|
|
mode = args.mode
|
|
if mode == "hybrid":
|
|
results = ret.hybrid_retrieve(
|
|
" ".join(args.query),
|
|
limit=args.limit,
|
|
min_confidence=args.min_confidence,
|
|
)
|
|
elif mode == "semantic":
|
|
results = ret.semantic_retrieve(
|
|
" ".join(args.query),
|
|
limit=args.limit,
|
|
min_confidence=args.min_confidence,
|
|
)
|
|
else:
|
|
results = ret.retrieve(
|
|
" ".join(args.query),
|
|
limit=args.limit,
|
|
min_confidence=args.min_confidence,
|
|
tag_filter=args.tag,
|
|
)
|
|
|
|
print(f"\n=== {len(results)} Results ({mode}) ===")
|
|
for r in results:
|
|
eg = r["engram"]
|
|
conf = eg.compute_confidence()
|
|
marker = "✅" if conf > 0.7 else "⚠️" if conf > 0.4 else "❌"
|
|
print(f"\n{marker} [{str(eg.id)[:8]}] Score: {conf:.2f} ({r['match_type']})")
|
|
print(f" {eg.content[:120]}{'...' if len(eg.content) > 120 else ''}")
|
|
print(f" Tags: {', '.join(eg.metadata.get('tags', []))} | Source: {eg.metadata.get('source')}")
|
|
print(f" Access: {eg.metadata.get('access_count', 0)} | Reviews: +{eg.correctness.confirmations}/-{eg.correctness.rejections}")
|
|
|
|
|
|
def cmd_show(args):
|
|
store = get_store()
|
|
eg = store.get(args.id)
|
|
if not eg:
|
|
print(f"Not found: {args.id}")
|
|
return
|
|
print(json.dumps(eg.to_dict(), indent=2, ensure_ascii=False, default=str))
|
|
|
|
|
|
def cmd_confirm(args):
|
|
store = get_store()
|
|
eg = store.get(args.id)
|
|
if not eg:
|
|
print(f"Not found: {args.id}")
|
|
return
|
|
eg.correctness.confirm(by="user", note=args.note or "Confirmed via CLI")
|
|
store.save(eg)
|
|
print(f"✅ Confirmed [{str(eg.id)[:8]}] -> Confidence: {eg.compute_confidence():.2f}")
|
|
|
|
|
|
def cmd_reject(args):
|
|
store = get_store()
|
|
eg = store.get(args.id)
|
|
if not eg:
|
|
print(f"Not found: {args.id}")
|
|
return
|
|
eg.correctness.reject(by="user", note=args.note or "Rejected via CLI")
|
|
store.save(eg)
|
|
print(f"❌ Rejected [{str(eg.id)[:8]}] -> Confidence: {eg.compute_confidence():.2f}")
|
|
|
|
|
|
def cmd_list(args):
|
|
store = get_store()
|
|
egs = store.get_all(limit=args.limit)
|
|
print(f"\n=== {len(egs)} Engrams ===")
|
|
for eg in egs:
|
|
conf = eg.compute_confidence()
|
|
marker = "✅" if conf > 0.7 else "⚠️" if conf > 0.4 else "❌"
|
|
print(f"{marker} [{str(eg.id)[:8]}] ({conf:.2f}) {eg.content[:60]}{'...' if len(eg.content) > 60 else ''}")
|
|
|
|
|
|
def cmd_stats(args):
|
|
store = get_store()
|
|
ret = Retriever(store)
|
|
try:
|
|
s = ret.stats()
|
|
except AttributeError:
|
|
egs = store.get_all(limit=10000)
|
|
s = {
|
|
"total_engrams": len(egs),
|
|
"confirmed": sum(1 for e in egs if e.correctness.confirmed),
|
|
"unconfirmed": sum(1 for e in egs if not e.correctness.confirmed),
|
|
"sources": {src: sum(1 for e in egs if e.metadata.get("source") == src) for src in {e.metadata.get("source") for e in egs}},
|
|
"db_size_bytes": os.path.getsize(str(DB_PATH)) if os.path.exists(str(DB_PATH)) else 0,
|
|
}
|
|
print("\n=== Second Brain Stats ===")
|
|
print(f" Total Engrams: {s['total_engrams']}")
|
|
print(f" Confirmed: {s['confirmed']}")
|
|
print(f" Unconfirmed: {s['unconfirmed']}")
|
|
print(f" Sources:")
|
|
for src, count in s.get("sources", {}).items():
|
|
print(f" {src}: {count}")
|
|
print(f" DB Size: {s['db_size_bytes'] / 1024:.1f} KB")
|
|
|
|
|
|
def cmd_export(args):
|
|
store = get_store()
|
|
count = store.export_jsonl(args.path)
|
|
print(f"Exported {count} engrams to {args.path}")
|
|
|
|
|
|
def cmd_graph(args):
|
|
store = get_store()
|
|
path = args.output or str(DB_PATH.parent / "graph_view.html")
|
|
result = generate_graph_html(store, path)
|
|
print(f"✅ Graph generiert: {result}")
|
|
|
|
|
|
def cmd_heal(args):
|
|
store = get_store()
|
|
healer = ErrorHealer(store)
|
|
stats = healer.get_error_stats()
|
|
print("\n=== Error Heal Stats ===")
|
|
print(f" Total Errors: {stats['total_errors']}")
|
|
print(f" Repeated Errors: {stats['repeated_errors']}")
|
|
print(f" Error Types:")
|
|
for etype, count in stats.get("error_types", {}).items():
|
|
print(f" {etype}: {count}")
|
|
|
|
if args.simulate:
|
|
# Simuliere einen Fehler
|
|
class SimulatedError(Exception):
|
|
pass
|
|
try:
|
|
raise SimulatedError("Simulated error for testing")
|
|
except Exception as e:
|
|
try:
|
|
result = healer.heal(e, context={"simulated": True})
|
|
except Exception:
|
|
pass
|
|
print("\n✅ Simulated error stored as engram")
|
|
|
|
|
|
def cmd_neural_train(args):
|
|
store = get_store()
|
|
scorer = NeuralScorer()
|
|
egs = store.get_all(limit=10000)
|
|
labeled = [e for e in egs if e.correctness.confirmed or e.correctness.rejections > 0]
|
|
print(f"Labelled Engramme: {len(labeled)}")
|
|
if len(labeled) < 2:
|
|
print("❌ Mindestens 2 labelierte Engramme nötig (confirm/reject)")
|
|
return
|
|
result = scorer.train(labeled, epochs=args.epochs)
|
|
print(f"✅ Training abgeschlossen")
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
def cmd_loop_check(args):
|
|
detector = LoopDetector()
|
|
result = detector.check(args.query, args.response)
|
|
print(json.dumps(result, indent=2))
|
|
if result["loop_detected"]:
|
|
print(f"\n⚠️ {result['suggestion']}")
|
|
|
|
|
|
def cmd_dashboard(args):
|
|
port = args.port
|
|
print(f"🚀 Starte Streamlit Dashboard auf Port {port}...")
|
|
script = Path(__file__).resolve().parent / "app_dashboard.py"
|
|
subprocess.run([sys.executable, "-m", "streamlit", "run", str(script), "--server.port", str(port)])
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Second Brain CLI")
|
|
sub = parser.add_subparsers(dest="cmd")
|
|
|
|
p_add = sub.add_parser("add", help="Add a new engram")
|
|
p_add.add_argument("content", nargs="+")
|
|
p_add.add_argument("--tag", action="append", default=[])
|
|
p_add.add_argument("--source", default="user")
|
|
p_add.add_argument("--grounding", choices=[g.name for g in Grounding])
|
|
p_add.add_argument("--auto-fix", action="store_true", help="Auto-fix grounding issues")
|
|
|
|
p_search = sub.add_parser("search", help="Search engrams")
|
|
p_search.add_argument("query", nargs="+")
|
|
p_search.add_argument("--limit", type=int, default=5)
|
|
p_search.add_argument("--min-confidence", type=float, default=0.0)
|
|
p_search.add_argument("--tag", default=None)
|
|
p_search.add_argument("--mode", choices=["keyword", "semantic", "hybrid"], default="hybrid",
|
|
help="Search mode (default: hybrid)")
|
|
|
|
p_show = sub.add_parser("show", help="Show engram details")
|
|
p_show.add_argument("id")
|
|
|
|
p_confirm = sub.add_parser("confirm", help="Confirm an engram")
|
|
p_confirm.add_argument("id")
|
|
p_confirm.add_argument("--note", default="")
|
|
|
|
p_reject = sub.add_parser("reject", help="Reject an engram")
|
|
p_reject.add_argument("id")
|
|
p_reject.add_argument("--note", default="")
|
|
|
|
p_list = sub.add_parser("list", help="List recent engrams")
|
|
p_list.add_argument("--limit", type=int, default=20)
|
|
|
|
p_stats = sub.add_parser("stats", help="Show statistics")
|
|
|
|
p_export = sub.add_parser("export", help="Export to JSONL")
|
|
p_export.add_argument("path")
|
|
|
|
p_graph = sub.add_parser("graph", help="Generate graph visualization")
|
|
p_graph.add_argument("--output", default=None, help="Output HTML path")
|
|
|
|
p_heal = sub.add_parser("heal", help="Show error healing stats")
|
|
p_heal.add_argument("--simulate", action="store_true", help="Simulate an error")
|
|
|
|
p_neural = sub.add_parser("neural-train", help="Train neural scorer")
|
|
p_neural.add_argument("--epochs", type=int, default=30)
|
|
|
|
p_loop = sub.add_parser("loop-check", help="Check for conversation loops")
|
|
p_loop.add_argument("query")
|
|
p_loop.add_argument("response")
|
|
|
|
p_dash = sub.add_parser("dashboard", help="Launch Streamlit dashboard")
|
|
p_dash.add_argument("--port", type=int, default=8501)
|
|
|
|
args = parser.parse_args()
|
|
if not args.cmd:
|
|
parser.print_help()
|
|
return
|
|
|
|
handlers = {
|
|
"add": cmd_add, "search": cmd_search, "show": cmd_show,
|
|
"confirm": cmd_confirm, "reject": cmd_reject, "list": cmd_list,
|
|
"stats": cmd_stats, "export": cmd_export, "graph": cmd_graph,
|
|
"heal": cmd_heal, "neural-train": cmd_neural_train,
|
|
"loop-check": cmd_loop_check, "dashboard": cmd_dashboard,
|
|
}
|
|
handler = handlers.get(args.cmd)
|
|
if handler:
|
|
handler(args)
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|