433 lines
14 KiB
Python
433 lines
14 KiB
Python
|
|
"""
|
||
|
|
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)
|