Odysseus v1.0
This commit is contained in:
153
scripts/odysseus-memory
Executable file
153
scripts/odysseus-memory
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""odysseus-memory — shell wrapper for the AI memory store.
|
||||
|
||||
The chat assistant accumulates durable facts about the user (name,
|
||||
projects, preferences, contacts) into `data/memory.json`. This CLI
|
||||
lets you inspect, search, and prune them from the shell.
|
||||
|
||||
odysseus-memory list # all entries (paged)
|
||||
odysseus-memory list --category preference
|
||||
odysseus-memory search "tokyo" # substring match
|
||||
odysseus-memory show MEMORY_ID
|
||||
odysseus-memory add "User lives in Tokyo" --category fact
|
||||
odysseus-memory delete MEMORY_ID
|
||||
odysseus-memory categories # counts per category
|
||||
|
||||
Reads/writes the same `data/memory.json` the web UI uses via the
|
||||
`MemoryManager` helper. Atomic — the manager handles the temp-file
|
||||
swap so partial writes can't corrupt the file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
|
||||
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
|
||||
quiet_logs()
|
||||
|
||||
import argparse, json, logging, os, sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from services.memory.memory import MemoryManager
|
||||
quiet_logs()
|
||||
except ModuleNotFoundError as e:
|
||||
sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n")
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
_DATA_DIR = str(_REPO_ROOT / "data")
|
||||
_mgr: MemoryManager | None = None
|
||||
|
||||
|
||||
def _manager() -> MemoryManager:
|
||||
global _mgr
|
||||
if _mgr is None:
|
||||
_mgr = MemoryManager(_DATA_DIR)
|
||||
return _mgr
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
entries = _manager().load_all()
|
||||
if args.category:
|
||||
entries = [e for e in entries if (e.get("category") or "fact") == args.category]
|
||||
if args.source:
|
||||
entries = [e for e in entries if (e.get("source") or "user") == args.source]
|
||||
if args.owner:
|
||||
entries = [e for e in entries if (e.get("owner") or "") == args.owner]
|
||||
# Newest first
|
||||
entries = sorted(entries, key=lambda e: e.get("timestamp", 0), reverse=True)
|
||||
emit(entries[: args.limit], args)
|
||||
|
||||
|
||||
def cmd_search(args):
|
||||
q = args.query.lower()
|
||||
entries = _manager().load_all()
|
||||
matches = [e for e in entries if q in (e.get("text") or "").lower()]
|
||||
matches = sorted(matches, key=lambda e: e.get("timestamp", 0), reverse=True)
|
||||
emit(matches[: args.limit], args)
|
||||
|
||||
|
||||
def cmd_show(args):
|
||||
for e in _manager().load_all():
|
||||
if e.get("id") == args.id:
|
||||
emit(e, args)
|
||||
return
|
||||
fail(f"no memory with id {args.id!r}")
|
||||
|
||||
|
||||
def cmd_add(args):
|
||||
entry = _manager().add_entry(
|
||||
args.text,
|
||||
source="cli",
|
||||
category=args.category,
|
||||
owner=args.owner,
|
||||
)
|
||||
# add_entry doesn't save by default — the call in chat does it
|
||||
# after dedup checks. Persist here so a one-shot CLI add sticks.
|
||||
all_entries = _manager().load_all()
|
||||
if not any(e.get("id") == entry.get("id") for e in all_entries):
|
||||
all_entries.append(entry)
|
||||
_manager().save(all_entries)
|
||||
emit(entry, args)
|
||||
|
||||
|
||||
def cmd_delete(args):
|
||||
entries = _manager().load_all()
|
||||
target = next((e for e in entries if e.get("id") == args.id), None)
|
||||
if not target:
|
||||
fail(f"no memory with id {args.id!r}")
|
||||
remaining = [e for e in entries if e.get("id") != args.id]
|
||||
_manager().save(remaining)
|
||||
emit({"ok": True, "deleted": target}, args)
|
||||
|
||||
|
||||
def cmd_categories(args):
|
||||
counts: dict[str, int] = {}
|
||||
for e in _manager().load_all():
|
||||
cat = e.get("category") or "fact"
|
||||
counts[cat] = counts.get(cat, 0) + 1
|
||||
rows = sorted(counts.items(), key=lambda kv: -kv[1])
|
||||
emit([{"category": c, "count": n} for c, n in rows], args)
|
||||
|
||||
|
||||
def _build_parser():
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--pretty", action="store_true")
|
||||
p = argparse.ArgumentParser(prog="odysseus-memory", parents=[common])
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pl = sub.add_parser("list", parents=[common])
|
||||
pl.add_argument("--category")
|
||||
pl.add_argument("--source")
|
||||
pl.add_argument("--owner")
|
||||
pl.add_argument("--limit", type=int, default=50)
|
||||
pl.set_defaults(func=cmd_list)
|
||||
|
||||
ps = sub.add_parser("search", parents=[common])
|
||||
ps.add_argument("query")
|
||||
ps.add_argument("--limit", type=int, default=50)
|
||||
ps.set_defaults(func=cmd_search)
|
||||
|
||||
psh = sub.add_parser("show", parents=[common])
|
||||
psh.add_argument("id")
|
||||
psh.set_defaults(func=cmd_show)
|
||||
|
||||
pa = sub.add_parser("add", parents=[common])
|
||||
pa.add_argument("text")
|
||||
pa.add_argument("--category", default="fact")
|
||||
pa.add_argument("--owner")
|
||||
pa.set_defaults(func=cmd_add)
|
||||
|
||||
pd = sub.add_parser("delete", parents=[common])
|
||||
pd.add_argument("id")
|
||||
pd.set_defaults(func=cmd_delete)
|
||||
|
||||
pc = sub.add_parser("categories", parents=[common])
|
||||
pc.set_defaults(func=cmd_categories)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run(_build_parser()))
|
||||
Reference in New Issue
Block a user