claude-evolve 1.8.51 → 1.9.0

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.
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ideation module for claude-evolve.
4
+ Generates new algorithm ideas using various strategies.
5
+
6
+ AIDEV-NOTE: This is the Python port of bin/claude-evolve-ideate.
7
+ Includes novelty filtering to prevent near-duplicate ideas.
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+ import re
13
+ import shutil
14
+ import sys
15
+ import tempfile
16
+ from abc import ABC, abstractmethod
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import List, Optional, Dict, Tuple
20
+
21
+ # Add lib to path
22
+ SCRIPT_DIR = Path(__file__).parent
23
+ sys.path.insert(0, str(SCRIPT_DIR.parent))
24
+
25
+ from lib.evolution_csv import EvolutionCSV
26
+ from lib.ai_cli import call_ai, get_git_protection_warning, AIError
27
+ from lib.embedding import check_novelty as check_embedding_novelty, get_embedding, set_cache_file, save_cache
28
+
29
+
30
+ @dataclass
31
+ class IdeationConfig:
32
+ """Configuration for ideation."""
33
+ csv_path: str
34
+ evolution_dir: str
35
+ brief_path: str
36
+ algorithm_path: str
37
+
38
+ # Strategy counts
39
+ total_ideas: int = 15
40
+ novel_exploration: int = 3
41
+ hill_climbing: int = 5
42
+ structural_mutation: int = 3
43
+ crossover_hybrid: int = 4
44
+ num_elites: int = 3
45
+
46
+ # Novelty filtering
47
+ novelty_enabled: bool = True
48
+ novelty_threshold: float = 0.92
49
+
50
+
51
+ @dataclass
52
+ class Idea:
53
+ """A generated idea."""
54
+ id: str
55
+ based_on_id: str
56
+ description: str
57
+ strategy: str
58
+
59
+
60
+ @dataclass
61
+ class IdeationContext:
62
+ """Context for ideation strategies."""
63
+ generation: int
64
+ top_performers: List[Dict]
65
+ brief_content: str
66
+ existing_descriptions: List[str]
67
+ config: IdeationConfig
68
+
69
+
70
+ class IdeationStrategy(ABC):
71
+ """Base class for ideation strategies."""
72
+
73
+ def __init__(self, config: IdeationConfig, csv: EvolutionCSV):
74
+ self.config = config
75
+ self.csv = csv
76
+
77
+ @property
78
+ @abstractmethod
79
+ def name(self) -> str:
80
+ """Strategy name."""
81
+ pass
82
+
83
+ @abstractmethod
84
+ def build_prompt(self, context: IdeationContext, ids: List[str], temp_csv_basename: str) -> str:
85
+ """Build the AI prompt."""
86
+ pass
87
+
88
+ def generate(self, context: IdeationContext, count: int) -> List[Idea]:
89
+ """Generate ideas using this strategy."""
90
+ if count <= 0:
91
+ return []
92
+
93
+ print(f"[IDEATE] Running {self.name} strategy for {count} ideas", file=sys.stderr)
94
+
95
+ # Get next IDs
96
+ ids = self.csv.get_next_ids(context.generation, count)
97
+ print(f"[IDEATE] Using IDs: {', '.join(ids)}", file=sys.stderr)
98
+
99
+ # Create temp CSV with stub rows
100
+ temp_csv = Path(self.config.evolution_dir) / f"temp-csv-{os.getpid()}.csv"
101
+ shutil.copy(self.config.csv_path, temp_csv)
102
+
103
+ # Add stub rows
104
+ with open(temp_csv, 'a') as f:
105
+ for id in ids:
106
+ parent = self._get_default_parent(context)
107
+ f.write(f'{id},{parent},"[PLACEHOLDER: Replace with algorithmic idea]",,pending\n')
108
+
109
+ try:
110
+ # Build prompt
111
+ prompt = self.build_prompt(context, ids, temp_csv.name)
112
+
113
+ # Call AI
114
+ output, model = call_ai(prompt, command="ideate", working_dir=self.config.evolution_dir)
115
+
116
+ # Parse results from modified CSV
117
+ ideas = self._parse_results(temp_csv, ids)
118
+
119
+ if ideas:
120
+ # Record model used
121
+ for idea in ideas:
122
+ idea.strategy = f"{self.name} ({model})"
123
+
124
+ return ideas
125
+
126
+ except AIError as e:
127
+ print(f"[IDEATE] AI error in {self.name}: {e}", file=sys.stderr)
128
+ return []
129
+ finally:
130
+ temp_csv.unlink(missing_ok=True)
131
+
132
+ def _get_default_parent(self, context: IdeationContext) -> str:
133
+ """Get default parent ID for this strategy."""
134
+ if context.top_performers:
135
+ return context.top_performers[0]['id']
136
+ return ""
137
+
138
+ def _parse_results(self, temp_csv: Path, expected_ids: List[str]) -> List[Idea]:
139
+ """Parse ideas from modified CSV."""
140
+ ideas = []
141
+
142
+ with open(temp_csv) as f:
143
+ import csv
144
+ reader = csv.reader(f)
145
+ for row in reader:
146
+ if len(row) >= 3:
147
+ id = row[0].strip().strip('"')
148
+ if id in expected_ids:
149
+ based_on = row[1].strip() if len(row) > 1 else ""
150
+ description = row[2].strip().strip('"')
151
+ # Skip if still placeholder
152
+ if "PLACEHOLDER" not in description and description:
153
+ ideas.append(Idea(
154
+ id=id,
155
+ based_on_id=based_on,
156
+ description=description,
157
+ strategy=self.name
158
+ ))
159
+
160
+ return ideas
161
+
162
+
163
+ class NovelExplorationStrategy(IdeationStrategy):
164
+ """Generate novel, creative ideas not based on existing algorithms."""
165
+
166
+ @property
167
+ def name(self) -> str:
168
+ return "novel_exploration"
169
+
170
+ def _get_default_parent(self, context: IdeationContext) -> str:
171
+ return "" # Novel ideas have no parent
172
+
173
+ def build_prompt(self, context: IdeationContext, ids: List[str], temp_csv_basename: str) -> str:
174
+ return f"""{get_git_protection_warning()}
175
+
176
+ I need you to use your file editing capabilities to fill in PLACEHOLDER descriptions in the CSV file: {temp_csv_basename}
177
+
178
+ Current evolution context:
179
+ - Generation: {context.generation}
180
+ - Brief: {context.brief_content[:500]}
181
+
182
+ CRITICAL TASK:
183
+ The CSV file already contains stub rows with these IDs: {', '.join(ids)}
184
+ Each stub row has a PLACEHOLDER description.
185
+ Your job is to REPLACE each PLACEHOLDER with a real algorithmic idea description.
186
+
187
+ IMPORTANT FILE READING INSTRUCTIONS:
188
+ Read ONLY the last 20-30 lines of the CSV file to see the placeholder rows.
189
+ DO NOT read the entire file - use offset and limit parameters.
190
+
191
+ CRITICAL INSTRUCTIONS:
192
+ 1. Read ONLY the last 20-30 lines of the CSV to see the placeholder rows
193
+ 2. DO NOT ADD OR DELETE ANY ROWS - only EDIT the placeholder descriptions
194
+ 3. DO NOT CHANGE THE IDs - they are already correct
195
+ 4. Use the Edit tool to replace EACH PLACEHOLDER text with a real algorithmic idea
196
+ 5. ALWAYS wrap the description field in double quotes
197
+ 6. Each description should be one clear sentence describing a novel algorithmic approach
198
+ 7. Focus on creative, ambitious ideas that haven't been tried yet
199
+
200
+ IMPORTANT: Use your file editing tools (Edit/MultiEdit) to modify the CSV file directly."""
201
+
202
+
203
+ class HillClimbingStrategy(IdeationStrategy):
204
+ """Generate incremental improvements to top performers."""
205
+
206
+ @property
207
+ def name(self) -> str:
208
+ return "hill_climbing"
209
+
210
+ def build_prompt(self, context: IdeationContext, ids: List[str], temp_csv_basename: str) -> str:
211
+ top_str = "\n".join(
212
+ f" {p['id']}: {p['description'][:100]}... (score: {p['performance']})"
213
+ for p in context.top_performers[:5]
214
+ )
215
+ valid_parents = ",".join(p['id'] for p in context.top_performers[:5])
216
+
217
+ return f"""{get_git_protection_warning()}
218
+
219
+ I need you to use your file editing capabilities to fill in PLACEHOLDER descriptions in the CSV file: {temp_csv_basename}
220
+
221
+ IMPORTANT: You MUST use one of these exact parent IDs: {valid_parents}
222
+
223
+ Successful algorithms to tune:
224
+ {top_str}
225
+
226
+ CRITICAL TASK:
227
+ The CSV file already contains stub rows with these IDs: {', '.join(ids)}
228
+ Your job is to REPLACE each PLACEHOLDER with a parameter tuning idea.
229
+
230
+ INSTRUCTIONS:
231
+ 1. Read ONLY the last 20-30 lines of the CSV file
232
+ 2. Each idea should be a small parameter adjustment or optimization
233
+ 3. Reference which parent you're improving and what specifically you're changing
234
+ 4. DO NOT ADD OR DELETE ANY ROWS - only EDIT the placeholder descriptions
235
+ 5. ALWAYS wrap descriptions in double quotes
236
+ 6. Use the Edit tool to modify the file directly"""
237
+
238
+
239
+ class StructuralMutationStrategy(IdeationStrategy):
240
+ """Generate structural changes to algorithms."""
241
+
242
+ @property
243
+ def name(self) -> str:
244
+ return "structural_mutation"
245
+
246
+ def build_prompt(self, context: IdeationContext, ids: List[str], temp_csv_basename: str) -> str:
247
+ top_str = "\n".join(
248
+ f" {p['id']}: {p['description'][:100]}..."
249
+ for p in context.top_performers[:5]
250
+ )
251
+ valid_parents = ",".join(p['id'] for p in context.top_performers[:5])
252
+
253
+ return f"""{get_git_protection_warning()}
254
+
255
+ I need you to use your file editing capabilities to fill in PLACEHOLDER descriptions in the CSV file: {temp_csv_basename}
256
+
257
+ IMPORTANT: You MUST use one of these exact parent IDs: {valid_parents}
258
+
259
+ Top algorithms for structural changes:
260
+ {top_str}
261
+
262
+ CRITICAL TASK:
263
+ The CSV file already contains stub rows with these IDs: {', '.join(ids)}
264
+ Your job is to REPLACE each PLACEHOLDER with a structural mutation idea.
265
+
266
+ INSTRUCTIONS:
267
+ 1. Read ONLY the last 20-30 lines of the CSV file
268
+ 2. Each idea should involve a significant architectural change
269
+ 3. Examples: adding new features, changing data flow, combining techniques
270
+ 4. DO NOT ADD OR DELETE ANY ROWS - only EDIT the placeholder descriptions
271
+ 5. ALWAYS wrap descriptions in double quotes
272
+ 6. Use the Edit tool to modify the file directly"""
273
+
274
+
275
+ class CrossoverStrategy(IdeationStrategy):
276
+ """Generate crossover ideas combining multiple algorithms."""
277
+
278
+ @property
279
+ def name(self) -> str:
280
+ return "crossover"
281
+
282
+ def build_prompt(self, context: IdeationContext, ids: List[str], temp_csv_basename: str) -> str:
283
+ top_str = "\n".join(
284
+ f" {p['id']}: {p['description'][:100]}..."
285
+ for p in context.top_performers[:5]
286
+ )
287
+ valid_parents = ",".join(p['id'] for p in context.top_performers[:5])
288
+
289
+ return f"""{get_git_protection_warning()}
290
+
291
+ I need you to use your file editing capabilities to fill in PLACEHOLDER descriptions in the CSV file: {temp_csv_basename}
292
+
293
+ IMPORTANT: Reference multiple parents from: {valid_parents}
294
+
295
+ Top algorithms to combine:
296
+ {top_str}
297
+
298
+ CRITICAL TASK:
299
+ The CSV file already contains stub rows with these IDs: {', '.join(ids)}
300
+ Your job is to REPLACE each PLACEHOLDER with a crossover idea.
301
+
302
+ INSTRUCTIONS:
303
+ 1. Read ONLY the last 20-30 lines of the CSV file
304
+ 2. Each idea should combine elements from 2+ top algorithms
305
+ 3. In parent_id, list the main parent (use comma-separated for multiple)
306
+ 4. Describe how you're combining the approaches
307
+ 5. DO NOT ADD OR DELETE ANY ROWS - only EDIT the placeholder descriptions
308
+ 6. ALWAYS wrap descriptions in double quotes
309
+ 7. Use the Edit tool to modify the file directly"""
310
+
311
+
312
+ class Ideator:
313
+ """Main ideation controller."""
314
+
315
+ def __init__(self, config: IdeationConfig):
316
+ self.config = config
317
+ self.csv = EvolutionCSV(config.csv_path)
318
+
319
+ # Initialize embedding cache for novelty filtering
320
+ if config.novelty_enabled:
321
+ cache_path = Path(config.evolution_dir) / "embeddings_cache.json"
322
+ set_cache_file(str(cache_path))
323
+ print(f"[IDEATE] Embedding cache: {cache_path}", file=sys.stderr)
324
+
325
+ # Initialize strategies
326
+ self.strategies = [
327
+ (NovelExplorationStrategy(config, self.csv), config.novel_exploration),
328
+ (HillClimbingStrategy(config, self.csv), config.hill_climbing),
329
+ (StructuralMutationStrategy(config, self.csv), config.structural_mutation),
330
+ (CrossoverStrategy(config, self.csv), config.crossover_hybrid),
331
+ ]
332
+
333
+ def get_context(self) -> IdeationContext:
334
+ """Build ideation context."""
335
+ with EvolutionCSV(self.config.csv_path) as csv:
336
+ top_performers = csv.get_top_performers(self.config.num_elites)
337
+ existing_descriptions = csv.get_all_descriptions()
338
+ generation = csv.get_highest_generation() + 1
339
+
340
+ # Read brief
341
+ brief_content = ""
342
+ if Path(self.config.brief_path).exists():
343
+ brief_content = Path(self.config.brief_path).read_text()[:1000]
344
+
345
+ return IdeationContext(
346
+ generation=generation,
347
+ top_performers=top_performers,
348
+ brief_content=brief_content,
349
+ existing_descriptions=existing_descriptions,
350
+ config=self.config
351
+ )
352
+
353
+ def check_novelty(self, description: str, existing: List[str]) -> Tuple[bool, float]:
354
+ """Check if description is novel enough."""
355
+ if not self.config.novelty_enabled:
356
+ return True, 0.0
357
+
358
+ if not existing:
359
+ return True, 0.0
360
+
361
+ try:
362
+ is_novel, max_sim = check_embedding_novelty(
363
+ description,
364
+ existing,
365
+ threshold=self.config.novelty_threshold
366
+ )
367
+ return is_novel, max_sim
368
+ except Exception as e:
369
+ print(f"[IDEATE] Novelty check failed: {e}", file=sys.stderr)
370
+ return True, 0.0 # Allow if check fails
371
+
372
+ def run(self) -> int:
373
+ """Run ideation. Returns number of ideas generated."""
374
+ context = self.get_context()
375
+ print(f"[IDEATE] Starting generation {context.generation}", file=sys.stderr)
376
+ print(f"[IDEATE] Top performers: {len(context.top_performers)}", file=sys.stderr)
377
+
378
+ all_ideas: List[Idea] = []
379
+ strategies_succeeded = 0
380
+
381
+ for strategy, count in self.strategies:
382
+ if count <= 0:
383
+ continue
384
+
385
+ ideas = strategy.generate(context, count)
386
+
387
+ if ideas:
388
+ strategies_succeeded += 1
389
+
390
+ # Filter for novelty
391
+ novel_ideas = []
392
+ for idea in ideas:
393
+ is_novel, similarity = self.check_novelty(
394
+ idea.description,
395
+ context.existing_descriptions + [i.description for i in all_ideas]
396
+ )
397
+
398
+ if is_novel:
399
+ novel_ideas.append(idea)
400
+ print(f"[IDEATE] Accepted: {idea.id} (sim={similarity:.2%})", file=sys.stderr)
401
+ else:
402
+ print(f"[IDEATE] Rejected (too similar {similarity:.2%}): {idea.description[:50]}...", file=sys.stderr)
403
+
404
+ all_ideas.extend(novel_ideas)
405
+
406
+ # Add ideas to CSV
407
+ if all_ideas:
408
+ with EvolutionCSV(self.config.csv_path) as csv:
409
+ candidates = [
410
+ {
411
+ 'id': idea.id,
412
+ 'basedOnId': idea.based_on_id,
413
+ 'description': idea.description,
414
+ 'status': 'pending',
415
+ 'idea-LLM': idea.strategy
416
+ }
417
+ for idea in all_ideas
418
+ ]
419
+ added = csv.append_candidates(candidates)
420
+ print(f"[IDEATE] Added {added} ideas to CSV", file=sys.stderr)
421
+
422
+ print(f"[IDEATE] Strategies succeeded: {strategies_succeeded}/{len([s for s, c in self.strategies if c > 0])}", file=sys.stderr)
423
+ print(f"[IDEATE] Total ideas generated: {len(all_ideas)}", file=sys.stderr)
424
+
425
+ # Final cache save
426
+ if self.config.novelty_enabled:
427
+ save_cache()
428
+
429
+ return len(all_ideas)
430
+
431
+
432
+ def load_config(config_path: Optional[str] = None) -> IdeationConfig:
433
+ """Load configuration from YAML."""
434
+ import yaml
435
+
436
+ # Find config
437
+ if config_path:
438
+ yaml_path = Path(config_path)
439
+ elif os.environ.get('CLAUDE_EVOLVE_CONFIG'):
440
+ yaml_path = Path(os.environ['CLAUDE_EVOLVE_CONFIG'])
441
+ else:
442
+ yaml_path = Path('evolution/config.yaml')
443
+ if not yaml_path.exists():
444
+ yaml_path = Path('config.yaml')
445
+
446
+ if not yaml_path.exists():
447
+ raise FileNotFoundError(f"Config not found: {yaml_path}")
448
+
449
+ with open(yaml_path) as f:
450
+ data = yaml.safe_load(f) or {}
451
+
452
+ base_dir = yaml_path.parent
453
+
454
+ def resolve(path: str) -> str:
455
+ p = Path(path)
456
+ if not p.is_absolute():
457
+ p = base_dir / p
458
+ return str(p.resolve())
459
+
460
+ ideation = data.get('ideation', {})
461
+ novelty = data.get('novelty', {})
462
+
463
+ return IdeationConfig(
464
+ csv_path=resolve(data.get('csv_file', 'evolution.csv')),
465
+ evolution_dir=str(base_dir.resolve()),
466
+ brief_path=resolve(data.get('brief_file', 'BRIEF.md')),
467
+ algorithm_path=resolve(data.get('algorithm_file', 'algorithm.py')),
468
+ total_ideas=ideation.get('total_ideas', 15),
469
+ novel_exploration=ideation.get('novel_exploration', 3),
470
+ hill_climbing=ideation.get('hill_climbing', 5),
471
+ structural_mutation=ideation.get('structural_mutation', 3),
472
+ crossover_hybrid=ideation.get('crossover_hybrid', 4),
473
+ num_elites=ideation.get('num_elites', 3),
474
+ novelty_enabled=novelty.get('enabled', True),
475
+ novelty_threshold=novelty.get('threshold', 0.92)
476
+ )
477
+
478
+
479
+ def main():
480
+ parser = argparse.ArgumentParser(description='Claude Evolve Ideation')
481
+ parser.add_argument('--config', help='Path to config.yaml')
482
+ parser.add_argument('--count', type=int, help='Override total idea count')
483
+ args = parser.parse_args()
484
+
485
+ try:
486
+ config = load_config(args.config)
487
+
488
+ ideator = Ideator(config)
489
+ count = ideator.run()
490
+
491
+ if count > 0:
492
+ print(f"Generated {count} ideas", file=sys.stderr)
493
+ sys.exit(0)
494
+ else:
495
+ print("No ideas generated", file=sys.stderr)
496
+ sys.exit(1)
497
+
498
+ except FileNotFoundError as e:
499
+ print(f"Error: {e}", file=sys.stderr)
500
+ sys.exit(1)
501
+ except Exception as e:
502
+ print(f"Error: {e}", file=sys.stderr)
503
+ import traceback
504
+ traceback.print_exc()
505
+ sys.exit(1)
506
+
507
+
508
+ if __name__ == '__main__':
509
+ main()