feat(core): Engram, Store, Retriever, CLI - Grundsystem Second Brain

- src/engram.py: Gedaechtniseinheit mit Confidence, Correctness, Links
- src/store.py: SQLite FTS5 persistenter Speicher
- src/retriever.py: Hybrid Suche + Reranking
- src/cli.py: Kommandozeilen-Interface

Issue: #1
This commit is contained in:
2026-05-25 00:53:56 +02:00
commit 5e4f21e680
7 changed files with 891 additions and 0 deletions

172
src/cli.py Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
Second Brain CLI - direkte Nutzung ohne externe Abhängigkeiten.
Usage:
python -m src.cli add "Das ist ein 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
"""
import sys
import json
import argparse
from pathlib import Path
from .store import EngramStore
from .engram import Engram, Grounding
from .retriever import Retriever
DB_PATH = Path(__file__).parent.parent / "data" / "brain.sqlite"
def get_store():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
return EngramStore(str(DB_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,
)
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()
ret = Retriever(store)
results = ret.retrieve(
" ".join(args.query),
limit=args.limit,
min_confidence=args.min_confidence,
tag_filter=args.tag,
)
print(f"\n=== {len(results)} Results ===")
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)
s = ret.stats()
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 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_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_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")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
return
{"add": cmd_add, "search": cmd_search, "show": cmd_show,
"confirm": cmd_confirm, "reject": cmd_reject, "list": cmd_list,
"stats": cmd_stats, "export": cmd_export}[args.cmd](args)
if __name__ == "__main__":
main()