""" Atomizer Playbook - Structured Knowledge Store Part of the ACE (Agentic Context Engineering) implementation for Atomizer. Based on ACE framework principles: - Incremental delta updates (never rewrite wholesale) - Helpful/harmful tracking for each insight - Semantic deduplication - Category-based organization This module provides the core data structures for accumulating optimization knowledge across sessions. """ from dataclasses import dataclass, field from typing import List, Dict, Optional, Any from enum import Enum import json from pathlib import Path from datetime import datetime import hashlib class InsightCategory(Enum): """Categories for playbook insights.""" STRATEGY = "str" # Optimization strategies CALCULATION = "cal" # Formulas and calculations MISTAKE = "mis" # Common mistakes to avoid TOOL = "tool" # Tool usage patterns DOMAIN = "dom" # Domain-specific knowledge (FEA, NX) WORKFLOW = "wf" # Workflow patterns @dataclass class PlaybookItem: """ Single insight in the playbook with helpful/harmful tracking. Each item accumulates feedback over time: - helpful_count: Times this insight led to success - harmful_count: Times this insight led to failure - net_score: helpful - harmful (used for ranking) - confidence: helpful / (helpful + harmful) """ id: str category: InsightCategory content: str helpful_count: int = 0 harmful_count: int = 0 created_at: str = field(default_factory=lambda: datetime.now().isoformat()) last_used: Optional[str] = None source_trials: List[int] = field(default_factory=list) tags: List[str] = field(default_factory=list) @property def net_score(self) -> int: """Net helpfulness score (helpful - harmful).""" return self.helpful_count - self.harmful_count @property def confidence(self) -> float: """Confidence score (0.0-1.0) based on outcome ratio.""" total = self.helpful_count + self.harmful_count if total == 0: return 0.5 # Neutral confidence for untested items return self.helpful_count / total def to_context_string(self) -> str: """Format for injection into LLM context.""" return f"[{self.id}] helpful={self.helpful_count} harmful={self.harmful_count} :: {self.content}" def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "id": self.id, "category": self.category.value, "content": self.content, "helpful_count": self.helpful_count, "harmful_count": self.harmful_count, "created_at": self.created_at, "last_used": self.last_used, "source_trials": self.source_trials, "tags": self.tags } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "PlaybookItem": """Create from dictionary.""" return cls( id=data["id"], category=InsightCategory(data["category"]), content=data["content"], helpful_count=data.get("helpful_count", 0), harmful_count=data.get("harmful_count", 0), created_at=data.get("created_at", ""), last_used=data.get("last_used"), source_trials=data.get("source_trials", []), tags=data.get("tags", []) ) @dataclass class AtomizerPlaybook: """ Evolving playbook that accumulates optimization knowledge. Based on ACE framework principles: - Incremental delta updates (never rewrite wholesale) - Helpful/harmful tracking for each insight - Semantic deduplication - Category-based organization Usage: playbook = AtomizerPlaybook.load(path) item = playbook.add_insight(InsightCategory.STRATEGY, "Use shell elements for thin walls") playbook.record_outcome(item.id, helpful=True) playbook.save(path) """ items: Dict[str, PlaybookItem] = field(default_factory=dict) version: int = 1 last_updated: str = field(default_factory=lambda: datetime.now().isoformat()) def _generate_id(self, category: InsightCategory) -> str: """Generate unique ID for new item.""" existing = [k for k in self.items.keys() if k.startswith(category.value)] next_num = len(existing) + 1 return f"{category.value}-{next_num:05d}" def _content_hash(self, content: str) -> str: """Generate hash for content deduplication.""" normalized = content.lower().strip() return hashlib.md5(normalized.encode()).hexdigest()[:12] def add_insight( self, category: InsightCategory, content: str, source_trial: Optional[int] = None, tags: Optional[List[str]] = None ) -> PlaybookItem: """ Add new insight with delta update (ACE principle). Checks for semantic duplicates before adding. If duplicate found, increments helpful_count instead. Args: category: Type of insight content: The insight text source_trial: Trial number that generated this insight tags: Optional tags for filtering Returns: The created or updated PlaybookItem """ content_hash = self._content_hash(content) # Check for near-duplicates for item in self.items.values(): existing_hash = self._content_hash(item.content) if content_hash == existing_hash: # Update existing instead of adding duplicate item.helpful_count += 1 if source_trial and source_trial not in item.source_trials: item.source_trials.append(source_trial) if tags: item.tags = list(set(item.tags + tags)) self.last_updated = datetime.now().isoformat() return item # Create new item item_id = self._generate_id(category) item = PlaybookItem( id=item_id, category=category, content=content, source_trials=[source_trial] if source_trial else [], tags=tags or [] ) self.items[item_id] = item self.last_updated = datetime.now().isoformat() self.version += 1 return item def record_outcome(self, item_id: str, helpful: bool) -> bool: """ Record whether using this insight was helpful or harmful. Args: item_id: The playbook item ID helpful: True if outcome was positive, False if negative Returns: True if item was found and updated, False otherwise """ if item_id not in self.items: return False if helpful: self.items[item_id].helpful_count += 1 else: self.items[item_id].harmful_count += 1 self.items[item_id].last_used = datetime.now().isoformat() self.last_updated = datetime.now().isoformat() return True def get_context_for_task( self, task_type: str, max_items: int = 20, min_confidence: float = 0.5, tags: Optional[List[str]] = None ) -> str: """ Generate context string for LLM consumption. Filters by relevance and confidence, sorted by net score. Args: task_type: Type of task (for filtering) max_items: Maximum items to include min_confidence: Minimum confidence threshold tags: Optional tags to filter by Returns: Formatted context string for LLM """ relevant_items = [ item for item in self.items.values() if item.confidence >= min_confidence ] # Filter by tags if provided if tags: relevant_items = [ item for item in relevant_items if any(tag in item.tags for tag in tags) ] # Sort by net score (most helpful first) relevant_items.sort(key=lambda x: x.net_score, reverse=True) # Group by category sections: Dict[str, List[str]] = {} for item in relevant_items[:max_items]: cat_name = item.category.name if cat_name not in sections: sections[cat_name] = [] sections[cat_name].append(item.to_context_string()) # Build context string lines = ["## Atomizer Knowledge Playbook", ""] for cat_name, items in sections.items(): lines.append(f"### {cat_name}") lines.extend(items) lines.append("") return "\n".join(lines) def search_by_content( self, query: str, category: Optional[InsightCategory] = None, limit: int = 5 ) -> List[PlaybookItem]: """ Search playbook items by content similarity. Simple keyword matching - could be enhanced with embeddings. Args: query: Search query category: Optional category filter limit: Maximum results Returns: List of matching items sorted by relevance """ query_lower = query.lower() query_words = set(query_lower.split()) scored_items = [] for item in self.items.values(): if category and item.category != category: continue content_lower = item.content.lower() content_words = set(content_lower.split()) # Simple word overlap scoring overlap = len(query_words & content_words) if overlap > 0 or query_lower in content_lower: score = overlap + (1 if query_lower in content_lower else 0) scored_items.append((score, item)) scored_items.sort(key=lambda x: (-x[0], -x[1].net_score)) return [item for _, item in scored_items[:limit]] def get_by_category( self, category: InsightCategory, min_score: int = 0 ) -> List[PlaybookItem]: """Get all items in a category with minimum net score.""" return [ item for item in self.items.values() if item.category == category and item.net_score >= min_score ] def prune_harmful(self, threshold: int = -3) -> int: """ Remove items that have proven consistently harmful. Args: threshold: Net score threshold (items at or below are removed) Returns: Number of items removed """ to_remove = [ item_id for item_id, item in self.items.items() if item.net_score <= threshold ] for item_id in to_remove: del self.items[item_id] if to_remove: self.last_updated = datetime.now().isoformat() self.version += 1 return len(to_remove) def get_stats(self) -> Dict[str, Any]: """Get playbook statistics.""" by_category = {} for item in self.items.values(): cat = item.category.name if cat not in by_category: by_category[cat] = 0 by_category[cat] += 1 scores = [item.net_score for item in self.items.values()] return { "total_items": len(self.items), "by_category": by_category, "version": self.version, "last_updated": self.last_updated, "avg_score": sum(scores) / len(scores) if scores else 0, "max_score": max(scores) if scores else 0, "min_score": min(scores) if scores else 0 } def save(self, path: Path) -> None: """ Persist playbook to JSON. Args: path: File path to save to """ data = { "version": self.version, "last_updated": self.last_updated, "items": {k: v.to_dict() for k, v in self.items.items()} } path.parent.mkdir(parents=True, exist_ok=True) with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2) @classmethod def load(cls, path: Path) -> "AtomizerPlaybook": """ Load playbook from JSON. Args: path: File path to load from Returns: Loaded playbook (or new empty playbook if file doesn't exist) """ if not path.exists(): return cls() with open(path, encoding='utf-8') as f: data = json.load(f) playbook = cls( version=data.get("version", 1), last_updated=data.get("last_updated", datetime.now().isoformat()) ) for item_data in data.get("items", {}).values(): item = PlaybookItem.from_dict(item_data) playbook.items[item.id] = item return playbook # Convenience function for global playbook access _global_playbook: Optional[AtomizerPlaybook] = None _global_playbook_path: Optional[Path] = None def get_playbook(path: Optional[Path] = None) -> AtomizerPlaybook: """ Get the global playbook instance. Args: path: Optional path to load from (uses default if not provided) Returns: The global AtomizerPlaybook instance """ global _global_playbook, _global_playbook_path if path is None: # Default path path = Path(__file__).parents[2] / "knowledge_base" / "playbook.json" if _global_playbook is None or _global_playbook_path != path: _global_playbook = AtomizerPlaybook.load(path) _global_playbook_path = path return _global_playbook def save_playbook() -> None: """Save the global playbook to its path.""" global _global_playbook, _global_playbook_path if _global_playbook is not None and _global_playbook_path is not None: _global_playbook.save(_global_playbook_path)