537 lines
18 KiB
Python
Executable File
537 lines
18 KiB
Python
Executable File
#!/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()
|