Refactor cockpit to use DockerTmuxController pattern
Based on claude-code-tools TmuxCLIController, this refactor: - Added DockerTmuxController class for robust tmux session management - Implements send_keys() with configurable delay_enter - Implements capture_pane() for output retrieval - Implements wait_for_prompt() for pattern-based completion detection - Implements wait_for_idle() for content-hash-based idle detection - Implements wait_for_shell_prompt() for shell prompt detection Also includes workflow improvements: - Pre-task git snapshot before agent execution - Post-task commit protocol in agent guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
642
lib/knowledge_graph.py
Normal file
642
lib/knowledge_graph.py
Normal file
@@ -0,0 +1,642 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Luz Knowledge Graph - Centralized documentation storage
|
||||
|
||||
Four domains:
|
||||
- sysadmin: Server admin docs, commands, procedures
|
||||
- users: User management, permissions, workflows
|
||||
- projects: Project-specific docs, features, APIs
|
||||
- research: Research sessions, findings, sources
|
||||
|
||||
All use SQLite with FTS5 for full-text search.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import uuid
|
||||
import time
|
||||
import os
|
||||
import grp
|
||||
import pwd
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Any
|
||||
from datetime import datetime
|
||||
|
||||
# Knowledge graph paths
|
||||
KG_BASE = Path("/etc/luz-knowledge")
|
||||
KG_PATHS = {
|
||||
"sysadmin": KG_BASE / "sysadmin.db",
|
||||
"users": KG_BASE / "users.db",
|
||||
"projects": KG_BASE / "projects.db",
|
||||
"research": KG_BASE / "research.db",
|
||||
}
|
||||
|
||||
# Entity types per domain
|
||||
ENTITY_TYPES = {
|
||||
"sysadmin": ["command", "service", "config", "procedure", "troubleshooting", "architecture"],
|
||||
"users": ["user_type", "permission", "workflow", "guide", "policy"],
|
||||
"projects": ["project", "feature", "api", "component", "changelog", "config"],
|
||||
"research": ["session", "finding", "source", "synthesis", "query"],
|
||||
}
|
||||
|
||||
# Relation types
|
||||
RELATION_TYPES = [
|
||||
"relates_to", # General relation
|
||||
"depends_on", # Dependency
|
||||
"documents", # Documentation link
|
||||
"implements", # Implementation
|
||||
"supersedes", # Replacement
|
||||
"contains", # Parent-child
|
||||
"references", # Cross-reference
|
||||
"triggers", # Causal
|
||||
]
|
||||
|
||||
# Access control per domain
|
||||
# Format: {domain: {"read": [users/groups], "write": [users/groups]}}
|
||||
# Special values: "admin" = admin user only, "operators" = operators group, "all" = everyone
|
||||
KG_PERMISSIONS = {
|
||||
"sysadmin": {"read": ["admin"], "write": ["admin"]},
|
||||
"users": {"read": ["admin"], "write": ["admin"]},
|
||||
"projects": {"read": ["admin", "operators"], "write": ["admin", "operators"]},
|
||||
"research": {"read": ["all"], "write": ["all"]}, # All users can write research via Zen
|
||||
}
|
||||
|
||||
|
||||
def get_current_user() -> str:
|
||||
"""Get current username."""
|
||||
return pwd.getpwuid(os.getuid()).pw_name
|
||||
|
||||
|
||||
def get_user_groups(username: str = None) -> List[str]:
|
||||
"""Get groups for a user."""
|
||||
if username is None:
|
||||
username = get_current_user()
|
||||
try:
|
||||
groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem]
|
||||
# Add primary group
|
||||
primary_gid = pwd.getpwnam(username).pw_gid
|
||||
primary_group = grp.getgrgid(primary_gid).gr_name
|
||||
if primary_group not in groups:
|
||||
groups.append(primary_group)
|
||||
return groups
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
|
||||
def check_permission(domain: str, action: str) -> bool:
|
||||
"""Check if current user has permission for action on domain.
|
||||
|
||||
Args:
|
||||
domain: KG domain (sysadmin, users, projects, research)
|
||||
action: "read" or "write"
|
||||
|
||||
Returns:
|
||||
True if permitted, False otherwise
|
||||
"""
|
||||
if domain not in KG_PERMISSIONS:
|
||||
return False
|
||||
|
||||
allowed = KG_PERMISSIONS[domain].get(action, [])
|
||||
|
||||
# "all" means everyone
|
||||
if "all" in allowed:
|
||||
return True
|
||||
|
||||
username = get_current_user()
|
||||
|
||||
# Root always has access
|
||||
if username == "root":
|
||||
return True
|
||||
|
||||
# Check direct user match
|
||||
if username in allowed:
|
||||
return True
|
||||
|
||||
# Check group membership
|
||||
user_groups = get_user_groups(username)
|
||||
for group in allowed:
|
||||
if group in user_groups:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class KnowledgeGraph:
|
||||
"""Knowledge graph operations for a single domain."""
|
||||
|
||||
def __init__(self, domain: str, skip_permission_check: bool = False):
|
||||
if domain not in KG_PATHS:
|
||||
raise ValueError(f"Unknown domain: {domain}. Valid: {list(KG_PATHS.keys())}")
|
||||
|
||||
self.domain = domain
|
||||
self.db_path = KG_PATHS[domain]
|
||||
self._skip_permission_check = skip_permission_check
|
||||
self._ensure_schema()
|
||||
|
||||
def _check_read(self):
|
||||
"""Check read permission."""
|
||||
if self._skip_permission_check:
|
||||
return
|
||||
if not check_permission(self.domain, "read"):
|
||||
user = get_current_user()
|
||||
raise PermissionError(f"User '{user}' does not have read access to '{self.domain}' KG")
|
||||
|
||||
def _check_write(self):
|
||||
"""Check write permission."""
|
||||
if self._skip_permission_check:
|
||||
return
|
||||
if not check_permission(self.domain, "write"):
|
||||
user = get_current_user()
|
||||
raise PermissionError(f"User '{user}' does not have write access to '{self.domain}' KG")
|
||||
|
||||
def _ensure_schema(self):
|
||||
"""Create tables if they don't exist."""
|
||||
KG_BASE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# Main entities table
|
||||
c.execute('''
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
content TEXT,
|
||||
metadata TEXT,
|
||||
created_at REAL,
|
||||
updated_at REAL,
|
||||
source TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# Relations table
|
||||
c.execute('''
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
context TEXT,
|
||||
weight INTEGER DEFAULT 1,
|
||||
created_at REAL,
|
||||
FOREIGN KEY (source_id) REFERENCES entities(id),
|
||||
FOREIGN KEY (target_id) REFERENCES entities(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Observations table (notes, QA findings, etc.)
|
||||
c.execute('''
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id TEXT PRIMARY KEY,
|
||||
entity_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
observer TEXT,
|
||||
created_at REAL,
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# FTS5 virtual table for full-text search
|
||||
c.execute('''
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
||||
name, type, content, metadata,
|
||||
content='entities',
|
||||
content_rowid='rowid'
|
||||
)
|
||||
''')
|
||||
|
||||
# Triggers to keep FTS in sync
|
||||
c.execute('''
|
||||
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
|
||||
INSERT INTO entities_fts(rowid, name, type, content, metadata)
|
||||
VALUES (NEW.rowid, NEW.name, NEW.type, NEW.content, NEW.metadata);
|
||||
END
|
||||
''')
|
||||
|
||||
c.execute('''
|
||||
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
|
||||
INSERT INTO entities_fts(entities_fts, rowid, name, type, content, metadata)
|
||||
VALUES ('delete', OLD.rowid, OLD.name, OLD.type, OLD.content, OLD.metadata);
|
||||
END
|
||||
''')
|
||||
|
||||
c.execute('''
|
||||
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
|
||||
INSERT INTO entities_fts(entities_fts, rowid, name, type, content, metadata)
|
||||
VALUES ('delete', OLD.rowid, OLD.name, OLD.type, OLD.content, OLD.metadata);
|
||||
INSERT INTO entities_fts(rowid, name, type, content, metadata)
|
||||
VALUES (NEW.rowid, NEW.name, NEW.type, NEW.content, NEW.metadata);
|
||||
END
|
||||
''')
|
||||
|
||||
# Indexes
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id)')
|
||||
c.execute('CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id)')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
# --- Entity Operations ---
|
||||
|
||||
def add_entity(self, name: str, entity_type: str, content: str = "",
|
||||
metadata: dict = None, source: str = None) -> str:
|
||||
"""Add or update an entity."""
|
||||
self._check_write()
|
||||
if entity_type not in ENTITY_TYPES.get(self.domain, []):
|
||||
valid = ENTITY_TYPES.get(self.domain, [])
|
||||
raise ValueError(f"Invalid type '{entity_type}' for {self.domain}. Valid: {valid}")
|
||||
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
now = time.time()
|
||||
entity_id = str(uuid.uuid4())
|
||||
metadata_json = json.dumps(metadata) if metadata else "{}"
|
||||
|
||||
# Upsert
|
||||
c.execute('''
|
||||
INSERT INTO entities (id, name, type, domain, content, metadata, created_at, updated_at, source)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
type = excluded.type,
|
||||
content = excluded.content,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = excluded.updated_at,
|
||||
source = excluded.source
|
||||
''', (entity_id, name, entity_type, self.domain, content, metadata_json, now, now, source))
|
||||
|
||||
# Get the actual ID (might be existing)
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (name,))
|
||||
row = c.fetchone()
|
||||
entity_id = row['id'] if row else entity_id
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return entity_id
|
||||
|
||||
def get_entity(self, name: str) -> Optional[Dict]:
|
||||
"""Get entity by name."""
|
||||
self._check_read()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT * FROM entities WHERE name = ?', (name,))
|
||||
row = c.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"type": row["type"],
|
||||
"domain": row["domain"],
|
||||
"content": row["content"],
|
||||
"metadata": json.loads(row["metadata"]) if row["metadata"] else {},
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
"source": row["source"],
|
||||
}
|
||||
|
||||
def get_entity_by_id(self, entity_id: str) -> Optional[Dict]:
|
||||
"""Get entity by ID."""
|
||||
self._check_read()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT * FROM entities WHERE id = ?', (entity_id,))
|
||||
row = c.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
return dict(row)
|
||||
|
||||
def list_entities(self, entity_type: str = None, limit: int = 100) -> List[Dict]:
|
||||
"""List entities, optionally filtered by type."""
|
||||
self._check_read()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
if entity_type:
|
||||
c.execute('''
|
||||
SELECT * FROM entities WHERE type = ?
|
||||
ORDER BY updated_at DESC LIMIT ?
|
||||
''', (entity_type, limit))
|
||||
else:
|
||||
c.execute('''
|
||||
SELECT * FROM entities
|
||||
ORDER BY updated_at DESC LIMIT ?
|
||||
''', (limit,))
|
||||
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def delete_entity(self, name: str) -> bool:
|
||||
"""Delete entity and its relations/observations."""
|
||||
self._check_write()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
# Get entity ID
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (name,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
entity_id = row['id']
|
||||
|
||||
# Delete relations
|
||||
c.execute('DELETE FROM relations WHERE source_id = ? OR target_id = ?',
|
||||
(entity_id, entity_id))
|
||||
|
||||
# Delete observations
|
||||
c.execute('DELETE FROM observations WHERE entity_id = ?', (entity_id,))
|
||||
|
||||
# Delete entity
|
||||
c.execute('DELETE FROM entities WHERE id = ?', (entity_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
# --- Search ---
|
||||
|
||||
def search(self, query: str, limit: int = 20) -> List[Dict]:
|
||||
"""Full-text search across entities."""
|
||||
self._check_read()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
# FTS5 search
|
||||
c.execute('''
|
||||
SELECT e.*, rank
|
||||
FROM entities_fts fts
|
||||
JOIN entities e ON e.rowid = fts.rowid
|
||||
WHERE entities_fts MATCH ?
|
||||
ORDER BY rank
|
||||
LIMIT ?
|
||||
''', (query, limit))
|
||||
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
# --- Relations ---
|
||||
|
||||
def add_relation(self, source_name: str, target_name: str,
|
||||
relation: str, context: str = None, weight: int = 1) -> Optional[str]:
|
||||
"""Add relation between entities."""
|
||||
self._check_write()
|
||||
if relation not in RELATION_TYPES:
|
||||
raise ValueError(f"Invalid relation: {relation}. Valid: {RELATION_TYPES}")
|
||||
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
# Get entity IDs
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (source_name,))
|
||||
source = c.fetchone()
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (target_name,))
|
||||
target = c.fetchone()
|
||||
|
||||
if not source or not target:
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
rel_id = str(uuid.uuid4())
|
||||
now = time.time()
|
||||
|
||||
c.execute('''
|
||||
INSERT INTO relations (id, source_id, target_id, relation, context, weight, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (rel_id, source['id'], target['id'], relation, context, weight, now))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return rel_id
|
||||
|
||||
def get_relations(self, entity_name: str, direction: str = "both") -> List[Dict]:
|
||||
"""Get relations for an entity."""
|
||||
self._check_read()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (entity_name,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return []
|
||||
|
||||
entity_id = row['id']
|
||||
|
||||
if direction == "outgoing":
|
||||
c.execute('''
|
||||
SELECT r.*, e.name as target_name
|
||||
FROM relations r
|
||||
JOIN entities e ON e.id = r.target_id
|
||||
WHERE r.source_id = ?
|
||||
''', (entity_id,))
|
||||
elif direction == "incoming":
|
||||
c.execute('''
|
||||
SELECT r.*, e.name as source_name
|
||||
FROM relations r
|
||||
JOIN entities e ON e.id = r.source_id
|
||||
WHERE r.target_id = ?
|
||||
''', (entity_id,))
|
||||
else:
|
||||
c.execute('''
|
||||
SELECT r.*,
|
||||
s.name as source_name,
|
||||
t.name as target_name
|
||||
FROM relations r
|
||||
JOIN entities s ON s.id = r.source_id
|
||||
JOIN entities t ON t.id = r.target_id
|
||||
WHERE r.source_id = ? OR r.target_id = ?
|
||||
''', (entity_id, entity_id))
|
||||
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
# --- Observations ---
|
||||
|
||||
def add_observation(self, entity_name: str, content: str, observer: str = "system") -> Optional[str]:
|
||||
"""Add observation to an entity."""
|
||||
self._check_write()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (entity_name,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
obs_id = str(uuid.uuid4())
|
||||
now = time.time()
|
||||
|
||||
c.execute('''
|
||||
INSERT INTO observations (id, entity_id, content, observer, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (obs_id, row['id'], content, observer, now))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return obs_id
|
||||
|
||||
def get_observations(self, entity_name: str) -> List[Dict]:
|
||||
"""Get observations for an entity."""
|
||||
self._check_read()
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT id FROM entities WHERE name = ?', (entity_name,))
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return []
|
||||
|
||||
c.execute('''
|
||||
SELECT * FROM observations
|
||||
WHERE entity_id = ?
|
||||
ORDER BY created_at DESC
|
||||
''', (row['id'],))
|
||||
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
# --- Stats ---
|
||||
|
||||
def stats(self) -> Dict:
|
||||
"""Get KG statistics."""
|
||||
conn = self._connect()
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT COUNT(*) as count FROM entities')
|
||||
entities = c.fetchone()['count']
|
||||
|
||||
c.execute('SELECT COUNT(*) as count FROM relations')
|
||||
relations = c.fetchone()['count']
|
||||
|
||||
c.execute('SELECT COUNT(*) as count FROM observations')
|
||||
observations = c.fetchone()['count']
|
||||
|
||||
c.execute('SELECT type, COUNT(*) as count FROM entities GROUP BY type')
|
||||
by_type = {row['type']: row['count'] for row in c.fetchall()}
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"domain": self.domain,
|
||||
"entities": entities,
|
||||
"relations": relations,
|
||||
"observations": observations,
|
||||
"by_type": by_type,
|
||||
}
|
||||
|
||||
|
||||
# --- Cross-Domain Search ---
|
||||
|
||||
def search_all(query: str, limit: int = 20) -> Dict[str, List[Dict]]:
|
||||
"""Search across all knowledge graphs."""
|
||||
results = {}
|
||||
for domain in KG_PATHS.keys():
|
||||
try:
|
||||
kg = KnowledgeGraph(domain)
|
||||
results[domain] = kg.search(query, limit)
|
||||
except Exception as e:
|
||||
results[domain] = [{"error": str(e)}]
|
||||
return results
|
||||
|
||||
|
||||
def get_all_stats() -> Dict[str, Dict]:
|
||||
"""Get stats from all knowledge graphs."""
|
||||
stats = {}
|
||||
for domain in KG_PATHS.keys():
|
||||
try:
|
||||
kg = KnowledgeGraph(domain)
|
||||
stats[domain] = kg.stats()
|
||||
except Exception as e:
|
||||
stats[domain] = {"error": str(e)}
|
||||
return stats
|
||||
|
||||
|
||||
# --- CLI for testing ---
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: knowledge_graph.py <command> [args]")
|
||||
print("Commands:")
|
||||
print(" stats - Show all KG stats")
|
||||
print(" search <query> - Search all KGs")
|
||||
print(" add <domain> <name> <type> <content>")
|
||||
print(" get <domain> <name>")
|
||||
print(" list <domain> [type]")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "stats":
|
||||
for domain, s in get_all_stats().items():
|
||||
print(f"\n{domain}:")
|
||||
for k, v in s.items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
elif cmd == "search" and len(sys.argv) >= 3:
|
||||
query = " ".join(sys.argv[2:])
|
||||
results = search_all(query)
|
||||
for domain, entities in results.items():
|
||||
if entities and not entities[0].get("error"):
|
||||
print(f"\n{domain}:")
|
||||
for e in entities:
|
||||
print(f" - {e.get('name', 'unknown')}: {e.get('type', '')}")
|
||||
|
||||
elif cmd == "add" and len(sys.argv) >= 5:
|
||||
domain, name, etype = sys.argv[2:5]
|
||||
content = " ".join(sys.argv[5:]) if len(sys.argv) > 5 else ""
|
||||
kg = KnowledgeGraph(domain)
|
||||
eid = kg.add_entity(name, etype, content)
|
||||
print(f"Added: {eid}")
|
||||
|
||||
elif cmd == "get" and len(sys.argv) >= 4:
|
||||
kg = KnowledgeGraph(sys.argv[2])
|
||||
entity = kg.get_entity(sys.argv[3])
|
||||
print(json.dumps(entity, indent=2))
|
||||
|
||||
elif cmd == "list" and len(sys.argv) >= 3:
|
||||
kg = KnowledgeGraph(sys.argv[2])
|
||||
etype = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
for e in kg.list_entities(etype):
|
||||
print(f" - {e['name']}: {e['type']}")
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user