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:
172
src/cli.py
Normal file
172
src/cli.py
Normal 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()
|
||||
Reference in New Issue
Block a user