claude-evolve 1.7.23 → 1.7.26
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.
- package/bin/claude-evolve-worker +39 -9
- package/package.json +1 -1
- package/bin/claude-evolve-ideate-py +0 -97
- package/lib/ideation_helper.py +0 -745
package/bin/claude-evolve-worker
CHANGED
|
@@ -28,17 +28,32 @@ cleanup_on_exit() {
|
|
|
28
28
|
echo "[WORKER-$$] Worker terminated while processing $CURRENT_CANDIDATE_ID" >&2
|
|
29
29
|
# Grab current status; only reset if it is not already a failure or complete
|
|
30
30
|
echo "[WORKER-$$] Determining status before resetting" >&2
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
local reset_output
|
|
33
|
+
local reset_exit_code
|
|
34
|
+
reset_output=$("$PYTHON_CMD" -c "
|
|
32
35
|
import sys
|
|
33
36
|
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
34
37
|
from lib.evolution_csv import EvolutionCSV
|
|
35
38
|
csv = EvolutionCSV('$FULL_CSV_PATH')
|
|
36
39
|
info = csv.get_candidate_info('$CURRENT_CANDIDATE_ID')
|
|
37
40
|
status = info.get('status', '').lower() if info else ''
|
|
41
|
+
print(f'Current status: {status}', file=sys.stderr)
|
|
38
42
|
# Do not reset if already failed, failed-ai-retry, or complete
|
|
39
|
-
|
|
43
|
+
if status and status not in ('complete', 'failed', 'failed-ai-retry', 'failed-parent-missing'):
|
|
44
|
+
print(f'Resetting {status} -> pending', file=sys.stderr)
|
|
40
45
|
csv.update_candidate_status('$CURRENT_CANDIDATE_ID', 'pending')
|
|
41
|
-
|
|
46
|
+
else:
|
|
47
|
+
print(f'Status {status} does not need reset', file=sys.stderr)
|
|
48
|
+
" 2>&1)
|
|
49
|
+
reset_exit_code=$?
|
|
50
|
+
|
|
51
|
+
if [[ $reset_exit_code -ne 0 ]]; then
|
|
52
|
+
echo "[WORKER-$$] Warning: Failed to reset status for $CURRENT_CANDIDATE_ID (exit code $reset_exit_code)" >&2
|
|
53
|
+
echo "[WORKER-$$] Error details: $reset_output" >&2
|
|
54
|
+
else
|
|
55
|
+
echo "[WORKER-$$] Status check result: $reset_output" >&2
|
|
56
|
+
fi
|
|
42
57
|
fi
|
|
43
58
|
}
|
|
44
59
|
|
|
@@ -596,19 +611,34 @@ with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
|
596
611
|
csv.update_candidate_status('$candidate_id', 'failed-ai-retry')
|
|
597
612
|
" 2>/dev/null || true
|
|
598
613
|
elif [[ $process_exit_code -eq 78 ]]; then
|
|
599
|
-
# Missing parent; mark as failed-parent
|
|
600
|
-
echo "[WORKER-$$] Parent missing for $candidate_id
|
|
614
|
+
# Missing parent; mark child as failed and auto-recover parent
|
|
615
|
+
echo "[WORKER-$$] Parent '$parent_id' missing for $candidate_id"
|
|
616
|
+
echo "[WORKER-$$] Marking $candidate_id as failed-parent-missing"
|
|
617
|
+
echo "[WORKER-$$] Auto-recovering: marking parent '$parent_id' as pending"
|
|
618
|
+
|
|
601
619
|
"$PYTHON_CMD" -c "
|
|
602
620
|
import sys
|
|
603
621
|
sys.path.insert(0, '$SCRIPT_DIR/..')
|
|
604
622
|
from lib.evolution_csv import EvolutionCSV
|
|
605
623
|
with EvolutionCSV('$FULL_CSV_PATH') as csv:
|
|
624
|
+
# Mark child as failed
|
|
606
625
|
csv.update_candidate_status('$candidate_id', 'failed-parent-missing')
|
|
607
|
-
|
|
608
|
-
#
|
|
626
|
+
|
|
627
|
+
# Auto-recover: mark parent as pending so it gets processed
|
|
628
|
+
parent_info = csv.get_candidate_info('$parent_id')
|
|
629
|
+
if parent_info:
|
|
630
|
+
parent_status = parent_info.get('status', '').lower()
|
|
631
|
+
if parent_status in ('', 'skipped', 'failed-parent-missing'):
|
|
632
|
+
csv.update_candidate_status('$parent_id', 'pending')
|
|
633
|
+
print(f'Auto-recovered parent $parent_id: {parent_status} -> pending', file=sys.stderr)
|
|
634
|
+
else:
|
|
635
|
+
print(f'Parent $parent_id has status: {parent_status} (not auto-recovering)', file=sys.stderr)
|
|
636
|
+
else:
|
|
637
|
+
print(f'Warning: parent $parent_id not found in CSV', file=sys.stderr)
|
|
638
|
+
" 2>&1 | grep -E "Auto-recovered|Parent.*status|Warning" || true
|
|
639
|
+
|
|
640
|
+
# Clear current candidate and continue to next (don't break)
|
|
609
641
|
CURRENT_CANDIDATE_ID=""
|
|
610
|
-
# Stop looping so we don't keep retrying for missing dependency
|
|
611
|
-
break
|
|
612
642
|
else
|
|
613
643
|
echo "[WORKER-$$] Failed to process $candidate_id"
|
|
614
644
|
# Other failures (evaluation errors, etc) mark as failed
|
package/package.json
CHANGED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Python-based ideation wrapper for claude-evolve
|
|
3
|
-
# This is a simple wrapper that calls the Python ideation helper
|
|
4
|
-
|
|
5
|
-
set -e
|
|
6
|
-
|
|
7
|
-
# Load configuration to ensure paths are set up
|
|
8
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
-
# shellcheck source=../lib/config.sh
|
|
10
|
-
source "$SCRIPT_DIR/../lib/config.sh"
|
|
11
|
-
|
|
12
|
-
# Use CLAUDE_EVOLVE_CONFIG if set, otherwise default
|
|
13
|
-
if [[ -n ${CLAUDE_EVOLVE_CONFIG:-} ]]; then
|
|
14
|
-
load_config "$CLAUDE_EVOLVE_CONFIG"
|
|
15
|
-
else
|
|
16
|
-
# Check if config.yaml exists in current directory
|
|
17
|
-
if [[ -f "config.yaml" ]]; then
|
|
18
|
-
CONFIG_FILE="$(pwd)/config.yaml"
|
|
19
|
-
load_config "$CONFIG_FILE"
|
|
20
|
-
else
|
|
21
|
-
load_config
|
|
22
|
-
fi
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
# Setup logging to file
|
|
26
|
-
if [[ -n "${FULL_EVOLUTION_DIR:-}" ]]; then
|
|
27
|
-
LOG_DIR="$FULL_EVOLUTION_DIR/logs"
|
|
28
|
-
mkdir -p "$LOG_DIR"
|
|
29
|
-
LOG_FILE="$LOG_DIR/ideate-py-$$-$(date +%Y%m%d-%H%M%S).log"
|
|
30
|
-
|
|
31
|
-
# Log to both terminal and file with timestamps
|
|
32
|
-
exec > >(while IFS= read -r line; do echo "$(date '+%Y-%m-%d %H:%M:%S'): $line"; done | tee -a "$LOG_FILE") 2>&1
|
|
33
|
-
echo "[IDEATE-PY-$$] Logging to: $LOG_FILE"
|
|
34
|
-
fi
|
|
35
|
-
|
|
36
|
-
# Parse arguments
|
|
37
|
-
use_strategies=true
|
|
38
|
-
|
|
39
|
-
while [[ $# -gt 0 ]]; do
|
|
40
|
-
case $1 in
|
|
41
|
-
--help)
|
|
42
|
-
cat <<EOF
|
|
43
|
-
claude-evolve ideate (Python version) - Generate new algorithm ideas
|
|
44
|
-
|
|
45
|
-
USAGE:
|
|
46
|
-
claude-evolve ideate-py [--legacy N]
|
|
47
|
-
|
|
48
|
-
OPTIONS:
|
|
49
|
-
--legacy N Use legacy mode with N ideas (ignores strategy config)
|
|
50
|
-
--help Show this help message
|
|
51
|
-
|
|
52
|
-
DESCRIPTION:
|
|
53
|
-
Python-based ideation that uses MD5 hashing to verify AI actually modified files.
|
|
54
|
-
This solves the problem where AIs claim they edited files but didn't.
|
|
55
|
-
|
|
56
|
-
Uses multi-strategy evolutionary approach:
|
|
57
|
-
- Novel exploration: Pure creativity, global search
|
|
58
|
-
- Hill climbing: Parameter tuning of top performers
|
|
59
|
-
- Structural mutation: Algorithmic changes to top performers
|
|
60
|
-
- Crossover hybrid: Combine successful approaches
|
|
61
|
-
|
|
62
|
-
Strategy distribution is configured in evolution/config.yaml
|
|
63
|
-
EOF
|
|
64
|
-
exit 0
|
|
65
|
-
;;
|
|
66
|
-
--legacy)
|
|
67
|
-
use_strategies=false
|
|
68
|
-
shift
|
|
69
|
-
if [[ $1 =~ ^[0-9]+$ ]]; then
|
|
70
|
-
export LEGACY_IDEA_COUNT=$1
|
|
71
|
-
shift
|
|
72
|
-
else
|
|
73
|
-
echo "[ERROR] --legacy requires a number" >&2
|
|
74
|
-
exit 1
|
|
75
|
-
fi
|
|
76
|
-
;;
|
|
77
|
-
*)
|
|
78
|
-
echo "[ERROR] Unknown option: $1" >&2
|
|
79
|
-
exit 1
|
|
80
|
-
;;
|
|
81
|
-
esac
|
|
82
|
-
done
|
|
83
|
-
|
|
84
|
-
# Check workspace using config
|
|
85
|
-
if [[ ! -d "$FULL_EVOLUTION_DIR" ]]; then
|
|
86
|
-
echo "[ERROR] Evolution workspace not found: $FULL_EVOLUTION_DIR. Run 'claude-evolve setup' first." >&2
|
|
87
|
-
exit 1
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
# Call Python helper
|
|
91
|
-
echo "[INFO] Using Python ideation helper (more reliable AI result verification)"
|
|
92
|
-
|
|
93
|
-
if [[ $use_strategies == true ]]; then
|
|
94
|
-
exec "$PYTHON_CMD" "$SCRIPT_DIR/../lib/ideation_helper.py" run
|
|
95
|
-
else
|
|
96
|
-
exec "$PYTHON_CMD" "$SCRIPT_DIR/../lib/ideation_helper.py" run --legacy "$LEGACY_IDEA_COUNT"
|
|
97
|
-
fi
|
package/lib/ideation_helper.py
DELETED
|
@@ -1,745 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Python ideation helper for claude-evolve.
|
|
4
|
-
Replaces the complex shell script logic in claude-evolve-ideate.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
import sys
|
|
9
|
-
import csv
|
|
10
|
-
import re
|
|
11
|
-
import subprocess
|
|
12
|
-
import tempfile
|
|
13
|
-
import shutil
|
|
14
|
-
import time
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import List, Tuple, Optional, Dict
|
|
17
|
-
from dataclasses import dataclass
|
|
18
|
-
|
|
19
|
-
from evolution_csv import EvolutionCSV
|
|
20
|
-
from config import Config
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class IdeationStrategy:
|
|
25
|
-
"""Configuration for a single ideation strategy."""
|
|
26
|
-
name: str
|
|
27
|
-
count: int
|
|
28
|
-
parent_required: bool = False
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class IdeationHelper:
|
|
32
|
-
"""Handles all ideation logic for claude-evolve."""
|
|
33
|
-
|
|
34
|
-
def __init__(self, config: Config):
|
|
35
|
-
"""Initialize with configuration."""
|
|
36
|
-
self.config = config
|
|
37
|
-
self.working_dir = Path(config.working_dir)
|
|
38
|
-
self.csv_path = self.working_dir / config.data['csv_file']
|
|
39
|
-
self.brief_path = self.working_dir / config.data['brief_file']
|
|
40
|
-
self.output_dir = self.working_dir / (config.data.get('output_dir') or '')
|
|
41
|
-
|
|
42
|
-
# Ideation config
|
|
43
|
-
ideation = config.data.get('ideation', {})
|
|
44
|
-
self.total_ideas = ideation.get('total_ideas', 15)
|
|
45
|
-
self.novel_exploration = ideation.get('novel_exploration', 3)
|
|
46
|
-
self.hill_climbing = ideation.get('hill_climbing', 5)
|
|
47
|
-
self.structural_mutation = ideation.get('structural_mutation', 3)
|
|
48
|
-
self.crossover_hybrid = ideation.get('crossover_hybrid', 4)
|
|
49
|
-
self.num_elites = ideation.get('num_elites', 3)
|
|
50
|
-
self.num_revolution = ideation.get('num_revolution', 2)
|
|
51
|
-
|
|
52
|
-
# Python command
|
|
53
|
-
self.python_cmd = config.data.get('python_cmd', 'python3')
|
|
54
|
-
|
|
55
|
-
# Paths to library scripts
|
|
56
|
-
self.script_dir = Path(__file__).parent.parent / 'bin'
|
|
57
|
-
self.lib_dir = Path(__file__).parent
|
|
58
|
-
|
|
59
|
-
def get_next_generation(self) -> str:
|
|
60
|
-
"""
|
|
61
|
-
Get the next generation number that doesn't have existing Python files.
|
|
62
|
-
Returns formatted generation string like '01', '02', etc.
|
|
63
|
-
"""
|
|
64
|
-
# Start with generation 1
|
|
65
|
-
start_gen = 1
|
|
66
|
-
|
|
67
|
-
if self.csv_path.exists():
|
|
68
|
-
# Find max generation from CSV
|
|
69
|
-
with EvolutionCSV(str(self.csv_path)) as csv_ops:
|
|
70
|
-
rows = csv_ops._read_csv()
|
|
71
|
-
max_gen = 0
|
|
72
|
-
|
|
73
|
-
for row in rows[1:]: # Skip header
|
|
74
|
-
if row and len(row) > 0:
|
|
75
|
-
id_field = row[0].strip()
|
|
76
|
-
if id_field.startswith('gen') and '-' in id_field:
|
|
77
|
-
try:
|
|
78
|
-
gen_part = id_field.split('-')[0] # e.g., 'gen01'
|
|
79
|
-
gen_num = int(gen_part[3:]) # Extract number after 'gen'
|
|
80
|
-
max_gen = max(max_gen, gen_num)
|
|
81
|
-
except (ValueError, IndexError):
|
|
82
|
-
pass
|
|
83
|
-
|
|
84
|
-
start_gen = max_gen + 1
|
|
85
|
-
|
|
86
|
-
# Keep incrementing until we find a generation with no Python files
|
|
87
|
-
candidate_gen = start_gen
|
|
88
|
-
while candidate_gen < 1000: # Safety limit
|
|
89
|
-
gen_formatted = f"{candidate_gen:02d}"
|
|
90
|
-
|
|
91
|
-
# Check if any Python files exist for this generation
|
|
92
|
-
py_pattern = self.output_dir / f"evolution_gen{gen_formatted}-*.py"
|
|
93
|
-
if not list(self.output_dir.glob(f"evolution_gen{gen_formatted}-*.py")):
|
|
94
|
-
return gen_formatted
|
|
95
|
-
|
|
96
|
-
print(f"[WARN] Generation {gen_formatted} already has Python files, skipping",
|
|
97
|
-
file=sys.stderr)
|
|
98
|
-
candidate_gen += 1
|
|
99
|
-
|
|
100
|
-
raise RuntimeError("Could not find a safe generation number (checked up to 999)")
|
|
101
|
-
|
|
102
|
-
def get_next_id(self, generation: str) -> str:
|
|
103
|
-
"""Get the next available ID for the given generation."""
|
|
104
|
-
if not self.csv_path.exists():
|
|
105
|
-
return f"gen{generation}-001"
|
|
106
|
-
|
|
107
|
-
max_id = 0
|
|
108
|
-
pattern = re.compile(rf'^gen{generation}-(\d+)$')
|
|
109
|
-
|
|
110
|
-
with EvolutionCSV(str(self.csv_path)) as csv_ops:
|
|
111
|
-
rows = csv_ops._read_csv()
|
|
112
|
-
for row in rows[1:]: # Skip header
|
|
113
|
-
if row and len(row) > 0:
|
|
114
|
-
id_field = row[0].strip()
|
|
115
|
-
match = pattern.match(id_field)
|
|
116
|
-
if match:
|
|
117
|
-
id_num = int(match.group(1))
|
|
118
|
-
max_id = max(max_id, id_num)
|
|
119
|
-
|
|
120
|
-
return f"gen{generation}-{max_id + 1:03d}"
|
|
121
|
-
|
|
122
|
-
def get_next_ids(self, generation: str, count: int) -> List[str]:
|
|
123
|
-
"""Get the next N available IDs for the given generation."""
|
|
124
|
-
if not self.csv_path.exists():
|
|
125
|
-
start_id = 1
|
|
126
|
-
else:
|
|
127
|
-
max_id = 0
|
|
128
|
-
pattern = re.compile(rf'^gen{generation}-(\d+)$')
|
|
129
|
-
|
|
130
|
-
with EvolutionCSV(str(self.csv_path)) as csv_ops:
|
|
131
|
-
rows = csv_ops._read_csv()
|
|
132
|
-
for row in rows[1:]: # Skip header
|
|
133
|
-
if row and len(row) > 0:
|
|
134
|
-
id_field = row[0].strip()
|
|
135
|
-
match = pattern.match(id_field)
|
|
136
|
-
if match:
|
|
137
|
-
id_num = int(match.group(1))
|
|
138
|
-
max_id = max(max_id, id_num)
|
|
139
|
-
|
|
140
|
-
start_id = max_id + 1
|
|
141
|
-
|
|
142
|
-
return [f"gen{generation}-{start_id + i:03d}" for i in range(count)]
|
|
143
|
-
|
|
144
|
-
def get_top_performers(self, num_requested: int) -> List[Tuple[str, str, float]]:
|
|
145
|
-
"""
|
|
146
|
-
Get top performing algorithms.
|
|
147
|
-
Returns list of (id, description, score) tuples.
|
|
148
|
-
"""
|
|
149
|
-
if not self.csv_path.exists():
|
|
150
|
-
return []
|
|
151
|
-
|
|
152
|
-
with EvolutionCSV(str(self.csv_path)) as csv_ops:
|
|
153
|
-
rows = csv_ops._read_csv()
|
|
154
|
-
|
|
155
|
-
completed = []
|
|
156
|
-
novel = []
|
|
157
|
-
|
|
158
|
-
# Skip header
|
|
159
|
-
for row in rows[1:]:
|
|
160
|
-
if len(row) >= 5 and row[3] and row[4] == 'complete':
|
|
161
|
-
try:
|
|
162
|
-
candidate_id = row[0]
|
|
163
|
-
parent_id = row[1] if len(row) > 1 else ''
|
|
164
|
-
description = row[2] if len(row) > 2 else ''
|
|
165
|
-
score = float(row[3])
|
|
166
|
-
|
|
167
|
-
completed.append((candidate_id, description, score))
|
|
168
|
-
|
|
169
|
-
# Track novel candidates separately
|
|
170
|
-
if not parent_id:
|
|
171
|
-
novel.append((candidate_id, description, score))
|
|
172
|
-
except ValueError:
|
|
173
|
-
pass
|
|
174
|
-
|
|
175
|
-
# Sort by score (descending)
|
|
176
|
-
completed.sort(key=lambda x: x[2], reverse=True)
|
|
177
|
-
novel.sort(key=lambda x: x[2], reverse=True)
|
|
178
|
-
|
|
179
|
-
# Collect top performers
|
|
180
|
-
selected_ids = set()
|
|
181
|
-
results = []
|
|
182
|
-
|
|
183
|
-
# Add top absolute performers
|
|
184
|
-
for candidate_id, description, score in completed[:num_requested]:
|
|
185
|
-
results.append((candidate_id, description, score))
|
|
186
|
-
selected_ids.add(candidate_id)
|
|
187
|
-
|
|
188
|
-
# Add top novel candidates (if not already selected)
|
|
189
|
-
for candidate_id, description, score in novel:
|
|
190
|
-
if candidate_id not in selected_ids and len([r for r in results if r[0] not in selected_ids]) < self.num_revolution:
|
|
191
|
-
results.append((candidate_id, description, score))
|
|
192
|
-
selected_ids.add(candidate_id)
|
|
193
|
-
|
|
194
|
-
return results
|
|
195
|
-
|
|
196
|
-
def call_ai_model(self, model_name: str, prompt: str, timeout: int = 300) -> Tuple[str, int]:
|
|
197
|
-
"""
|
|
198
|
-
Call an AI model using subprocess.
|
|
199
|
-
Returns (output, exit_code).
|
|
200
|
-
"""
|
|
201
|
-
# Map model names to commands
|
|
202
|
-
model_commands = {
|
|
203
|
-
'opus': ['claude', '--dangerously-skip-permissions', '--mcp-config', '', '--model', 'opus', '-p', prompt],
|
|
204
|
-
'sonnet': ['claude', '--dangerously-skip-permissions', '--mcp-config', '', '--model', 'sonnet', '-p', prompt],
|
|
205
|
-
'haiku': ['claude', '--dangerously-skip-permissions', '--mcp-config', '', '--model', 'haiku', '-p', prompt],
|
|
206
|
-
'gemini-pro': ['gemini', '-y', '-m', 'gemini-2.5-pro', '-p', prompt],
|
|
207
|
-
'gemini-flash': ['gemini', '-y', '-m', 'gemini-2.5-flash', '-p', prompt],
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if model_name not in model_commands:
|
|
211
|
-
return f"Unknown model: {model_name}", 1
|
|
212
|
-
|
|
213
|
-
try:
|
|
214
|
-
result = subprocess.run(
|
|
215
|
-
model_commands[model_name],
|
|
216
|
-
capture_output=True,
|
|
217
|
-
text=True,
|
|
218
|
-
timeout=timeout
|
|
219
|
-
)
|
|
220
|
-
return result.stdout + result.stderr, result.returncode
|
|
221
|
-
except subprocess.TimeoutExpired:
|
|
222
|
-
return f"Model {model_name} timed out after {timeout}s", 124
|
|
223
|
-
except Exception as e:
|
|
224
|
-
return f"Error calling model {model_name}: {e}", 1
|
|
225
|
-
|
|
226
|
-
def prepopulate_csv_stubs(self, temp_csv: Path, ids: List[str],
|
|
227
|
-
placeholder_text: str, parent_id: str = "") -> None:
|
|
228
|
-
"""Pre-populate CSV with stub rows containing placeholders."""
|
|
229
|
-
print(f"[INFO] Pre-populating CSV with stub rows: {' '.join(ids)}")
|
|
230
|
-
|
|
231
|
-
with open(temp_csv, 'a') as f:
|
|
232
|
-
for id_str in ids:
|
|
233
|
-
f.write(f'{id_str},{parent_id},"{placeholder_text}",,pending\n')
|
|
234
|
-
|
|
235
|
-
def validate_csv_modification(self, temp_csv: Path, expected_count: int,
|
|
236
|
-
original_count: int, required_ids: List[str]) -> bool:
|
|
237
|
-
"""
|
|
238
|
-
Validate that AI correctly modified the CSV.
|
|
239
|
-
Returns True if valid, False otherwise.
|
|
240
|
-
"""
|
|
241
|
-
if not temp_csv.exists():
|
|
242
|
-
print("[ERROR] CSV file was not found after AI modification", file=sys.stderr)
|
|
243
|
-
return False
|
|
244
|
-
|
|
245
|
-
# Count data rows (skip header, skip empty lines)
|
|
246
|
-
with open(temp_csv, 'r') as f:
|
|
247
|
-
lines = [line for line in f if line.strip()]
|
|
248
|
-
new_count = len(lines) - 1 # Subtract header
|
|
249
|
-
|
|
250
|
-
if new_count < original_count:
|
|
251
|
-
print(f"[ERROR] AI overwrote CSV instead of editing ({new_count} < {original_count})",
|
|
252
|
-
file=sys.stderr)
|
|
253
|
-
return False
|
|
254
|
-
|
|
255
|
-
if new_count != original_count:
|
|
256
|
-
print(f"[ERROR] Row count changed ({new_count} != {original_count})", file=sys.stderr)
|
|
257
|
-
return False
|
|
258
|
-
|
|
259
|
-
# Check that no placeholders remain
|
|
260
|
-
with open(temp_csv, 'r') as f:
|
|
261
|
-
content = f.read()
|
|
262
|
-
if 'PLACEHOLDER' in content:
|
|
263
|
-
placeholder_count = content.count('PLACEHOLDER')
|
|
264
|
-
print(f"[ERROR] {placeholder_count} placeholders remain unfilled", file=sys.stderr)
|
|
265
|
-
return False
|
|
266
|
-
|
|
267
|
-
# Validate IDs are correct
|
|
268
|
-
with open(temp_csv, 'r') as f:
|
|
269
|
-
reader = csv.reader(f)
|
|
270
|
-
next(reader) # Skip header
|
|
271
|
-
rows = list(reader)
|
|
272
|
-
actual_ids = [row[0].strip('"') for row in rows[-len(required_ids):]]
|
|
273
|
-
|
|
274
|
-
if actual_ids != required_ids:
|
|
275
|
-
print(f"[ERROR] AI used wrong IDs!", file=sys.stderr)
|
|
276
|
-
print(f"[ERROR] Expected: {required_ids}", file=sys.stderr)
|
|
277
|
-
print(f"[ERROR] Got: {actual_ids}", file=sys.stderr)
|
|
278
|
-
return False
|
|
279
|
-
|
|
280
|
-
print(f"[INFO] ✓ AI correctly used the specified IDs: {' '.join(actual_ids)}")
|
|
281
|
-
return True
|
|
282
|
-
|
|
283
|
-
def apply_csv_changes(self, temp_csv: Path, added_count: int,
|
|
284
|
-
ai_model: str) -> bool:
|
|
285
|
-
"""
|
|
286
|
-
Apply changes from temp CSV to main CSV with proper locking.
|
|
287
|
-
"""
|
|
288
|
-
print("[INFO] Acquiring CSV lock to apply changes...")
|
|
289
|
-
|
|
290
|
-
try:
|
|
291
|
-
with EvolutionCSV(str(self.csv_path)) as csv_ops:
|
|
292
|
-
# Get the original line count
|
|
293
|
-
with open(self.csv_path, 'r') as f:
|
|
294
|
-
original_lines = len(f.readlines())
|
|
295
|
-
|
|
296
|
-
# Append only the new lines from temp CSV
|
|
297
|
-
with open(temp_csv, 'r') as f_temp:
|
|
298
|
-
temp_lines = f_temp.readlines()
|
|
299
|
-
new_lines = temp_lines[original_lines:]
|
|
300
|
-
|
|
301
|
-
with open(self.csv_path, 'a') as f_main:
|
|
302
|
-
f_main.writelines(new_lines)
|
|
303
|
-
|
|
304
|
-
# Update idea-LLM field for newly added rows
|
|
305
|
-
if ai_model:
|
|
306
|
-
print(f"[INFO] Recording that {ai_model} generated the ideas")
|
|
307
|
-
# Get IDs of newly added rows
|
|
308
|
-
reader = csv.reader(new_lines)
|
|
309
|
-
for row in reader:
|
|
310
|
-
if row and row[0] and row[0] != 'id':
|
|
311
|
-
candidate_id = row[0].strip('"')
|
|
312
|
-
csv_ops.update_candidate_field(candidate_id, 'idea-LLM', ai_model)
|
|
313
|
-
|
|
314
|
-
print(f"[INFO] Successfully added {added_count} ideas to CSV")
|
|
315
|
-
return True
|
|
316
|
-
|
|
317
|
-
except Exception as e:
|
|
318
|
-
print(f"[ERROR] Failed to apply CSV changes: {e}", file=sys.stderr)
|
|
319
|
-
return False
|
|
320
|
-
|
|
321
|
-
def call_ai_for_ideation(self, prompt: str, generation: str, expected_count: int,
|
|
322
|
-
temp_csv_path: Path) -> Optional[str]:
|
|
323
|
-
"""
|
|
324
|
-
Call AI models in round-robin fashion until one successfully fills placeholders.
|
|
325
|
-
|
|
326
|
-
This is the critical function that addresses the main problem:
|
|
327
|
-
- Takes MD5 hash of CSV before calling AI
|
|
328
|
-
- Takes MD5 hash after AI completes
|
|
329
|
-
- Only accepts result if:
|
|
330
|
-
1. File actually changed (hash is different)
|
|
331
|
-
2. Row count stayed the same (no adds/deletes)
|
|
332
|
-
3. All PLACEHOLDERs are gone (AI filled them in)
|
|
333
|
-
|
|
334
|
-
Returns the model name that succeeded, or None if all failed.
|
|
335
|
-
"""
|
|
336
|
-
import hashlib
|
|
337
|
-
|
|
338
|
-
# Get models for ideation (hardcoded for now, could be from config)
|
|
339
|
-
models = ['sonnet', 'gemini-pro', 'opus']
|
|
340
|
-
|
|
341
|
-
# Calculate starting index for round-robin
|
|
342
|
-
gen_num = int(generation.lstrip('0')) if generation.lstrip('0') else 1
|
|
343
|
-
start_index = gen_num % len(models)
|
|
344
|
-
|
|
345
|
-
# Create ordered list based on round-robin
|
|
346
|
-
ordered_models = [models[(start_index + i) % len(models)] for i in range(len(models))]
|
|
347
|
-
|
|
348
|
-
print(f"[AI] Model order for ideate (round-robin): {' '.join(ordered_models)}",
|
|
349
|
-
file=sys.stderr)
|
|
350
|
-
|
|
351
|
-
# Get original CSV count and hash BEFORE calling AI
|
|
352
|
-
with open(temp_csv_path, 'rb') as f:
|
|
353
|
-
original_content = f.read()
|
|
354
|
-
original_hash = hashlib.md5(original_content).hexdigest()
|
|
355
|
-
original_lines = [line for line in original_content.decode('utf-8').split('\n') if line.strip()]
|
|
356
|
-
original_count = len(original_lines) - 1 # Subtract header
|
|
357
|
-
|
|
358
|
-
print(f"[DEBUG] Pre-AI state: {original_count} rows, hash={original_hash[:8]}...", file=sys.stderr)
|
|
359
|
-
|
|
360
|
-
# Backup temp CSV
|
|
361
|
-
temp_csv_backup = Path(str(temp_csv_path) + '.backup')
|
|
362
|
-
shutil.copy(temp_csv_path, temp_csv_backup)
|
|
363
|
-
|
|
364
|
-
# Try each model
|
|
365
|
-
for model in ordered_models:
|
|
366
|
-
print(f"[AI] Attempting ideate with {model}", file=sys.stderr)
|
|
367
|
-
|
|
368
|
-
# Restore temp CSV from backup before each attempt
|
|
369
|
-
shutil.copy(temp_csv_backup, temp_csv_path)
|
|
370
|
-
|
|
371
|
-
# Re-hash after restore to ensure we start from known state
|
|
372
|
-
with open(temp_csv_path, 'rb') as f:
|
|
373
|
-
pre_call_hash = hashlib.md5(f.read()).hexdigest()
|
|
374
|
-
|
|
375
|
-
print(f"[DEBUG] Before {model}: hash={pre_call_hash[:8]}...", file=sys.stderr)
|
|
376
|
-
|
|
377
|
-
# Call the model
|
|
378
|
-
output, exit_code = self.call_ai_model(model, prompt)
|
|
379
|
-
|
|
380
|
-
# CRITICAL: Hash the file AFTER the AI call to detect if it actually changed
|
|
381
|
-
with open(temp_csv_path, 'rb') as f:
|
|
382
|
-
post_call_content = f.read()
|
|
383
|
-
post_call_hash = hashlib.md5(post_call_content).hexdigest()
|
|
384
|
-
post_call_lines = [line for line in post_call_content.decode('utf-8').split('\n') if line.strip()]
|
|
385
|
-
new_count = len(post_call_lines) - 1 # Subtract header
|
|
386
|
-
|
|
387
|
-
print(f"[DEBUG] After {model}: {new_count} rows, hash={post_call_hash[:8]}...", file=sys.stderr)
|
|
388
|
-
|
|
389
|
-
# Check 1: Did the file actually change?
|
|
390
|
-
if post_call_hash == pre_call_hash:
|
|
391
|
-
print(f"[WARN] {model} CLAIMED to edit but file hash is IDENTICAL - file was NOT modified!",
|
|
392
|
-
file=sys.stderr)
|
|
393
|
-
print(f"[DEBUG] AI output (last 500 chars): ...{output[-500:]}", file=sys.stderr)
|
|
394
|
-
continue
|
|
395
|
-
|
|
396
|
-
print(f"[INFO] ✓ {model} actually modified the file (hash changed)", file=sys.stderr)
|
|
397
|
-
|
|
398
|
-
# Check 2: Row count should stay the same (we're editing, not adding)
|
|
399
|
-
if new_count != original_count:
|
|
400
|
-
print(f"[WARN] {model} changed row count ({original_count} -> {new_count}) - should only edit, not add/delete",
|
|
401
|
-
file=sys.stderr)
|
|
402
|
-
continue
|
|
403
|
-
|
|
404
|
-
print(f"[INFO] ✓ Row count unchanged ({new_count} rows)", file=sys.stderr)
|
|
405
|
-
|
|
406
|
-
# Check 3: All placeholders should be gone
|
|
407
|
-
content_str = post_call_content.decode('utf-8')
|
|
408
|
-
placeholder_count = content_str.count('PLACEHOLDER')
|
|
409
|
-
|
|
410
|
-
if placeholder_count > 0:
|
|
411
|
-
print(f"[WARN] {model} left {placeholder_count} PLACEHOLDERs unfilled - trying next model",
|
|
412
|
-
file=sys.stderr)
|
|
413
|
-
# Show which rows still have placeholders for debugging
|
|
414
|
-
for i, line in enumerate(content_str.split('\n')):
|
|
415
|
-
if 'PLACEHOLDER' in line:
|
|
416
|
-
print(f"[DEBUG] Row {i} still has PLACEHOLDER: {line[:100]}", file=sys.stderr)
|
|
417
|
-
continue
|
|
418
|
-
|
|
419
|
-
print(f"[INFO] ✓ All {expected_count} placeholders filled", file=sys.stderr)
|
|
420
|
-
|
|
421
|
-
# All checks passed! This is a successful edit
|
|
422
|
-
print(f"[INFO] CSV successfully modified by {model}: filled {expected_count} placeholder rows ✓",
|
|
423
|
-
file=sys.stderr)
|
|
424
|
-
|
|
425
|
-
# Run CSV fixer to ensure proper formatting
|
|
426
|
-
fixed_csv = Path(str(temp_csv_path) + '.fixed')
|
|
427
|
-
result = subprocess.run(
|
|
428
|
-
[self.python_cmd, str(self.lib_dir / 'csv_fixer.py'),
|
|
429
|
-
str(temp_csv_path), str(fixed_csv)],
|
|
430
|
-
capture_output=True
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
if result.returncode == 0:
|
|
434
|
-
shutil.move(fixed_csv, temp_csv_path)
|
|
435
|
-
print("[INFO] CSV format validated and fixed if needed", file=sys.stderr)
|
|
436
|
-
else:
|
|
437
|
-
print(f"[WARN] CSV fixer failed, using original: {result.stderr.decode()}", file=sys.stderr)
|
|
438
|
-
|
|
439
|
-
# Cleanup backup
|
|
440
|
-
temp_csv_backup.unlink()
|
|
441
|
-
return model
|
|
442
|
-
|
|
443
|
-
# All models failed
|
|
444
|
-
temp_csv_backup.unlink()
|
|
445
|
-
print("[ERROR] All AI models failed to generate ideas", file=sys.stderr)
|
|
446
|
-
print("[ERROR] Either they claimed to edit but didn't, or they added/removed rows instead of editing",
|
|
447
|
-
file=sys.stderr)
|
|
448
|
-
return None
|
|
449
|
-
|
|
450
|
-
def generate_strategy_ideas(self, strategy_name: str, count: int, generation: str,
|
|
451
|
-
top_performers: List[Tuple[str, str, float]]) -> bool:
|
|
452
|
-
"""
|
|
453
|
-
Generate ideas for a specific strategy.
|
|
454
|
-
Returns True if successful, False otherwise.
|
|
455
|
-
"""
|
|
456
|
-
print(f"[INFO] Generating {count} {strategy_name} ideas")
|
|
457
|
-
|
|
458
|
-
# Get next IDs
|
|
459
|
-
next_ids = self.get_next_ids(generation, count)
|
|
460
|
-
print(f"[INFO] Using IDs: {' '.join(next_ids)}")
|
|
461
|
-
|
|
462
|
-
# Create temp CSV
|
|
463
|
-
temp_csv = self.working_dir / f"temp-csv-{os.getpid()}.csv"
|
|
464
|
-
shutil.copy(self.csv_path, temp_csv)
|
|
465
|
-
|
|
466
|
-
# Pre-populate with stubs
|
|
467
|
-
placeholder_map = {
|
|
468
|
-
'novel': '[PLACEHOLDER: Replace this with your algorithmic idea]',
|
|
469
|
-
'hill-climbing': '[PLACEHOLDER: Replace with parameter tuning idea]',
|
|
470
|
-
'structural': '[PLACEHOLDER: Replace with structural modification idea]',
|
|
471
|
-
'crossover': '[PLACEHOLDER: Replace with crossover hybrid idea]'
|
|
472
|
-
}
|
|
473
|
-
placeholder_text = placeholder_map.get(strategy_name, '[PLACEHOLDER]')
|
|
474
|
-
|
|
475
|
-
# Get parent ID for non-novel strategies
|
|
476
|
-
parent_id = ""
|
|
477
|
-
if strategy_name != 'novel' and top_performers:
|
|
478
|
-
parent_id = top_performers[0][0]
|
|
479
|
-
|
|
480
|
-
self.prepopulate_csv_stubs(temp_csv, next_ids, placeholder_text, parent_id)
|
|
481
|
-
|
|
482
|
-
# Count lines for offset calculation
|
|
483
|
-
with open(temp_csv, 'r') as f:
|
|
484
|
-
total_lines = len(f.readlines())
|
|
485
|
-
read_offset = max(1, total_lines - 25)
|
|
486
|
-
|
|
487
|
-
# Build prompt based on strategy
|
|
488
|
-
prompt = self._build_strategy_prompt(
|
|
489
|
-
strategy_name, count, generation, next_ids,
|
|
490
|
-
top_performers, temp_csv.name, total_lines, read_offset
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
# Change to working directory
|
|
494
|
-
original_cwd = os.getcwd()
|
|
495
|
-
os.chdir(self.working_dir)
|
|
496
|
-
|
|
497
|
-
try:
|
|
498
|
-
# Call AI
|
|
499
|
-
successful_model = self.call_ai_for_ideation(
|
|
500
|
-
prompt, generation, count, temp_csv
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
if not successful_model:
|
|
504
|
-
return False
|
|
505
|
-
|
|
506
|
-
# Validate
|
|
507
|
-
with open(self.csv_path, 'r') as f:
|
|
508
|
-
original_count = len([line for line in f if line.strip()]) - 1
|
|
509
|
-
|
|
510
|
-
if not self.validate_csv_modification(temp_csv, count, original_count + count, next_ids):
|
|
511
|
-
return False
|
|
512
|
-
|
|
513
|
-
# Apply changes
|
|
514
|
-
return self.apply_csv_changes(temp_csv, count, successful_model)
|
|
515
|
-
|
|
516
|
-
finally:
|
|
517
|
-
os.chdir(original_cwd)
|
|
518
|
-
if temp_csv.exists():
|
|
519
|
-
temp_csv.unlink()
|
|
520
|
-
|
|
521
|
-
def _build_strategy_prompt(self, strategy_name: str, count: int, generation: str,
|
|
522
|
-
next_ids: List[str], top_performers: List[Tuple[str, str, float]],
|
|
523
|
-
temp_csv_name: str, total_lines: int, read_offset: int) -> str:
|
|
524
|
-
"""Build the AI prompt for a specific strategy."""
|
|
525
|
-
|
|
526
|
-
# Common header
|
|
527
|
-
prompt = f"""I need you to use your file editing capabilities to fill in PLACEHOLDER descriptions in the CSV file: {temp_csv_name}
|
|
528
|
-
|
|
529
|
-
THE FILE HAS {total_lines} TOTAL LINES. Read from line {read_offset} to see the placeholder rows at the end.
|
|
530
|
-
|
|
531
|
-
⚠️ CRITICAL FILE READING INSTRUCTIONS ⚠️
|
|
532
|
-
THE CSV FILE IS VERY LARGE (OVER 100,000 TOKENS). YOU WILL RUN OUT OF CONTEXT IF YOU READ IT ALL!
|
|
533
|
-
- DO NOT read the entire file or you will exceed context limits and CRASH
|
|
534
|
-
- Use: Read(file_path='{temp_csv_name}', offset={read_offset}, limit=25)
|
|
535
|
-
- This will read ONLY the last 25 lines where the placeholders are
|
|
536
|
-
- DO NOT READ FROM OFFSET 0 - that will load the entire huge file and fail!
|
|
537
|
-
|
|
538
|
-
CRITICAL TASK:
|
|
539
|
-
The CSV file already contains {count} stub rows with these IDs: {' '.join(next_ids)}
|
|
540
|
-
Each stub row has a PLACEHOLDER description.
|
|
541
|
-
Your job is to REPLACE each PLACEHOLDER with a real algorithmic idea description.
|
|
542
|
-
|
|
543
|
-
CRITICAL INSTRUCTIONS:
|
|
544
|
-
1. Read ONLY the last 25 lines using the offset specified above
|
|
545
|
-
2. DO NOT ADD OR DELETE ANY ROWS - only EDIT the placeholder descriptions
|
|
546
|
-
3. DO NOT CHANGE THE IDs - they are already correct
|
|
547
|
-
4. Use the Edit tool to replace EACH PLACEHOLDER text with a real idea
|
|
548
|
-
5. When editing, preserve the CSV structure
|
|
549
|
-
6. ALWAYS wrap descriptions in double quotes
|
|
550
|
-
7. DO NOT use any git commands
|
|
551
|
-
"""
|
|
552
|
-
|
|
553
|
-
# Strategy-specific instructions
|
|
554
|
-
if strategy_name == 'novel':
|
|
555
|
-
prompt += f"""
|
|
556
|
-
8. Focus on creative, ambitious ideas that haven't been tried yet
|
|
557
|
-
9. Consider machine learning, new indicators, regime detection, risk management, etc.
|
|
558
|
-
10. Each description should be one clear sentence describing a novel algorithmic approach
|
|
559
|
-
|
|
560
|
-
Current context:
|
|
561
|
-
- Generation: {generation}
|
|
562
|
-
- Brief: {self._get_brief_preview()}
|
|
563
|
-
"""
|
|
564
|
-
|
|
565
|
-
elif strategy_name == 'hill-climbing':
|
|
566
|
-
valid_parent_ids = ','.join([p[0] for p in top_performers])
|
|
567
|
-
performers_str = '\n'.join([f"{p[0]},{p[1]},{p[2]}" for p in top_performers])
|
|
568
|
-
|
|
569
|
-
prompt += f"""
|
|
570
|
-
8. You MUST use one of these exact parent IDs: {valid_parent_ids}
|
|
571
|
-
9. Generate parameter tuning ideas based on these top performers:
|
|
572
|
-
{performers_str}
|
|
573
|
-
|
|
574
|
-
10. Focus on adjusting specific parameters - include current and new values
|
|
575
|
-
11. Example: "Lower rsi_entry from 21 to 18" or "Increase MA period from 20 to 50"
|
|
576
|
-
12. You may change the parent_id field to reference a different top performer
|
|
577
|
-
"""
|
|
578
|
-
|
|
579
|
-
elif strategy_name == 'structural':
|
|
580
|
-
valid_parent_ids = ','.join([p[0] for p in top_performers])
|
|
581
|
-
performers_str = '\n'.join([f"{p[0]},{p[1]},{p[2]}" for p in top_performers])
|
|
582
|
-
|
|
583
|
-
prompt += f"""
|
|
584
|
-
8. You MUST use one of these exact parent IDs: {valid_parent_ids}
|
|
585
|
-
9. Generate structural modification ideas based on these top performers:
|
|
586
|
-
{performers_str}
|
|
587
|
-
|
|
588
|
-
10. Focus on architectural/structural changes
|
|
589
|
-
11. DO NOT read evolution_*.py files - generate ideas based on descriptions only
|
|
590
|
-
12. You may change the parent_id field to reference a different top performer
|
|
591
|
-
"""
|
|
592
|
-
|
|
593
|
-
elif strategy_name == 'crossover':
|
|
594
|
-
valid_parent_ids = ','.join([p[0] for p in top_performers])
|
|
595
|
-
performers_str = '\n'.join([f"{p[0]},{p[1]},{p[2]}" for p in top_performers])
|
|
596
|
-
|
|
597
|
-
prompt += f"""
|
|
598
|
-
8. You MUST use ONLY these exact parent IDs: {valid_parent_ids}
|
|
599
|
-
9. Combine 2+ algorithms from these top performers:
|
|
600
|
-
{performers_str}
|
|
601
|
-
|
|
602
|
-
10. Each description should combine actual elements from 2+ top performers
|
|
603
|
-
11. Reference at least 2 algorithms in each idea
|
|
604
|
-
12. You may change the parent_id field (choose the primary parent)
|
|
605
|
-
"""
|
|
606
|
-
|
|
607
|
-
return prompt
|
|
608
|
-
|
|
609
|
-
def _get_brief_preview(self) -> str:
|
|
610
|
-
"""Get a preview of the brief file."""
|
|
611
|
-
if not self.brief_path.exists():
|
|
612
|
-
return "No brief file found"
|
|
613
|
-
|
|
614
|
-
with open(self.brief_path, 'r') as f:
|
|
615
|
-
lines = f.readlines()[:5]
|
|
616
|
-
text = ''.join(lines)
|
|
617
|
-
return text[:500] if len(text) > 500 else text
|
|
618
|
-
|
|
619
|
-
def run_ideation(self, generation: str, use_strategies: bool = True) -> bool:
|
|
620
|
-
"""
|
|
621
|
-
Run the full ideation process.
|
|
622
|
-
Returns True if successful, False otherwise.
|
|
623
|
-
"""
|
|
624
|
-
if not self.brief_path.exists():
|
|
625
|
-
print(f"[ERROR] {self.config.data['brief_file']} not found", file=sys.stderr)
|
|
626
|
-
return False
|
|
627
|
-
|
|
628
|
-
# Ensure CSV exists
|
|
629
|
-
if not self.csv_path.exists():
|
|
630
|
-
with open(self.csv_path, 'w') as f:
|
|
631
|
-
f.write("id,basedOnId,description,performance,status,idea-LLM,run-LLM\n")
|
|
632
|
-
|
|
633
|
-
if use_strategies:
|
|
634
|
-
# Validate strategy configuration
|
|
635
|
-
total_check = (self.novel_exploration + self.hill_climbing +
|
|
636
|
-
self.structural_mutation + self.crossover_hybrid)
|
|
637
|
-
if total_check != self.total_ideas:
|
|
638
|
-
print(f"[ERROR] Strategy counts don't sum to total_ideas ({total_check} != {self.total_ideas})",
|
|
639
|
-
file=sys.stderr)
|
|
640
|
-
return False
|
|
641
|
-
|
|
642
|
-
print(f"[INFO] Generating {self.total_ideas} ideas using multi-strategy approach:")
|
|
643
|
-
print(f" Novel exploration: {self.novel_exploration}")
|
|
644
|
-
print(f" Hill climbing: {self.hill_climbing}")
|
|
645
|
-
print(f" Structural mutation: {self.structural_mutation}")
|
|
646
|
-
print(f" Crossover hybrid: {self.crossover_hybrid}")
|
|
647
|
-
|
|
648
|
-
# Get top performers
|
|
649
|
-
top_performers = self.get_top_performers(self.num_elites)
|
|
650
|
-
if not top_performers:
|
|
651
|
-
print("[INFO] No completed algorithms found, will use baseline for hill climbing")
|
|
652
|
-
top_performers = [("000", "Baseline Algorithm (algorithm.py)", 0.0)]
|
|
653
|
-
|
|
654
|
-
# Run each strategy
|
|
655
|
-
strategies_attempted = 0
|
|
656
|
-
strategies_succeeded = 0
|
|
657
|
-
|
|
658
|
-
strategies = [
|
|
659
|
-
('novel', self.novel_exploration, False),
|
|
660
|
-
('hill-climbing', self.hill_climbing, True),
|
|
661
|
-
('structural', self.structural_mutation, True),
|
|
662
|
-
('crossover', self.crossover_hybrid, True),
|
|
663
|
-
]
|
|
664
|
-
|
|
665
|
-
for strategy_name, count, needs_parents in strategies:
|
|
666
|
-
if count > 0:
|
|
667
|
-
strategies_attempted += 1
|
|
668
|
-
parents = top_performers if needs_parents else []
|
|
669
|
-
|
|
670
|
-
if self.generate_strategy_ideas(strategy_name, count, generation, parents):
|
|
671
|
-
strategies_succeeded += 1
|
|
672
|
-
else:
|
|
673
|
-
print(f"[WARN] {strategy_name} strategy failed", file=sys.stderr)
|
|
674
|
-
|
|
675
|
-
print(f"[INFO] Strategy results: {strategies_succeeded}/{strategies_attempted} succeeded")
|
|
676
|
-
|
|
677
|
-
# REQUIRE ALL strategies to succeed
|
|
678
|
-
if strategies_succeeded == strategies_attempted:
|
|
679
|
-
return True
|
|
680
|
-
else:
|
|
681
|
-
print(f"[ERROR] Not all strategies succeeded ({strategies_succeeded}/{strategies_attempted})",
|
|
682
|
-
file=sys.stderr)
|
|
683
|
-
print("[ERROR] Rejecting partial results - will retry with exponential backoff",
|
|
684
|
-
file=sys.stderr)
|
|
685
|
-
return False
|
|
686
|
-
else:
|
|
687
|
-
# Legacy mode - not implemented yet
|
|
688
|
-
print("[ERROR] Legacy mode not implemented in Python helper yet", file=sys.stderr)
|
|
689
|
-
return False
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
def main():
|
|
693
|
-
"""CLI interface for testing."""
|
|
694
|
-
if len(sys.argv) < 2:
|
|
695
|
-
print("Usage: ideation_helper.py <command> [args...]")
|
|
696
|
-
print("Commands:")
|
|
697
|
-
print(" run [--legacy N] - Run ideation")
|
|
698
|
-
sys.exit(1)
|
|
699
|
-
|
|
700
|
-
command = sys.argv[1]
|
|
701
|
-
|
|
702
|
-
# Load config
|
|
703
|
-
config = Config()
|
|
704
|
-
config_path = os.environ.get('CLAUDE_EVOLVE_CONFIG')
|
|
705
|
-
if config_path:
|
|
706
|
-
config.load(config_path)
|
|
707
|
-
elif Path('config.yaml').exists():
|
|
708
|
-
config.load('config.yaml')
|
|
709
|
-
else:
|
|
710
|
-
config.load()
|
|
711
|
-
|
|
712
|
-
helper = IdeationHelper(config)
|
|
713
|
-
|
|
714
|
-
if command == 'run':
|
|
715
|
-
use_strategies = True
|
|
716
|
-
if len(sys.argv) > 2 and sys.argv[2] == '--legacy':
|
|
717
|
-
use_strategies = False
|
|
718
|
-
|
|
719
|
-
generation = helper.get_next_generation()
|
|
720
|
-
print(f"[INFO] Starting ideation for generation {generation}")
|
|
721
|
-
|
|
722
|
-
# Run with retry logic
|
|
723
|
-
retry_count = 0
|
|
724
|
-
wait_seconds = 300 # 5 minutes
|
|
725
|
-
max_wait_seconds = 1800 # 30 minutes
|
|
726
|
-
|
|
727
|
-
while True:
|
|
728
|
-
if helper.run_ideation(generation, use_strategies):
|
|
729
|
-
print(f"[INFO] Ideation complete! Check {helper.csv_path} for new ideas.")
|
|
730
|
-
sys.exit(0)
|
|
731
|
-
|
|
732
|
-
retry_count += 1
|
|
733
|
-
print(f"[WARN] All ideation attempts failed (retry #{retry_count})", file=sys.stderr)
|
|
734
|
-
print(f"[INFO] Waiting {wait_seconds} seconds before retrying...", file=sys.stderr)
|
|
735
|
-
|
|
736
|
-
time.sleep(wait_seconds)
|
|
737
|
-
|
|
738
|
-
wait_seconds = min(wait_seconds * 2, max_wait_seconds)
|
|
739
|
-
else:
|
|
740
|
-
print(f"Unknown command: {command}")
|
|
741
|
-
sys.exit(1)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if __name__ == '__main__':
|
|
745
|
-
main()
|