scope-optimizer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scope/__init__.py ADDED
@@ -0,0 +1,119 @@
1
+ """
2
+ SCOPE: Self-evolving Context Optimization via Prompt Evolution
3
+
4
+ A framework for automatic prompt optimization that learns from agent execution traces.
5
+ SCOPE synthesizes guidelines from execution traces, routes them via dual-stream
6
+ (tactical/strategic), and explores diverse strategies through multiple perspectives.
7
+
8
+ Quick Start:
9
+ ```python
10
+ from scope import SCOPEOptimizer
11
+ from scope.models import create_openai_model
12
+
13
+ # Create model adapter
14
+ model = create_openai_model("gpt-4o-mini")
15
+
16
+ # Initialize optimizer
17
+ optimizer = SCOPEOptimizer(
18
+ synthesizer_model=model,
19
+ exp_path="./scope_data",
20
+ )
21
+
22
+ # Use in your agent loop
23
+ result = await optimizer.on_step_complete(
24
+ agent_name="my_agent",
25
+ agent_role="AI Assistant",
26
+ task="...",
27
+ model_output="...",
28
+ current_system_prompt="...",
29
+ task_id="task_001",
30
+ )
31
+ ```
32
+
33
+ Key Components:
34
+ - SCOPEOptimizer: Main orchestrator for prompt optimization
35
+ - GuidelineSynthesizer: Generates guidelines from execution traces
36
+ - StrategicMemoryStore: Manages persistent cross-task strategic rules
37
+ - GuidelineHistory: Optional history logging for analysis
38
+ - MemoryOptimizer: Optimizes accumulated rules
39
+
40
+ Customization:
41
+ All LLM prompts are centralized in `scope.prompts` for easy customization:
42
+
43
+ ```python
44
+ from scope import prompts
45
+ # View/modify prompts as needed
46
+ print(prompts.ERROR_REFLECTION_PROMPT)
47
+ ```
48
+
49
+ Logging:
50
+ SCOPE uses Python's standard logging module. By default, logging is silent
51
+ (NullHandler). To enable logging:
52
+
53
+ ```python
54
+ import logging
55
+ logging.getLogger("scope").setLevel(logging.INFO)
56
+ logging.getLogger("scope").addHandler(logging.StreamHandler())
57
+ ```
58
+ """
59
+
60
+ import logging
61
+
62
+ __version__ = "0.1.0"
63
+
64
+ # Setup default logger with NullHandler (silent by default, following library best practices)
65
+ logger = logging.getLogger("scope")
66
+ logger.addHandler(logging.NullHandler())
67
+
68
+ # Core components
69
+ from . import prompts
70
+ from .history_store import GuidelineHistory
71
+ from .memory_optimizer import MemoryOptimizer
72
+
73
+ # Model adapters (re-export for convenience)
74
+ from .models import (
75
+ AnthropicAdapter,
76
+ BaseModelAdapter,
77
+ CallableModelAdapter,
78
+ LiteLLMAdapter,
79
+ Message,
80
+ ModelProtocol,
81
+ ModelResponse,
82
+ OpenAIAdapter,
83
+ SyncModelAdapter,
84
+ create_anthropic_model,
85
+ create_litellm_model,
86
+ create_openai_model,
87
+ )
88
+ from .optimizer import SCOPEOptimizer
89
+ from .strategic_store import StrategicMemoryStore
90
+ from .synthesizer import Guideline, GuidelineSynthesizer
91
+
92
+ __all__ = [
93
+ # Main interface
94
+ "SCOPEOptimizer",
95
+ # Guideline synthesis
96
+ "GuidelineSynthesizer",
97
+ "Guideline",
98
+ # Memory stores
99
+ "GuidelineHistory",
100
+ "StrategicMemoryStore",
101
+ # Optimization
102
+ "MemoryOptimizer",
103
+ # Model interface
104
+ "Message",
105
+ "ModelResponse",
106
+ "ModelProtocol",
107
+ "BaseModelAdapter",
108
+ "SyncModelAdapter",
109
+ "CallableModelAdapter",
110
+ # Model adapters
111
+ "OpenAIAdapter",
112
+ "AnthropicAdapter",
113
+ "LiteLLMAdapter",
114
+ "create_openai_model",
115
+ "create_anthropic_model",
116
+ "create_litellm_model",
117
+ # Prompt templates (for customization)
118
+ "prompts",
119
+ ]
scope/history_store.py ADDED
@@ -0,0 +1,256 @@
1
+ """
2
+ GuidelineHistory: Stores guideline generation history for analysis.
3
+
4
+ This module provides optional history logging for SCOPE:
5
+ - Logs all generated guidelines to disk (accepted/rejected)
6
+ - Tracks active rules per task for deduplication
7
+ - Useful for debugging and analyzing guideline generation patterns
8
+
9
+ Note: This is optional and disabled by default (store_history=False).
10
+ """
11
+ import json
12
+ import os
13
+ from datetime import datetime
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from .utils import lock_file, unlock_file
17
+
18
+
19
+ class GuidelineHistory:
20
+ """
21
+ Stores guideline generation history for analysis and debugging.
22
+
23
+ This is an optional component, enabled via store_history=True in SCOPEOptimizer.
24
+
25
+ Structure:
26
+ {exp_path}/prompt_updates/
27
+ ├── {agent_name}.jsonl # One line per guideline
28
+ └── active_rules.json # Currently active rules per task
29
+
30
+ active_rules.json format:
31
+ {
32
+ "task_123": {
33
+ "planning_agent": [{"update_text": "...", ...}],
34
+ ...
35
+ },
36
+ ...
37
+ }
38
+ """
39
+
40
+ def __init__(self, exp_path: str):
41
+ """
42
+ Initialize the history store.
43
+
44
+ Args:
45
+ exp_path: Experiment path (e.g., workdir/hle)
46
+ """
47
+ self.exp_path = exp_path
48
+ self.updates_dir = os.path.join(exp_path, "prompt_updates")
49
+ os.makedirs(self.updates_dir, exist_ok=True)
50
+
51
+ self.active_rules_path = os.path.join(self.updates_dir, "active_rules.json")
52
+ self._active_rules = self._load_active_rules()
53
+
54
+ def _load_active_rules(self) -> Dict[str, List[Dict[str, Any]]]:
55
+ """Load currently active rules from disk."""
56
+ if os.path.exists(self.active_rules_path):
57
+ try:
58
+ with open(self.active_rules_path, encoding='utf-8') as f:
59
+ return json.load(f)
60
+ except Exception:
61
+ pass
62
+ return {}
63
+
64
+ def _save_active_rules(self):
65
+ """Save active rules to disk, merging with existing data for concurrent safety."""
66
+ try:
67
+ # Use file locking to prevent concurrent write conflicts
68
+ # Open in read-write mode to allow locking
69
+ mode = 'r+' if os.path.exists(self.active_rules_path) else 'w'
70
+
71
+ with open(self.active_rules_path, mode, encoding='utf-8') as f:
72
+ try:
73
+ # Acquire exclusive lock (cross-platform)
74
+ lock_file(f)
75
+
76
+ # Read current content from disk
77
+ if mode == 'r+':
78
+ f.seek(0)
79
+ try:
80
+ disk_rules = json.load(f)
81
+ except (json.JSONDecodeError, ValueError):
82
+ disk_rules = {}
83
+ else:
84
+ disk_rules = {}
85
+
86
+ # Merge our rules with disk rules (deep merge)
87
+ for task_id, agents in self._active_rules.items():
88
+ if task_id not in disk_rules:
89
+ disk_rules[task_id] = {}
90
+ for agent_name, rules in agents.items():
91
+ if agent_name not in disk_rules[task_id]:
92
+ disk_rules[task_id][agent_name] = []
93
+
94
+ # Merge rules, avoiding duplicates
95
+ existing_texts = {r.get("update_text", "").strip().lower()
96
+ for r in disk_rules[task_id][agent_name]}
97
+ for rule in rules:
98
+ rule_text = rule.get("update_text", "").strip().lower()
99
+ if rule_text not in existing_texts:
100
+ disk_rules[task_id][agent_name].append(rule)
101
+ existing_texts.add(rule_text)
102
+
103
+ # Write merged data back
104
+ f.seek(0)
105
+ f.truncate()
106
+ json.dump(disk_rules, f, indent=2, ensure_ascii=False)
107
+
108
+ finally:
109
+ # Release lock (cross-platform)
110
+ unlock_file(f)
111
+
112
+ except Exception as e:
113
+ print(f"[GuidelineHistory] Failed to save active rules: {e}")
114
+
115
+ def add_update(
116
+ self,
117
+ agent_name: str,
118
+ update_text: str,
119
+ rationale: str,
120
+ error_type: str,
121
+ task_id: Optional[str] = None,
122
+ scope: str = "session",
123
+ confidence: str = "medium",
124
+ ) -> bool:
125
+ """
126
+ Add a new prompt update.
127
+
128
+ Args:
129
+ agent_name: Name of the agent this update applies to
130
+ update_text: The actual prompt addition
131
+ rationale: Why this update was created
132
+ error_type: Type of error that triggered this
133
+ task_id: Optional task ID
134
+ scope: session | experiment | persistent
135
+ confidence: low | medium | high
136
+
137
+ Returns:
138
+ True if added successfully, False otherwise
139
+ """
140
+ try:
141
+ # Create entry
142
+ entry = {
143
+ "timestamp": datetime.now().isoformat(),
144
+ "agent_name": agent_name,
145
+ "update_text": update_text,
146
+ "rationale": rationale,
147
+ "error_type": error_type,
148
+ "task_id": task_id,
149
+ "scope": scope,
150
+ "confidence": confidence,
151
+ }
152
+
153
+ # Append to history file
154
+ history_file = os.path.join(self.updates_dir, f"{agent_name}.jsonl")
155
+ with open(history_file, 'a', encoding='utf-8') as f:
156
+ f.write(json.dumps(entry, ensure_ascii=False) + '\n')
157
+
158
+ # Add to active rules (task-aware, with deduplication)
159
+ if task_id: # Only track active rules for tasks with IDs
160
+ # Reload from disk to get latest state (for concurrent execution)
161
+ self._active_rules = self._load_active_rules()
162
+
163
+ if task_id not in self._active_rules:
164
+ self._active_rules[task_id] = {}
165
+
166
+ if agent_name not in self._active_rules[task_id]:
167
+ self._active_rules[task_id][agent_name] = []
168
+
169
+ # Check if similar rule already exists (simple text match within this task/agent)
170
+ normalized_text = update_text.strip().lower()
171
+ task_agent_rules = self._active_rules[task_id][agent_name]
172
+ if not any(r.get("update_text", "").strip().lower() == normalized_text
173
+ for r in task_agent_rules):
174
+ self._active_rules[task_id][agent_name].append({
175
+ "update_text": update_text,
176
+ "rationale": rationale,
177
+ "timestamp": entry["timestamp"],
178
+ "confidence": confidence,
179
+ })
180
+ self._save_active_rules()
181
+ return True
182
+ else:
183
+ print(f"[GuidelineHistory] Duplicate rule detected for {agent_name} (task {task_id}), skipping")
184
+ return False
185
+
186
+ return True # If no task_id, still log to history but don't track active rules
187
+
188
+ except Exception as e:
189
+ print(f"[GuidelineHistory] Error adding update: {e}")
190
+ return False
191
+
192
+ def get_active_rules(self, agent_name: str, task_id: Optional[str] = None, max_rules: int = 5) -> List[str]:
193
+ """
194
+ Get active prompt rules for an agent in a specific task.
195
+
196
+ Args:
197
+ agent_name: Name of the agent
198
+ task_id: Task ID (if None, returns empty list since rules are now task-specific)
199
+ max_rules: Maximum number of rules to return
200
+
201
+ Returns:
202
+ List of rule texts (most recent first)
203
+ """
204
+ # Reload from disk to get latest state (for concurrent execution)
205
+ self._active_rules = self._load_active_rules()
206
+
207
+ if not task_id or task_id not in self._active_rules:
208
+ return []
209
+
210
+ rules = self._active_rules[task_id].get(agent_name, [])
211
+
212
+ # Sort by timestamp (most recent first) and take top max_rules
213
+ sorted_rules = sorted(rules, key=lambda x: x.get("timestamp", ""), reverse=True)
214
+ return [r["update_text"] for r in sorted_rules[:max_rules]]
215
+
216
+ def get_all_history(self, agent_name: str) -> List[Dict[str, Any]]:
217
+ """Get all historical updates for an agent."""
218
+ history_file = os.path.join(self.updates_dir, f"{agent_name}.jsonl")
219
+
220
+ if not os.path.exists(history_file):
221
+ return []
222
+
223
+ history = []
224
+ try:
225
+ with open(history_file, encoding='utf-8') as f:
226
+ for line in f:
227
+ try:
228
+ history.append(json.loads(line.strip()))
229
+ except json.JSONDecodeError:
230
+ continue
231
+ except Exception as e:
232
+ print(f"[GuidelineHistory] Error reading history: {e}")
233
+
234
+ return history
235
+
236
+ def clear_active_rules(self, agent_name: str):
237
+ """Clear all active rules for an agent (useful for testing/reset)."""
238
+ if agent_name in self._active_rules:
239
+ self._active_rules[agent_name] = []
240
+ self._save_active_rules()
241
+
242
+ def get_statistics(self) -> Dict[str, Any]:
243
+ """Get statistics about prompt updates."""
244
+ stats = {
245
+ "total_agents": len(self._active_rules),
246
+ "agents": {}
247
+ }
248
+
249
+ for agent_name in self._active_rules:
250
+ history = self.get_all_history(agent_name)
251
+ stats["agents"][agent_name] = {
252
+ "active_rules_count": len(self._active_rules[agent_name]),
253
+ "total_updates": len(history),
254
+ }
255
+
256
+ return stats