#!/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()