|
| 1 | +""" |
| 2 | +Atomspace Core Integration |
| 3 | +
|
| 4 | +Manages Atomspace initialization, persistence, and schema definitions. |
| 5 | +""" |
| 6 | + |
| 7 | +import json |
| 8 | +import pickle |
| 9 | +from datetime import datetime, timezone |
| 10 | +from pathlib import Path |
| 11 | +from typing import Dict, List, Optional, Any |
| 12 | +from dataclasses import dataclass, asdict |
| 13 | +from enum import Enum |
| 14 | + |
| 15 | + |
| 16 | +class AtomType(Enum): |
| 17 | + """Atom types for cognitive architecture""" |
| 18 | + EPISODIC_MEMORY = "EpisodicMemoryNode" |
| 19 | + CONCEPT = "ConceptNode" |
| 20 | + SELF_MODEL = "SelfModelNode" |
| 21 | + GOAL = "GoalNode" |
| 22 | + RELATIONAL_FRAME = "RelationalFrameNode" |
| 23 | + CODE = "CodeNode" |
| 24 | + PERCEPTION = "PerceptionNode" |
| 25 | + EMOTIONAL_STATE = "EmotionalStateNode" |
| 26 | + |
| 27 | + |
| 28 | +@dataclass |
| 29 | +class Atom: |
| 30 | + """Base atom structure""" |
| 31 | + id: str |
| 32 | + type: AtomType |
| 33 | + content: Any |
| 34 | + timestamp: datetime |
| 35 | + truth_value: float = 1.0 |
| 36 | + confidence: float = 1.0 |
| 37 | + |
| 38 | + def to_dict(self) -> Dict: |
| 39 | + return { |
| 40 | + 'id': self.id, |
| 41 | + 'type': self.type.value, |
| 42 | + 'content': self.content, |
| 43 | + 'timestamp': self.timestamp.isoformat(), |
| 44 | + 'truth_value': self.truth_value, |
| 45 | + 'confidence': self.confidence |
| 46 | + } |
| 47 | + |
| 48 | + |
| 49 | +@dataclass |
| 50 | +class Link: |
| 51 | + """Link between atoms""" |
| 52 | + source_id: str |
| 53 | + target_id: str |
| 54 | + link_type: str |
| 55 | + strength: float = 1.0 |
| 56 | + |
| 57 | + def to_dict(self) -> Dict: |
| 58 | + return asdict(self) |
| 59 | + |
| 60 | + |
| 61 | +class Atomspace: |
| 62 | + """ |
| 63 | + Atomspace implementation with persistence. |
| 64 | +
|
| 65 | + This is a simplified Atomspace until Hyperon integration is complete. |
| 66 | + Will be replaced with actual Hyperon Atomspace bindings. |
| 67 | + """ |
| 68 | + |
| 69 | + def __init__(self, persistence_path: Optional[Path] = None): |
| 70 | + self.atoms: Dict[str, Atom] = {} |
| 71 | + self.links: List[Link] = [] |
| 72 | + self.persistence_path = persistence_path |
| 73 | + self._atom_counter = 0 |
| 74 | + |
| 75 | + if persistence_path and persistence_path.exists(): |
| 76 | + self.load() |
| 77 | + |
| 78 | + def add_atom(self, atom: Atom) -> str: |
| 79 | + """Add atom to atomspace""" |
| 80 | + if not atom.id: |
| 81 | + atom.id = self._generate_atom_id() |
| 82 | + self.atoms[atom.id] = atom |
| 83 | + return atom.id |
| 84 | + |
| 85 | + def add_link(self, link: Link): |
| 86 | + """Add link between atoms""" |
| 87 | + self.links.append(link) |
| 88 | + |
| 89 | + def get_atom(self, atom_id: str) -> Optional[Atom]: |
| 90 | + """Retrieve atom by ID""" |
| 91 | + return self.atoms.get(atom_id) |
| 92 | + |
| 93 | + def query_by_type(self, atom_type: AtomType) -> List[Atom]: |
| 94 | + """Query atoms by type""" |
| 95 | + return [atom for atom in self.atoms.values() if atom.type == atom_type] |
| 96 | + |
| 97 | + def get_linked_atoms(self, atom_id: str, link_type: Optional[str] = None) -> List[Atom]: |
| 98 | + """Get atoms linked to given atom""" |
| 99 | + linked_ids = [] |
| 100 | + for link in self.links: |
| 101 | + if link.source_id == atom_id: |
| 102 | + if link_type is None or link.link_type == link_type: |
| 103 | + linked_ids.append(link.target_id) |
| 104 | + |
| 105 | + return [self.atoms[aid] for aid in linked_ids if aid in self.atoms] |
| 106 | + |
| 107 | + def save(self): |
| 108 | + """Save atomspace to disk""" |
| 109 | + if not self.persistence_path: |
| 110 | + return |
| 111 | + |
| 112 | + self.persistence_path.mkdir(parents=True, exist_ok=True) |
| 113 | + |
| 114 | + # Save atoms |
| 115 | + atoms_data = {aid: atom.to_dict() for aid, atom in self.atoms.items()} |
| 116 | + with open(self.persistence_path / 'atoms.json', 'w') as f: |
| 117 | + json.dump(atoms_data, f, indent=2) |
| 118 | + |
| 119 | + # Save links |
| 120 | + links_data = [link.to_dict() for link in self.links] |
| 121 | + with open(self.persistence_path / 'links.json', 'w') as f: |
| 122 | + json.dump(links_data, f, indent=2) |
| 123 | + |
| 124 | + def load(self): |
| 125 | + """Load atomspace from disk""" |
| 126 | + if not self.persistence_path: |
| 127 | + return |
| 128 | + |
| 129 | + # Load atoms |
| 130 | + atoms_file = self.persistence_path / 'atoms.json' |
| 131 | + if atoms_file.exists(): |
| 132 | + with open(atoms_file, 'r') as f: |
| 133 | + atoms_data = json.load(f) |
| 134 | + for aid, atom_dict in atoms_data.items(): |
| 135 | + atom = Atom( |
| 136 | + id=atom_dict['id'], |
| 137 | + type=AtomType(atom_dict['type']), |
| 138 | + content=atom_dict['content'], |
| 139 | + timestamp=datetime.fromisoformat(atom_dict['timestamp']), |
| 140 | + truth_value=atom_dict['truth_value'], |
| 141 | + confidence=atom_dict['confidence'] |
| 142 | + ) |
| 143 | + self.atoms[aid] = atom |
| 144 | + |
| 145 | + # Load links |
| 146 | + links_file = self.persistence_path / 'links.json' |
| 147 | + if links_file.exists(): |
| 148 | + with open(links_file, 'r') as f: |
| 149 | + links_data = json.load(f) |
| 150 | + self.links = [Link(**link_dict) for link_dict in links_data] |
| 151 | + |
| 152 | + def create_snapshot(self) -> str: |
| 153 | + """Create versioned snapshot""" |
| 154 | + if not self.persistence_path: |
| 155 | + return "" |
| 156 | + |
| 157 | + timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') |
| 158 | + snapshot_dir = self.persistence_path / 'snapshots' / timestamp |
| 159 | + snapshot_dir.mkdir(parents=True, exist_ok=True) |
| 160 | + |
| 161 | + # Save current state as snapshot |
| 162 | + atoms_data = {aid: atom.to_dict() for aid, atom in self.atoms.items()} |
| 163 | + with open(snapshot_dir / 'atoms.json', 'w') as f: |
| 164 | + json.dump(atoms_data, f, indent=2) |
| 165 | + |
| 166 | + links_data = [link.to_dict() for link in self.links] |
| 167 | + with open(snapshot_dir / 'links.json', 'w') as f: |
| 168 | + json.dump(links_data, f, indent=2) |
| 169 | + |
| 170 | + return timestamp |
| 171 | + |
| 172 | + def restore_snapshot(self, snapshot_id: str): |
| 173 | + """Restore from snapshot""" |
| 174 | + if not self.persistence_path: |
| 175 | + return |
| 176 | + |
| 177 | + snapshot_dir = self.persistence_path / 'snapshots' / snapshot_id |
| 178 | + if not snapshot_dir.exists(): |
| 179 | + raise ValueError(f"Snapshot {snapshot_id} not found") |
| 180 | + |
| 181 | + # Clear current state |
| 182 | + self.atoms.clear() |
| 183 | + self.links.clear() |
| 184 | + |
| 185 | + # Load snapshot |
| 186 | + with open(snapshot_dir / 'atoms.json', 'r') as f: |
| 187 | + atoms_data = json.load(f) |
| 188 | + for aid, atom_dict in atoms_data.items(): |
| 189 | + atom = Atom( |
| 190 | + id=atom_dict['id'], |
| 191 | + type=AtomType(atom_dict['type']), |
| 192 | + content=atom_dict['content'], |
| 193 | + timestamp=datetime.fromisoformat(atom_dict['timestamp']), |
| 194 | + truth_value=atom_dict['truth_value'], |
| 195 | + confidence=atom_dict['confidence'] |
| 196 | + ) |
| 197 | + self.atoms[aid] = atom |
| 198 | + |
| 199 | + with open(snapshot_dir / 'links.json', 'r') as f: |
| 200 | + links_data = json.load(f) |
| 201 | + self.links = [Link(**link_dict) for link_dict in links_data] |
| 202 | + |
| 203 | + def _generate_atom_id(self) -> str: |
| 204 | + """Generate unique atom ID""" |
| 205 | + self._atom_counter += 1 |
| 206 | + return f"atom_{self._atom_counter}_{datetime.now(timezone.utc).timestamp()}" |
| 207 | + |
| 208 | + def count_atoms(self) -> int: |
| 209 | + """Count total atoms""" |
| 210 | + return len(self.atoms) |
| 211 | + |
| 212 | + def count_concepts(self) -> int: |
| 213 | + """Count concept nodes""" |
| 214 | + return len([a for a in self.atoms.values() if a.type == AtomType.CONCEPT]) |
| 215 | + |
| 216 | + |
| 217 | +class PersistenceManager: |
| 218 | + """ |
| 219 | + Manages incremental saves and transaction logging. |
| 220 | + """ |
| 221 | + |
| 222 | + def __init__(self, atomspace: Atomspace): |
| 223 | + self.atomspace = atomspace |
| 224 | + self.transaction_log: List[Dict] = [] |
| 225 | + |
| 226 | + def log_transaction(self, operation: str, data: Dict): |
| 227 | + """Log transaction for recovery""" |
| 228 | + self.transaction_log.append({ |
| 229 | + 'timestamp': datetime.now(timezone.utc).isoformat(), |
| 230 | + 'operation': operation, |
| 231 | + 'data': data |
| 232 | + }) |
| 233 | + |
| 234 | + def incremental_save(self): |
| 235 | + """Perform incremental save""" |
| 236 | + self.atomspace.save() |
| 237 | + self._save_transaction_log() |
| 238 | + |
| 239 | + def _save_transaction_log(self): |
| 240 | + """Save transaction log""" |
| 241 | + if not self.atomspace.persistence_path: |
| 242 | + return |
| 243 | + |
| 244 | + log_file = self.atomspace.persistence_path / 'transaction_log.json' |
| 245 | + with open(log_file, 'w') as f: |
| 246 | + json.dump(self.transaction_log, f, indent=2) |
| 247 | + |
| 248 | + |
| 249 | +def bootstrap_atomspace(persistence_path: Optional[Path] = None) -> Atomspace: |
| 250 | + """ |
| 251 | + Bootstrap fresh atomspace with structural schema only. |
| 252 | + NO HARDCODED CONTENT - only creates capacity for experience. |
| 253 | + """ |
| 254 | + atomspace = Atomspace(persistence_path) |
| 255 | + |
| 256 | + # Create self-reference node (empty self-model) |
| 257 | + self_model = Atom( |
| 258 | + id="self_model_root", |
| 259 | + type=AtomType.SELF_MODEL, |
| 260 | + content={ |
| 261 | + 'birth_time': datetime.now(timezone.utc).isoformat(), |
| 262 | + 'capabilities': ['perceive', 'act', 'remember', 'learn'], |
| 263 | + 'identity_narrative': None # Emerges from experience |
| 264 | + }, |
| 265 | + timestamp=datetime.now(timezone.utc) |
| 266 | + ) |
| 267 | + atomspace.add_atom(self_model) |
| 268 | + |
| 269 | + # Initialize time system (empty timeline) |
| 270 | + timeline_root = Atom( |
| 271 | + id="timeline_root", |
| 272 | + type=AtomType.EPISODIC_MEMORY, |
| 273 | + content={ |
| 274 | + 'type': 'timeline_root', |
| 275 | + 'episodes': [] |
| 276 | + }, |
| 277 | + timestamp=datetime.now(timezone.utc) |
| 278 | + ) |
| 279 | + atomspace.add_atom(timeline_root) |
| 280 | + |
| 281 | + return atomspace |
0 commit comments