Files
timmy-config/wizards/allegro-primus/knowledge/cli.py

537 lines
18 KiB
Python
Raw Normal View History

2026-03-31 20:02:01 +00:00
#!/usr/bin/env python3
"""
AP Knowledge Base - Command Line Interface
Commands:
add-memory Add a new memory
search-memories Search for memories
show-memory Display a specific memory
edit-memory Edit an existing memory
delete-memory Delete a memory
archive-memory Archive a memory
list-memories List all memories
consolidate Consolidate similar memories
export Export memories
import Import memories
sync Sync with father
stats Show statistics
visualize Visualize knowledge graph
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime
from typing import Optional, List
# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from memory_types import MemoryEntry, MemoryQuery, MemoryType, MemoryScope
from memdir import MemoryDirectory
from knowledge_graph import KnowledgeGraph
from sync import FatherSync
def get_memdir() -> MemoryDirectory:
"""Get or create the memory directory."""
base_dir = Path.home() / ".ap" / "knowledge"
base_dir.mkdir(parents=True, exist_ok=True)
return MemoryDirectory(base_dir)
def cmd_add_memory(args):
"""Add a new memory."""
memdir = get_memdir()
# Interactive mode if no content provided
if not args.content:
print("Enter memory content (Ctrl+D when done):")
lines = []
try:
while True:
lines.append(input())
except EOFError:
pass
args.content = '\n'.join(lines)
entry = MemoryEntry(
name=args.name,
description=args.description or args.name,
type=MemoryType(args.type),
scope=MemoryScope(args.scope),
content=args.content,
tags=args.tags or [],
source=args.source or "cli",
confidence=args.confidence or 1.0
)
path = memdir.save(entry)
print(f"✓ Memory saved: {path}")
print(f" ID: {entry.id}")
print(f" Type: {entry.type.value}")
print(f" Tags: {', '.join(entry.tags) or '(none)'}")
# Check for similar memories
similar = memdir.find_similar(entry, threshold=0.7)
if similar:
print(f"\n⚠ Note: {len(similar)} similar memories exist:")
for mem in similar[:3]:
print(f" - {mem.name} ({mem.id})")
def cmd_search_memories(args):
"""Search for memories."""
memdir = get_memdir()
query = MemoryQuery(
text=args.query or "",
types=[MemoryType(t) for t in args.type] if args.type else None,
scopes=[MemoryType(s) for s in args.scope] if args.scope else None,
tags=args.tags,
tags_all=args.tags_all,
source=args.source,
min_confidence=args.min_confidence or 0.0,
max_age_days=args.max_age,
limit=args.limit or 10,
sort_by=args.sort or "relevance"
)
results = memdir.search(query)
if not results:
print("No memories found matching your query.")
return
print(f"Found {len(results)} memories:\n")
for entry, score in results:
freshness = "🟢" if entry.freshness_score() > 0.8 else "🟡" if entry.freshness_score() > 0.5 else "🔴"
print(f"{freshness} [{entry.type.value:12}] {entry.name}")
print(f" ID: {entry.id} | Score: {score:.2f} | Age: {entry.age_days()}d")
print(f" {entry.description}")
if entry.tags:
print(f" Tags: {', '.join(entry.tags)}")
print()
def cmd_show_memory(args):
"""Display a specific memory."""
memdir = get_memdir()
entry = memdir.get(args.id)
if not entry:
print(f"Memory not found: {args.id}")
sys.exit(1)
print(f"# {entry.name}")
print(f"ID: {entry.id}")
print(f"Type: {entry.type.value}")
print(f"Scope: {entry.scope.value}")
print(f"Created: {entry.created_at}")
print(f"Modified: {entry.modified_at}")
print(f"Accessed: {entry.access_count} times")
print(f"Source: {entry.source}")
print(f"Confidence: {entry.confidence}")
print(f"Tags: {', '.join(entry.tags) or '(none)'}")
print()
print(entry.content)
# Show related memories
graph = KnowledgeGraph(memdir.base_dir)
related = graph.get_relationships(entry.id, direction="both")
if related:
print("\n## Related Memories")
for rel in related[:5]:
other_id = rel.target_id if rel.source_id == entry.id else rel.source_id
other = memdir.get(other_id)
if other:
print(f" [{rel.type.value}] {other.name} ({other_id})")
def cmd_edit_memory(args):
"""Edit an existing memory."""
memdir = get_memdir()
entry = memdir.get(args.id)
if not entry:
print(f"Memory not found: {args.id}")
sys.exit(1)
if args.name:
entry.name = args.name
if args.description:
entry.description = args.description
if args.content:
entry.content = args.content
if args.type:
entry.type = MemoryType(args.type)
if args.tags:
entry.tags = args.tags
memdir.save(entry)
print(f"✓ Memory updated: {entry.id}")
def cmd_delete_memory(args):
"""Delete a memory."""
memdir = get_memdir()
if not args.force:
confirm = input(f"Delete memory {args.id}? [y/N] ")
if confirm.lower() != 'y':
print("Cancelled.")
return
if memdir.delete(args.id):
print(f"✓ Memory deleted: {args.id}")
else:
print(f"Memory not found: {args.id}")
sys.exit(1)
def cmd_archive_memory(args):
"""Archive a memory."""
memdir = get_memdir()
if memdir.archive(args.id):
print(f"✓ Memory archived: {args.id}")
else:
print(f"Memory not found: {args.id}")
sys.exit(1)
def cmd_list_memories(args):
"""List all memories."""
memdir = get_memdir()
entries = list(memdir._memories.values())
if args.type:
entries = [e for e in entries if e.type.value in args.type]
if args.scope:
entries = [e for e in entries if e.scope.value in args.scope]
if args.tag:
entries = [e for e in entries if any(t in e.tags for t in args.tag)]
# Sort
if args.sort == "date":
entries.sort(key=lambda e: e.created_at, reverse=True)
elif args.sort == "access":
entries.sort(key=lambda e: e.access_count, reverse=True)
elif args.sort == "name":
entries.sort(key=lambda e: e.name.lower())
else: # age
entries.sort(key=lambda e: e.age_days())
if args.limit:
entries = entries[:args.limit]
print(f"Total: {len(entries)} memories\n")
for entry in entries:
age = entry.age_days()
age_str = "today" if age == 0 else f"{age}d ago"
print(f"[{entry.type.value:12}] {entry.name[:40]:40} ({age_str})")
print(f" {entry.description[:60]}{'...' if len(entry.description) > 60 else ''}")
def cmd_consolidate(args):
"""Consolidate similar memories."""
memdir = get_memdir()
print("Scanning for similar memories...")
merged = memdir.consolidate(dry_run=args.dry_run)
if not merged:
print("No similar memories found for consolidation.")
return
print(f"Found {len(merged)} pairs to consolidate:")
for old, new in merged:
print(f"'{old.name}' → merged into '{new.name}'")
if args.dry_run:
print("\n(Dry run - no changes made)")
else:
print(f"\n✓ Consolidated {len(merged)} memory pairs")
def cmd_export(args):
"""Export memories."""
memdir = get_memdir()
data = memdir.export_all()
if args.output:
output_path = Path(args.output)
else:
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
output_path = Path(f"ap_knowledge_export_{timestamp}.json")
output_path.write_text(json.dumps(data, indent=2), encoding='utf-8')
print(f"✓ Exported {data['memories'].__len__()} memories to {output_path}")
def cmd_import(args):
"""Import memories."""
memdir = get_memdir()
data = json.loads(Path(args.file).read_text())
imported, skipped = memdir.import_data(data, overwrite=args.overwrite)
print(f"✓ Imported {imported} memories")
if skipped:
print(f" Skipped {skipped} (already exist, use --overwrite to replace)")
def cmd_sync(args):
"""Sync with father."""
memdir = get_memdir()
sync = FatherSync(memdir)
if args.status:
status = sync.sync_status()
print("Sync Status:")
print(f" Last export: {status['last_export'] or 'Never'}")
print(f" Last import: {status['last_import'] or 'Never'}")
print(f" Pending exports: {status['pending_exports']}")
print(f" Pending imports: {status['pending_imports']}")
print(f" Unresolved conflicts: {status['unresolved_conflicts']}")
return
if args.export:
path = sync.export_for_father(
scope=MemoryScope(args.scope) if args.scope else None,
since_last_export=not args.full
)
print(f"✓ Exported to: {path}")
if args.import_:
imported, skipped, conflicts = sync.import_from_father(auto_resolve=args.auto_resolve)
print(f"✓ Imported: {imported}, Skipped: {skipped}")
if conflicts:
print(f"{len(conflicts)} conflicts need resolution")
if args.package:
path = sync.create_father_package(notes=args.notes or "")
print(f"✓ Package created: {path}")
def cmd_stats(args):
"""Show statistics."""
memdir = get_memdir()
stats = memdir.get_stats()
print("# AP Knowledge Base Statistics")
print()
print(f"Total memories: {stats['total_memories']}")
print(f"Total relationships: {stats['total_relationships']}")
print()
print("## By Type")
for mem_type, count in stats['by_type'].items():
print(f" {mem_type:12}: {count}")
print()
print("## By Scope")
for scope, count in stats['by_scope'].items():
print(f" {scope:12}: {count}")
print()
print("## By Source")
for source, count in stats['by_source'].items():
print(f" {source:12}: {count}")
print()
print(f"Average age: {stats['avg_age_days']:.1f} days")
print(f"Average confidence: {stats['avg_confidence']:.2f}")
# Knowledge graph stats
graph = KnowledgeGraph(memdir.base_dir)
graph_stats = graph.get_stats()
print()
print("## Knowledge Graph")
print(f" Nodes: {graph_stats['total_nodes']}")
print(f" Edges: {graph_stats['total_edges']}")
print(f" Avg clustering: {graph_stats['avg_clustering']:.3f}")
def cmd_visualize(args):
"""Visualize knowledge graph."""
memdir = get_memdir()
graph = KnowledgeGraph(memdir.base_dir)
try:
path = graph.visualize()
print(f"✓ Visualization saved: {path}")
except ImportError as e:
print(f"Error: {e}")
print("Install matplotlib: pip install matplotlib")
sys.exit(1)
except Exception as e:
print(f"Error creating visualization: {e}")
sys.exit(1)
def cmd_relate(args):
"""Create a relationship between memories."""
memdir = get_memdir()
graph = KnowledgeGraph(memdir.base_dir)
from memory_types import RelationshipType
graph.add_relationship(
source_id=args.source,
target_id=args.target,
rel_type=RelationshipType(args.type),
description=args.description or "",
strength=args.strength or 1.0
)
print(f"✓ Created relationship: {args.source} --[{args.type}]--> {args.target}")
def main():
parser = argparse.ArgumentParser(
prog='ap-kb',
description='AP Knowledge Base - Manage Allegro-Primus memories'
)
subparsers = parser.add_subparsers(dest='command', help='Commands')
# add-memory
add_p = subparsers.add_parser('add-memory', help='Add a new memory')
add_p.add_argument('--name', '-n', required=True, help='Memory name')
add_p.add_argument('--description', '-d', help='One-line description')
add_p.add_argument('--type', '-t', choices=['fact', 'procedure', 'observation', 'lesson'],
default='fact', help='Memory type')
add_p.add_argument('--scope', '-s', choices=['private', 'team'],
default='private', help='Memory scope')
add_p.add_argument('--content', '-c', help='Memory content (or read from stdin)')
add_p.add_argument('--tags', metavar='TAG', nargs='+', help='Tags')
add_p.add_argument('--source', help='Source of memory')
add_p.add_argument('--confidence', type=float, help='Confidence score (0-1)')
add_p.set_defaults(func=cmd_add_memory)
# search-memories
search_p = subparsers.add_parser('search', help='Search memories')
search_p.add_argument('query', nargs='?', help='Search query')
search_p.add_argument('--type', choices=['fact', 'procedure', 'observation', 'lesson'],
nargs='+', help='Filter by type')
search_p.add_argument('--scope', choices=['private', 'team'],
nargs='+', help='Filter by scope')
search_p.add_argument('--tags', metavar='TAG', nargs='+', help='Filter by tags (any)')
search_p.add_argument('--tags-all', metavar='TAG', nargs='+', help='Filter by tags (all)')
search_p.add_argument('--source', help='Filter by source')
search_p.add_argument('--min-confidence', type=float, help='Minimum confidence')
search_p.add_argument('--max-age', type=int, help='Maximum age in days')
search_p.add_argument('--limit', '-l', type=int, default=10, help='Maximum results')
search_p.add_argument('--sort', choices=['relevance', 'date', 'freshness', 'access'],
default='relevance', help='Sort order')
search_p.set_defaults(func=cmd_search_memories)
# show-memory
show_p = subparsers.add_parser('show', help='Show a memory')
show_p.add_argument('id', help='Memory ID')
show_p.set_defaults(func=cmd_show_memory)
# edit-memory
edit_p = subparsers.add_parser('edit', help='Edit a memory')
edit_p.add_argument('id', help='Memory ID')
edit_p.add_argument('--name', help='New name')
edit_p.add_argument('--description', help='New description')
edit_p.add_argument('--content', help='New content')
edit_p.add_argument('--type', choices=['fact', 'procedure', 'observation', 'lesson'],
help='New type')
edit_p.add_argument('--tags', nargs='+', help='New tags')
edit_p.set_defaults(func=cmd_edit_memory)
# delete-memory
del_p = subparsers.add_parser('delete', help='Delete a memory')
del_p.add_argument('id', help='Memory ID')
del_p.add_argument('--force', '-f', action='store_true', help='Skip confirmation')
del_p.set_defaults(func=cmd_delete_memory)
# archive-memory
archive_p = subparsers.add_parser('archive', help='Archive a memory')
archive_p.add_argument('id', help='Memory ID')
archive_p.set_defaults(func=cmd_archive_memory)
# list-memories
list_p = subparsers.add_parser('list', help='List memories')
list_p.add_argument('--type', choices=['fact', 'procedure', 'observation', 'lesson'],
nargs='+', help='Filter by type')
list_p.add_argument('--scope', choices=['private', 'team'],
nargs='+', help='Filter by scope')
list_p.add_argument('--tag', nargs='+', help='Filter by tag')
list_p.add_argument('--sort', choices=['date', 'age', 'access', 'name'],
default='date', help='Sort order')
list_p.add_argument('--limit', '-l', type=int, help='Limit results')
list_p.set_defaults(func=cmd_list_memories)
# consolidate
cons_p = subparsers.add_parser('consolidate', help='Consolidate similar memories')
cons_p.add_argument('--dry-run', '-n', action='store_true',
help='Show what would be merged without doing it')
cons_p.set_defaults(func=cmd_consolidate)
# export
export_p = subparsers.add_parser('export', help='Export memories')
export_p.add_argument('--output', '-o', help='Output file path')
export_p.set_defaults(func=cmd_export)
# import
import_p = subparsers.add_parser('import', help='Import memories')
import_p.add_argument('file', help='Import file path')
import_p.add_argument('--overwrite', action='store_true',
help='Overwrite existing memories')
import_p.set_defaults(func=cmd_import)
# sync
sync_p = subparsers.add_parser('sync', help='Sync with father')
sync_p.add_argument('--status', action='store_true', help='Show sync status')
sync_p.add_argument('--export', action='store_true', help='Export for father')
sync_p.add_argument('--import', dest='import_', action='store_true',
help='Import from father')
sync_p.add_argument('--package', action='store_true',
help='Create complete package for father')
sync_p.add_argument('--scope', choices=['private', 'team'],
help='Scope for export')
sync_p.add_argument('--full', action='store_true',
help='Full export (not just since last)')
sync_p.add_argument('--auto-resolve', action='store_true',
help='Auto-resolve import conflicts')
sync_p.add_argument('--notes', help='Notes for father package')
sync_p.set_defaults(func=cmd_sync)
# stats
stats_p = subparsers.add_parser('stats', help='Show statistics')
stats_p.set_defaults(func=cmd_stats)
# visualize
viz_p = subparsers.add_parser('visualize', help='Visualize knowledge graph')
viz_p.set_defaults(func=cmd_visualize)
# relate
relate_p = subparsers.add_parser('relate', help='Create relationship between memories')
relate_p.add_argument('source', help='Source memory ID')
relate_p.add_argument('target', help='Target memory ID')
relate_p.add_argument('--type', required=True,
choices=['related', 'depends_on', 'supersedes', 'derived_from', 'conflicts_with'],
help='Relationship type')
relate_p.add_argument('--description', help='Relationship description')
relate_p.add_argument('--strength', type=float, default=1.0, help='Relationship strength')
relate_p.set_defaults(func=cmd_relate)
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(1)
args.func(args)
if __name__ == '__main__':
main()