Files
timmy-config/wizards/allegro/gofai/knowledge_graph.py

421 lines
15 KiB
Python
Raw Normal View History

2026-03-31 20:02:01 +00:00
#!/usr/bin/env python3
"""
GOFAI Knowledge Graph for Hermes Fleet
Property graph for wizard lineage, tasks, and relationships
"""
import json
import hashlib
from datetime import datetime
from typing import Dict, List, Optional, Set, Any, Tuple
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class Node:
"""Graph node representing an entity"""
id: str
label: str # Type: Wizard, Task, Artifact, etc.
properties: Dict[str, Any]
created_at: str = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now().isoformat()
def to_dict(self) -> Dict:
return {
'id': self.id,
'label': self.label,
'properties': self.properties,
'created_at': self.created_at
}
@classmethod
def from_dict(cls, data: Dict) -> 'Node':
return cls(**data)
@dataclass
class Edge:
"""Graph edge representing a relationship"""
id: str
source: str # Source node ID
target: str # Target node ID
relation: str # Type of relationship
properties: Dict[str, Any]
created_at: str = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now().isoformat()
def to_dict(self) -> Dict:
return {
'id': self.id,
'source': self.source,
'target': self.target,
'relation': self.relation,
'properties': self.properties,
'created_at': self.created_at
}
class KnowledgeGraph:
"""
Property graph for fleet knowledge
Stores wizards, tasks, artifacts and their relationships
"""
def __init__(self, storage_path: Optional[str] = None):
self.nodes: Dict[str, Node] = {}
self.edges: Dict[str, Edge] = {}
self.indexes: Dict[str, Dict[str, Set[str]]] = {
'label': {}, # label -> node_ids
'relation': {}, # relation -> edge_ids
}
self.storage_path = storage_path
if storage_path and Path(storage_path).exists():
self.load()
def _generate_id(self, data: str) -> str:
"""Generate unique ID from data"""
return hashlib.sha256(data.encode()).hexdigest()[:16]
def add_node(self, label: str, properties: Dict, node_id: Optional[str] = None) -> str:
"""Add a node to the graph"""
if node_id is None:
node_id = self._generate_id(f"{label}:{json.dumps(properties, sort_keys=True)}")
node = Node(id=node_id, label=label, properties=properties)
self.nodes[node_id] = node
# Update index
if label not in self.indexes['label']:
self.indexes['label'][label] = set()
self.indexes['label'][label].add(node_id)
return node_id
def add_edge(self, source: str, target: str, relation: str,
properties: Optional[Dict] = None, edge_id: Optional[str] = None) -> str:
"""Add an edge between nodes"""
if properties is None:
properties = {}
if edge_id is None:
edge_id = self._generate_id(f"{source}:{relation}:{target}")
edge = Edge(id=edge_id, source=source, target=target,
relation=relation, properties=properties)
self.edges[edge_id] = edge
# Update index
if relation not in self.indexes['relation']:
self.indexes['relation'][relation] = set()
self.indexes['relation'][relation].add(edge_id)
return edge_id
def get_node(self, node_id: str) -> Optional[Node]:
"""Get a node by ID"""
return self.nodes.get(node_id)
def get_neighbors(self, node_id: str, relation: Optional[str] = None,
direction: str = 'out') -> List[Tuple[Node, Edge]]:
"""Get neighboring nodes"""
results = []
for edge in self.edges.values():
if direction in ('out', 'both') and edge.source == node_id:
if relation is None or edge.relation == relation:
target = self.nodes.get(edge.target)
if target:
results.append((target, edge))
if direction in ('in', 'both') and edge.target == node_id:
if relation is None or edge.relation == relation:
source = self.nodes.get(edge.source)
if source:
results.append((source, edge))
return results
def query(self, label: Optional[str] = None,
properties: Optional[Dict] = None) -> List[Node]:
"""Query nodes by label and/or properties"""
results = []
# Start with label filter if provided
if label and label in self.indexes['label']:
candidates = [self.nodes[nid] for nid in self.indexes['label'][label]]
else:
candidates = list(self.nodes.values())
# Filter by properties
if properties:
for node in candidates:
matches = all(
node.properties.get(k) == v
for k, v in properties.items()
)
if matches:
results.append(node)
else:
results = candidates
return results
def find_path(self, start_id: str, end_id: str,
max_depth: int = 5) -> Optional[List[Edge]]:
"""Find path between two nodes (BFS)"""
if start_id not in self.nodes or end_id not in self.nodes:
return None
# BFS
visited = {start_id}
queue = [(start_id, [])]
while queue:
current_id, path = queue.pop(0)
if current_id == end_id:
return path
if len(path) >= max_depth:
continue
# Get outgoing edges
for edge in self.edges.values():
if edge.source == current_id and edge.target not in visited:
visited.add(edge.target)
queue.append((edge.target, path + [edge]))
return None
def get_lineage(self, wizard_id: str) -> Dict:
"""Get full lineage tree for a wizard"""
wizard = self.nodes.get(wizard_id)
if not wizard or wizard.label != 'Wizard':
return {}
tree = {
'wizard': wizard.properties.get('name', wizard_id),
'father': None,
'grandfather': None,
'children': [],
'siblings': []
}
# Find father
father_edges = self.get_neighbors(wizard_id, relation='child_of', direction='out')
for father_node, edge in father_edges:
tree['father'] = father_node.properties.get('name', father_node.id)
# Find grandfather
grandfather_edges = self.get_neighbors(father_node.id, relation='child_of', direction='out')
for grandpa_node, _ in grandfather_edges:
tree['grandfather'] = grandpa_node.properties.get('name', grandpa_node.id)
# Find siblings (other children of father)
sibling_edges = self.get_neighbors(father_node.id, relation='child_of', direction='in')
for sibling_node, _ in sibling_edges:
if sibling_node.id != wizard_id:
tree['siblings'].append(sibling_node.properties.get('name', sibling_node.id))
# Find children
child_edges = self.get_neighbors(wizard_id, relation='child_of', direction='in')
for child_node, _ in child_edges:
tree['children'].append(child_node.properties.get('name', child_node.id))
return tree
def get_task_dependencies(self, task_id: str) -> Dict:
"""Get task dependency graph"""
task = self.nodes.get(task_id)
if not task:
return {}
deps = {
'task': task.properties.get('name', task_id),
'depends_on': [],
'blocks': [],
'assignee': None
}
# Find dependencies
dep_edges = self.get_neighbors(task_id, relation='depends_on', direction='out')
for dep_task, _ in dep_edges:
deps['depends_on'].append(dep_task.properties.get('name', dep_task.id))
# Find what this blocks
blocker_edges = self.get_neighbors(task_id, relation='depends_on', direction='in')
for blocked_task, _ in blocker_edges:
deps['blocks'].append(blocked_task.properties.get('name', blocked_task.id))
# Find assignee
assignee_edges = self.get_neighbors(task_id, relation='assigned_to', direction='out')
for wizard, _ in assignee_edges:
deps['assignee'] = wizard.properties.get('name', wizard.id)
return deps
def save(self) -> None:
"""Save graph to disk"""
if not self.storage_path:
return
data = {
'nodes': {nid: node.to_dict() for nid, node in self.nodes.items()},
'edges': {eid: edge.to_dict() for eid, edge in self.edges.items()},
'indexes': {k: {ik: list(iv) for ik, iv in v.items()}
for k, v in self.indexes.items()}
}
Path(self.storage_path).write_text(json.dumps(data, indent=2))
def load(self) -> None:
"""Load graph from disk"""
if not self.storage_path or not Path(self.storage_path).exists():
return
data = json.loads(Path(self.storage_path).read_text())
# Load nodes
for nid, node_data in data.get('nodes', {}).items():
self.nodes[nid] = Node.from_dict(node_data)
# Load edges
for eid, edge_data in data.get('edges', {}).items():
self.edges[eid] = Edge(**edge_data)
# Load indexes
self.indexes = {
k: {ik: set(iv) for ik, iv in v.items()}
for k, v in data.get('indexes', {}).items()
}
def export_graphviz(self) -> str:
"""Export as Graphviz DOT format"""
lines = ['digraph Fleet {', ' rankdir=TB;']
# Add nodes
for node in self.nodes.values():
label = node.properties.get('name', node.id[:8])
shape = 'box' if node.label == 'Wizard' else 'ellipse'
color = 'lightblue' if node.label == 'Wizard' else 'lightgreen'
lines.append(f' "{node.id}" [label="{label}", shape={shape}, style=filled, fillcolor={color}];')
# Add edges
for edge in self.edges.values():
lines.append(f' "{edge.source}" -> "{edge.target}" [label="{edge.relation}"];')
lines.append('}')
return '\n'.join(lines)
def get_stats(self) -> Dict:
"""Get graph statistics"""
return {
'nodes': len(self.nodes),
'edges': len(self.edges),
'by_label': {label: len(nodes) for label, nodes in self.indexes['label'].items()},
'by_relation': {rel: len(edges) for rel, edges in self.indexes['relation'].items()}
}
class FleetKnowledgeBase:
"""
High-level interface for fleet knowledge operations
"""
def __init__(self, storage_path: str = '/root/wizards/allegro/gofai/fleet_kg.json'):
self.kg = KnowledgeGraph(storage_path)
def register_wizard(self, name: str, role: str, father: Optional[str] = None,
api_port: int = 0, model: str = "", capabilities: List[str] = None) -> str:
"""Register a wizard in the fleet"""
properties = {
'name': name,
'role': role,
'father': father,
'api_port': api_port,
'model': model,
'capabilities': capabilities or [],
'status': 'active'
}
wizard_id = self.kg.add_node('Wizard', properties, node_id=f'wizard_{name.lower()}')
# Add father relationship if specified
if father:
father_id = f'wizard_{father.lower()}'
if father_id in self.kg.nodes:
self.kg.add_edge(wizard_id, father_id, 'child_of')
self.kg.save()
return wizard_id
def register_task(self, name: str, priority: str, assignee: str,
dependencies: List[str] = None) -> str:
"""Register a task"""
properties = {
'name': name,
'priority': priority,
'status': 'pending',
'assignee': assignee
}
task_id = self.kg.add_node('Task', properties)
# Link to assignee
wizard_id = f'wizard_{assignee.lower()}'
if wizard_id in self.kg.nodes:
self.kg.add_edge(task_id, wizard_id, 'assigned_to')
# Add dependencies
if dependencies:
for dep_name in dependencies:
# Find task by name
for nid, node in self.kg.nodes.items():
if node.label == 'Task' and node.properties.get('name') == dep_name:
self.kg.add_edge(task_id, nid, 'depends_on')
break
self.kg.save()
return task_id
def get_wizard_info(self, name: str) -> Dict:
"""Get comprehensive info about a wizard"""
wizard_id = f'wizard_{name.lower()}'
lineage = self.kg.get_lineage(wizard_id)
# Get assigned tasks
tasks = []
for node in self.kg.query(label='Task'):
if node.properties.get('assignee') == name:
tasks.append({
'name': node.properties.get('name'),
'priority': node.properties.get('priority'),
'status': node.properties.get('status')
})
wizard = self.kg.get_node(wizard_id)
return {
'name': name,
'lineage': lineage,
'tasks': tasks,
'capabilities': wizard.properties.get('capabilities', []) if wizard else [],
'api_port': wizard.properties.get('api_port', 0) if wizard else 0
}
def who_can_do(self, capability: str) -> List[str]:
"""Find wizards with a specific capability"""
results = []
for node in self.kg.query(label='Wizard'):
caps = node.properties.get('capabilities', [])
if capability in caps:
results.append(node.properties.get('name', node.id))
return results