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 +119 -0
- scope/history_store.py +256 -0
- scope/memory_optimizer.py +657 -0
- scope/models/__init__.py +77 -0
- scope/models/anthropic_adapter.py +198 -0
- scope/models/base.py +253 -0
- scope/models/litellm_adapter.py +191 -0
- scope/models/openai_adapter.py +182 -0
- scope/optimizer.py +660 -0
- scope/prompts.py +378 -0
- scope/strategic_store.py +386 -0
- scope/synthesizer.py +542 -0
- scope/utils.py +58 -0
- scope_optimizer-0.1.0.dist-info/METADATA +380 -0
- scope_optimizer-0.1.0.dist-info/RECORD +31 -0
- scope_optimizer-0.1.0.dist-info/WHEEL +5 -0
- scope_optimizer-0.1.0.dist-info/licenses/LICENSE +22 -0
- scope_optimizer-0.1.0.dist-info/top_level.txt +2 -0
- scope_saved/__init__.py +119 -0
- scope_saved/history_store.py +256 -0
- scope_saved/memory_optimizer.py +657 -0
- scope_saved/models/__init__.py +77 -0
- scope_saved/models/anthropic_adapter.py +198 -0
- scope_saved/models/base.py +253 -0
- scope_saved/models/litellm_adapter.py +191 -0
- scope_saved/models/openai_adapter.py +182 -0
- scope_saved/optimizer.py +660 -0
- scope_saved/prompts.py +378 -0
- scope_saved/strategic_store.py +386 -0
- scope_saved/synthesizer.py +542 -0
- scope_saved/utils.py +58 -0
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
|