claude-evolve 1.9.5 → 1.9.6
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.
|
Binary file
|
|
Binary file
|
package/lib/ai_cli.py
CHANGED
|
@@ -5,11 +5,13 @@ AIDEV-NOTE: This keeps ai-cli.sh as the source of truth for model configs and ti
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import random
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import tempfile
|
|
12
|
+
import time
|
|
11
13
|
from pathlib import Path
|
|
12
|
-
from typing import Optional, Tuple
|
|
14
|
+
from typing import Optional, Tuple, List
|
|
13
15
|
|
|
14
16
|
# Path to ai-cli.sh relative to this file
|
|
15
17
|
SCRIPT_DIR = Path(__file__).parent
|
|
@@ -155,6 +157,178 @@ def call_ai(
|
|
|
155
157
|
raise AIError(f"Failed to call AI: {e}")
|
|
156
158
|
|
|
157
159
|
|
|
160
|
+
def get_models_for_command(command: str) -> List[str]:
|
|
161
|
+
"""
|
|
162
|
+
Get the list of available models for a command.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
command: Either "run" or "ideate"
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of model names
|
|
169
|
+
"""
|
|
170
|
+
bash_script = f'''
|
|
171
|
+
source "{SCRIPT_DIR}/config.sh"
|
|
172
|
+
load_config
|
|
173
|
+
case "$1" in
|
|
174
|
+
run) echo "$LLM_RUN" ;;
|
|
175
|
+
ideate) echo "$LLM_IDEATE" ;;
|
|
176
|
+
esac
|
|
177
|
+
'''
|
|
178
|
+
|
|
179
|
+
result = subprocess.run(
|
|
180
|
+
["bash", "-c", bash_script, "bash", command],
|
|
181
|
+
capture_output=True,
|
|
182
|
+
text=True
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if result.returncode != 0:
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
model_list = result.stdout.strip()
|
|
189
|
+
if not model_list:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
return model_list.split()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def call_ai_model(
|
|
196
|
+
prompt: str,
|
|
197
|
+
model_name: str,
|
|
198
|
+
working_dir: Optional[str] = None,
|
|
199
|
+
env_vars: Optional[dict] = None
|
|
200
|
+
) -> Tuple[str, str]:
|
|
201
|
+
"""
|
|
202
|
+
Call a specific AI model.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
prompt: The prompt to send to the AI
|
|
206
|
+
model_name: The specific model to use
|
|
207
|
+
working_dir: Directory to run the command in
|
|
208
|
+
env_vars: Additional environment variables
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Tuple of (output, model_name)
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
TimeoutError, RateLimitError, APIExhaustedError, AIError
|
|
215
|
+
"""
|
|
216
|
+
bash_script = f'''
|
|
217
|
+
source "{SCRIPT_DIR}/config.sh"
|
|
218
|
+
load_config
|
|
219
|
+
source "{AI_CLI_PATH}"
|
|
220
|
+
call_ai_model_configured "$1" "$2"
|
|
221
|
+
'''
|
|
222
|
+
|
|
223
|
+
env = os.environ.copy()
|
|
224
|
+
if working_dir:
|
|
225
|
+
env['CLAUDE_EVOLVE_WORKING_DIR'] = working_dir
|
|
226
|
+
if env_vars:
|
|
227
|
+
env.update(env_vars)
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
result = subprocess.run(
|
|
231
|
+
["bash", "-c", bash_script, "bash", model_name, prompt],
|
|
232
|
+
capture_output=True,
|
|
233
|
+
text=True,
|
|
234
|
+
cwd=working_dir,
|
|
235
|
+
env=env
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
output = result.stdout
|
|
239
|
+
stderr = result.stderr
|
|
240
|
+
exit_code = result.returncode
|
|
241
|
+
|
|
242
|
+
# Print stderr (contains debug info)
|
|
243
|
+
if stderr:
|
|
244
|
+
for line in stderr.strip().split('\n'):
|
|
245
|
+
if line:
|
|
246
|
+
print(f" {line}", file=sys.stderr)
|
|
247
|
+
|
|
248
|
+
# Handle exit codes
|
|
249
|
+
if exit_code == 124:
|
|
250
|
+
raise TimeoutError(f"AI call timed out (model: {model_name})")
|
|
251
|
+
elif exit_code == 2:
|
|
252
|
+
raise RateLimitError(f"Rate limit hit (model: {model_name})")
|
|
253
|
+
elif exit_code == 3:
|
|
254
|
+
raise APIExhaustedError(f"API quota exhausted (model: {model_name})")
|
|
255
|
+
elif exit_code != 0:
|
|
256
|
+
raise AIError(f"AI call failed with exit code {exit_code}: {stderr}")
|
|
257
|
+
|
|
258
|
+
return output, model_name
|
|
259
|
+
|
|
260
|
+
except subprocess.SubprocessError as e:
|
|
261
|
+
raise AIError(f"Failed to call AI: {e}")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def call_ai_with_backoff(
|
|
265
|
+
prompt: str,
|
|
266
|
+
command: str = "ideate",
|
|
267
|
+
working_dir: Optional[str] = None,
|
|
268
|
+
env_vars: Optional[dict] = None,
|
|
269
|
+
max_rounds: int = 10,
|
|
270
|
+
initial_wait: int = 60,
|
|
271
|
+
max_wait: int = 600
|
|
272
|
+
) -> Tuple[str, str]:
|
|
273
|
+
"""
|
|
274
|
+
Call AI with round-based retries and exponential backoff.
|
|
275
|
+
|
|
276
|
+
AIDEV-NOTE: This is the robust retry mechanism for handling rate limits.
|
|
277
|
+
- Tries each model in the pool (shuffled order)
|
|
278
|
+
- If all models fail in a round, waits with exponential backoff
|
|
279
|
+
- Keeps going until success or max_rounds exhausted
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
prompt: The prompt to send
|
|
283
|
+
command: "run" or "ideate" - determines model pool
|
|
284
|
+
working_dir: Directory for file operations
|
|
285
|
+
env_vars: Additional environment variables
|
|
286
|
+
max_rounds: Maximum number of full rounds to attempt
|
|
287
|
+
initial_wait: Initial wait time in seconds after first failed round
|
|
288
|
+
max_wait: Maximum wait time in seconds between rounds
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Tuple of (output, model_name)
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
AIError: If all rounds exhausted without success
|
|
295
|
+
"""
|
|
296
|
+
models = get_models_for_command(command)
|
|
297
|
+
if not models:
|
|
298
|
+
raise AIError(f"No models configured for command: {command}")
|
|
299
|
+
|
|
300
|
+
wait_time = initial_wait
|
|
301
|
+
last_errors = {}
|
|
302
|
+
|
|
303
|
+
for round_num in range(max_rounds):
|
|
304
|
+
# Shuffle models each round for fairness
|
|
305
|
+
shuffled_models = models.copy()
|
|
306
|
+
random.shuffle(shuffled_models)
|
|
307
|
+
|
|
308
|
+
print(f"[AI] Round {round_num + 1}/{max_rounds}: trying {len(shuffled_models)} models", file=sys.stderr)
|
|
309
|
+
|
|
310
|
+
for model in shuffled_models:
|
|
311
|
+
try:
|
|
312
|
+
output, model_name = call_ai_model(prompt, model, working_dir, env_vars)
|
|
313
|
+
if round_num > 0:
|
|
314
|
+
print(f"[AI] Succeeded on round {round_num + 1} with {model}", file=sys.stderr)
|
|
315
|
+
return output, model_name
|
|
316
|
+
except AIError as e:
|
|
317
|
+
last_errors[model] = str(e)
|
|
318
|
+
# Continue to next model
|
|
319
|
+
|
|
320
|
+
# All models failed in this round
|
|
321
|
+
if round_num < max_rounds - 1:
|
|
322
|
+
print(f"[AI] All models failed in round {round_num + 1}, waiting {wait_time}s before retry...", file=sys.stderr)
|
|
323
|
+
time.sleep(wait_time)
|
|
324
|
+
# Exponential backoff: 60 -> 120 -> 240 -> 480 (capped at max_wait)
|
|
325
|
+
wait_time = min(wait_time * 2, max_wait)
|
|
326
|
+
|
|
327
|
+
# All rounds exhausted
|
|
328
|
+
error_summary = "; ".join(f"{m}: {e[:50]}" for m, e in list(last_errors.items())[:3])
|
|
329
|
+
raise AIError(f"All {max_rounds} rounds exhausted. Last errors: {error_summary}")
|
|
330
|
+
|
|
331
|
+
|
|
158
332
|
def call_ai_for_file_edit(
|
|
159
333
|
prompt: str,
|
|
160
334
|
file_path: str,
|
package/lib/evolve_ideate.py
CHANGED
|
@@ -23,7 +23,7 @@ SCRIPT_DIR = Path(__file__).parent
|
|
|
23
23
|
sys.path.insert(0, str(SCRIPT_DIR.parent))
|
|
24
24
|
|
|
25
25
|
from lib.evolution_csv import EvolutionCSV
|
|
26
|
-
from lib.ai_cli import
|
|
26
|
+
from lib.ai_cli import call_ai_with_backoff, get_git_protection_warning, AIError
|
|
27
27
|
from lib.embedding import check_novelty as check_embedding_novelty, get_embedding, set_cache_file, save_cache
|
|
28
28
|
|
|
29
29
|
|
|
@@ -47,6 +47,13 @@ class IdeationConfig:
|
|
|
47
47
|
novelty_enabled: bool = True
|
|
48
48
|
novelty_threshold: float = 0.92
|
|
49
49
|
|
|
50
|
+
# Retry configuration with exponential backoff
|
|
51
|
+
# AIDEV-NOTE: This implements round-based retries like the shell version.
|
|
52
|
+
# Each round tries ALL models. If all fail, wait and retry.
|
|
53
|
+
max_rounds: int = 10 # Max full rounds of all models
|
|
54
|
+
initial_wait: int = 60 # Seconds to wait after first failed round
|
|
55
|
+
max_wait: int = 600 # Max wait between rounds (10 minutes)
|
|
56
|
+
|
|
50
57
|
|
|
51
58
|
@dataclass
|
|
52
59
|
class Idea:
|
|
@@ -85,8 +92,13 @@ class IdeationStrategy(ABC):
|
|
|
85
92
|
"""Build the AI prompt."""
|
|
86
93
|
pass
|
|
87
94
|
|
|
88
|
-
def generate(self, context: IdeationContext, count: int
|
|
89
|
-
|
|
95
|
+
def generate(self, context: IdeationContext, count: int,
|
|
96
|
+
max_rounds: int = 10, initial_wait: int = 60, max_wait: int = 600) -> List[Idea]:
|
|
97
|
+
"""Generate ideas using this strategy with round-based retry and backoff.
|
|
98
|
+
|
|
99
|
+
AIDEV-NOTE: Uses call_ai_with_backoff for robust retry handling.
|
|
100
|
+
Each round tries ALL models. If all fail, waits with exponential backoff.
|
|
101
|
+
"""
|
|
90
102
|
if count <= 0:
|
|
91
103
|
return []
|
|
92
104
|
|
|
@@ -110,8 +122,15 @@ class IdeationStrategy(ABC):
|
|
|
110
122
|
# Build prompt
|
|
111
123
|
prompt = self.build_prompt(context, ids, temp_csv.name)
|
|
112
124
|
|
|
113
|
-
# Call AI
|
|
114
|
-
output, model =
|
|
125
|
+
# Call AI with round-based retry and backoff
|
|
126
|
+
output, model = call_ai_with_backoff(
|
|
127
|
+
prompt,
|
|
128
|
+
command="ideate",
|
|
129
|
+
working_dir=self.config.evolution_dir,
|
|
130
|
+
max_rounds=max_rounds,
|
|
131
|
+
initial_wait=initial_wait,
|
|
132
|
+
max_wait=max_wait
|
|
133
|
+
)
|
|
115
134
|
|
|
116
135
|
# Parse results from modified CSV
|
|
117
136
|
ideas = self._parse_results(temp_csv, ids)
|
|
@@ -120,12 +139,15 @@ class IdeationStrategy(ABC):
|
|
|
120
139
|
# Record model used
|
|
121
140
|
for idea in ideas:
|
|
122
141
|
idea.strategy = f"{self.name} ({model})"
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
return ideas
|
|
143
|
+
else:
|
|
144
|
+
print(f"[IDEATE] AI completed but no ideas parsed from output", file=sys.stderr)
|
|
145
|
+
return []
|
|
125
146
|
|
|
126
147
|
except AIError as e:
|
|
127
|
-
print(f"[IDEATE]
|
|
148
|
+
print(f"[IDEATE] All retries exhausted in {self.name}: {e}", file=sys.stderr)
|
|
128
149
|
return []
|
|
150
|
+
|
|
129
151
|
finally:
|
|
130
152
|
temp_csv.unlink(missing_ok=True)
|
|
131
153
|
|
|
@@ -382,7 +404,12 @@ class Ideator:
|
|
|
382
404
|
if count <= 0:
|
|
383
405
|
continue
|
|
384
406
|
|
|
385
|
-
ideas = strategy.generate(
|
|
407
|
+
ideas = strategy.generate(
|
|
408
|
+
context, count,
|
|
409
|
+
max_rounds=self.config.max_rounds,
|
|
410
|
+
initial_wait=self.config.initial_wait,
|
|
411
|
+
max_wait=self.config.max_wait
|
|
412
|
+
)
|
|
386
413
|
|
|
387
414
|
if ideas:
|
|
388
415
|
strategies_succeeded += 1
|
|
@@ -472,7 +499,10 @@ def load_config(config_path: Optional[str] = None) -> IdeationConfig:
|
|
|
472
499
|
crossover_hybrid=ideation.get('crossover_hybrid', 4),
|
|
473
500
|
num_elites=ideation.get('num_elites', 3),
|
|
474
501
|
novelty_enabled=novelty.get('enabled', True),
|
|
475
|
-
novelty_threshold=novelty.get('threshold', 0.92)
|
|
502
|
+
novelty_threshold=novelty.get('threshold', 0.92),
|
|
503
|
+
max_rounds=ideation.get('max_rounds', 10),
|
|
504
|
+
initial_wait=ideation.get('initial_wait', 60),
|
|
505
|
+
max_wait=ideation.get('max_wait', 600)
|
|
476
506
|
)
|
|
477
507
|
|
|
478
508
|
|