""" Learning Journal ================ Tracks what works and what doesn't. Meta-learning layer for HAIM. """ import json import os from datetime import datetime, timezone from typing import Dict, List, Optional from dataclasses import dataclass, field, asdict JOURNAL_PATH = "./data/learning_journal.json" @dataclass class LearningEntry: """A single learning.""" id: str lesson: str context: str outcome: str # "success" | "failure" | "mixed" confidence: float # 0.0 - 1.0 applications: int = 0 # Times this learning was applied created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) tags: List[str] = field(default_factory=list) class LearningJournal: """Meta-learning storage.""" def __init__(self, path: str = JOURNAL_PATH): self.path = path self.entries: Dict[str, LearningEntry] = {} self.predictions: Dict[str, dict] = {} # In-memory prediction buffer self._load() def _load(self): if os.path.exists(self.path): with open(self.path, "r") as f: data = json.load(f) for eid, entry_data in data.items(): self.entries[eid] = LearningEntry(**entry_data) def _save(self): os.makedirs(os.path.dirname(self.path), exist_ok=True) with open(self.path, "w") as f: json.dump({k: asdict(v) for k, v in self.entries.items()}, f, indent=2) def record( self, lesson: str, context: str, outcome: str = "success", confidence: float = 0.7, tags: List[str] = None, surprise: float = 0.0 ) -> str: """Record a new learning.""" entry_id = f"learn_{len(self.entries)}" # Boost confidence if high surprise (flashbulb learning) if surprise > 0.5: confidence = min(1.0, confidence * (1.0 + surprise)) entry = LearningEntry( id=entry_id, lesson=lesson, context=context, outcome=outcome, confidence=confidence, tags=tags or [] ) if surprise > 0: entry.tags.append(f"surprise_{surprise:.2f}") if surprise > 0.7: entry.tags.append("flashbulb_memory") self.entries[entry_id] = entry self._save() return entry_id def register_prediction(self, context: str, expectation: str) -> str: """Start a prediction cycle (v1.6).""" pred_id = f"pred_{datetime.now(timezone.utc).timestamp()}" self.predictions[pred_id] = { "context": context, "expectation": expectation, "timestamp": datetime.now(timezone.utc) } return pred_id def evaluate_surprise(self, expectation: str, actual: str) -> float: """ Calculate surprise metric (0.0 to 1.0). Simple heuristic: Length difference + keyword overlap. In v1.8 this should use semantic embedding distance. """ if expectation == actual: return 0.0 # Jaccard similarity of words exp_words = set(expectation.lower().split()) act_words = set(actual.lower().split()) if not exp_words or not act_words: return 1.0 intersection = len(exp_words.intersection(act_words)) union = len(exp_words.union(act_words)) similarity = intersection / union surprise = 1.0 - similarity return surprise def resolve_prediction(self, pred_id: str, actual_result: str) -> Optional[str]: """ Close the loop: Compare expectation vs reality, record learning if surprised. """ pred = self.predictions.pop(pred_id, None) if not pred: return None surprise = self.evaluate_surprise(pred['expectation'], actual_result) # Auto-record if significant surprise if surprise > 0.3: return self.record( lesson=f"Expectation '{pred['expectation']}' differed from '{actual_result}'", context=pred['context'], outcome="mixed" if surprise < 0.8 else "failure", confidence=0.8, surprise=surprise, tags=["prediction_error", "auto_generated"] ) return None def apply(self, entry_id: str): """Mark a learning as applied (reinforcement).""" if entry_id in self.entries: self.entries[entry_id].applications += 1 self.entries[entry_id].confidence = min(1.0, self.entries[entry_id].confidence * 1.05) self._save() def contradict(self, entry_id: str): """Mark a learning as contradicted (weakening).""" if entry_id in self.entries: self.entries[entry_id].confidence *= 0.8 self._save() def query(self, context: str, top_k: int = 5) -> List[LearningEntry]: """Find relevant learnings for a context.""" # Simple keyword matching for now context_lower = context.lower() scored = [] for entry in self.entries.values(): score = 0 for word in context_lower.split(): if word in entry.context.lower() or word in entry.lesson.lower(): score += 1 if word in entry.tags: score += 2 score *= entry.confidence if score > 0: scored.append((score, entry)) scored.sort(key=lambda x: x[0], reverse=True) return [e for _, e in scored[:top_k]] def get_top_learnings(self, n: int = 10) -> List[LearningEntry]: """Get most confident/applied learnings.""" sorted_entries = sorted( self.entries.values(), key=lambda e: e.confidence * (1 + e.applications * 0.1), reverse=True ) return sorted_entries[:n] def stats(self) -> Dict: return { "total_learnings": len(self.entries), "successes": sum(1 for e in self.entries.values() if e.outcome == "success"), "failures": sum(1 for e in self.entries.values() if e.outcome == "failure"), "avg_confidence": sum(e.confidence for e in self.entries.values()) / max(1, len(self.entries)), "total_applications": sum(e.applications for e in self.entries.values()) }