claude-evolve 1.8.51 → 1.9.1
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-ideate-py +15 -0
- package/bin/claude-evolve-main +12 -0
- package/bin/claude-evolve-run-py +15 -0
- package/bin/claude-evolve-worker-py +15 -0
- package/lib/__pycache__/ai_cli.cpython-314.pyc +0 -0
- package/lib/__pycache__/embedding.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolution_csv.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolve_ideate.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolve_run.cpython-314.pyc +0 -0
- package/lib/__pycache__/evolve_worker.cpython-314.pyc +0 -0
- package/lib/ai_cli.py +196 -0
- package/lib/embedding.py +200 -0
- package/lib/evolution_csv.py +325 -0
- package/lib/evolve_ideate.py +509 -0
- package/lib/evolve_run.py +402 -0
- package/lib/evolve_worker.py +518 -0
- package/package.json +4 -1
package/lib/evolution_csv.py
CHANGED
|
@@ -558,6 +558,331 @@ class EvolutionCSV:
|
|
|
558
558
|
"""Check if there are any pending candidates. Used by dispatcher."""
|
|
559
559
|
return self.count_pending_candidates() > 0
|
|
560
560
|
|
|
561
|
+
def get_top_performers(self, n: int = 10, include_novel: bool = True) -> List[Dict[str, Any]]:
|
|
562
|
+
"""
|
|
563
|
+
Get top performing candidates sorted by performance score.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
n: Number of top performers to return
|
|
567
|
+
include_novel: If True, also include candidates from recent generations
|
|
568
|
+
even if they're not top performers (for diversity)
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
List of dicts with id, basedOnId, description, performance, status
|
|
572
|
+
"""
|
|
573
|
+
rows = self._read_csv()
|
|
574
|
+
if not rows:
|
|
575
|
+
return []
|
|
576
|
+
|
|
577
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
578
|
+
start_idx = 1 if has_header else 0
|
|
579
|
+
|
|
580
|
+
candidates = []
|
|
581
|
+
for row in rows[start_idx:]:
|
|
582
|
+
if not self.is_valid_candidate_row(row):
|
|
583
|
+
continue
|
|
584
|
+
|
|
585
|
+
candidate_id = row[0].strip().strip('"')
|
|
586
|
+
status = row[4].strip().lower() if len(row) > 4 else ''
|
|
587
|
+
|
|
588
|
+
# Only include completed candidates with valid performance
|
|
589
|
+
if status != 'complete':
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
performance_str = row[3].strip() if len(row) > 3 else ''
|
|
593
|
+
if not performance_str:
|
|
594
|
+
continue
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
performance = float(performance_str)
|
|
598
|
+
except ValueError:
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
candidates.append({
|
|
602
|
+
'id': candidate_id,
|
|
603
|
+
'basedOnId': row[1].strip() if len(row) > 1 else '',
|
|
604
|
+
'description': row[2].strip() if len(row) > 2 else '',
|
|
605
|
+
'performance': performance,
|
|
606
|
+
'status': status
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
# Sort by performance (descending)
|
|
610
|
+
candidates.sort(key=lambda x: x['performance'], reverse=True)
|
|
611
|
+
|
|
612
|
+
# Get top n
|
|
613
|
+
top_n = candidates[:n]
|
|
614
|
+
|
|
615
|
+
# Optionally include novel candidates from recent generations
|
|
616
|
+
if include_novel and len(candidates) > n:
|
|
617
|
+
# Find the highest generation number
|
|
618
|
+
max_gen = 0
|
|
619
|
+
for c in candidates:
|
|
620
|
+
gen_match = c['id'].split('-')[0] if '-' in c['id'] else ''
|
|
621
|
+
if gen_match.startswith('gen'):
|
|
622
|
+
try:
|
|
623
|
+
gen_num = int(gen_match[3:])
|
|
624
|
+
max_gen = max(max_gen, gen_num)
|
|
625
|
+
except ValueError:
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
# Include recent candidates not already in top_n
|
|
629
|
+
top_ids = {c['id'] for c in top_n}
|
|
630
|
+
for c in candidates:
|
|
631
|
+
if c['id'] in top_ids:
|
|
632
|
+
continue
|
|
633
|
+
gen_match = c['id'].split('-')[0] if '-' in c['id'] else ''
|
|
634
|
+
if gen_match.startswith('gen'):
|
|
635
|
+
try:
|
|
636
|
+
gen_num = int(gen_match[3:])
|
|
637
|
+
# Include from last 2 generations
|
|
638
|
+
if gen_num >= max_gen - 1:
|
|
639
|
+
top_n.append(c)
|
|
640
|
+
except ValueError:
|
|
641
|
+
pass
|
|
642
|
+
|
|
643
|
+
return top_n
|
|
644
|
+
|
|
645
|
+
def get_generation_stats(self) -> Dict[str, Dict[str, int]]:
|
|
646
|
+
"""
|
|
647
|
+
Get statistics per generation.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Dict mapping generation (e.g., "gen01") to stats dict with:
|
|
651
|
+
- total: total candidates
|
|
652
|
+
- pending: pending candidates
|
|
653
|
+
- complete: completed candidates
|
|
654
|
+
- failed: failed candidates
|
|
655
|
+
"""
|
|
656
|
+
rows = self._read_csv()
|
|
657
|
+
if not rows:
|
|
658
|
+
return {}
|
|
659
|
+
|
|
660
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
661
|
+
start_idx = 1 if has_header else 0
|
|
662
|
+
|
|
663
|
+
stats: Dict[str, Dict[str, int]] = {}
|
|
664
|
+
|
|
665
|
+
for row in rows[start_idx:]:
|
|
666
|
+
if not self.is_valid_candidate_row(row):
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
candidate_id = row[0].strip().strip('"')
|
|
670
|
+
status = row[4].strip().lower() if len(row) > 4 else 'pending'
|
|
671
|
+
|
|
672
|
+
# Extract generation from ID (e.g., "gen01-001" -> "gen01")
|
|
673
|
+
if '-' in candidate_id:
|
|
674
|
+
generation = candidate_id.split('-')[0]
|
|
675
|
+
elif candidate_id.startswith('baseline'):
|
|
676
|
+
generation = 'baseline'
|
|
677
|
+
else:
|
|
678
|
+
generation = 'unknown'
|
|
679
|
+
|
|
680
|
+
if generation not in stats:
|
|
681
|
+
stats[generation] = {'total': 0, 'pending': 0, 'complete': 0, 'failed': 0}
|
|
682
|
+
|
|
683
|
+
stats[generation]['total'] += 1
|
|
684
|
+
|
|
685
|
+
if status == 'complete':
|
|
686
|
+
stats[generation]['complete'] += 1
|
|
687
|
+
elif status.startswith('failed'):
|
|
688
|
+
stats[generation]['failed'] += 1
|
|
689
|
+
elif status in ('pending', 'running', '') or status.startswith('failed-retry'):
|
|
690
|
+
stats[generation]['pending'] += 1
|
|
691
|
+
|
|
692
|
+
return stats
|
|
693
|
+
|
|
694
|
+
def get_all_descriptions(self) -> List[str]:
|
|
695
|
+
"""
|
|
696
|
+
Get all candidate descriptions for novelty checking.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
List of description strings
|
|
700
|
+
"""
|
|
701
|
+
rows = self._read_csv()
|
|
702
|
+
if not rows:
|
|
703
|
+
return []
|
|
704
|
+
|
|
705
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
706
|
+
start_idx = 1 if has_header else 0
|
|
707
|
+
|
|
708
|
+
descriptions = []
|
|
709
|
+
for row in rows[start_idx:]:
|
|
710
|
+
if not self.is_valid_candidate_row(row):
|
|
711
|
+
continue
|
|
712
|
+
|
|
713
|
+
description = row[2].strip() if len(row) > 2 else ''
|
|
714
|
+
if description:
|
|
715
|
+
descriptions.append(description)
|
|
716
|
+
|
|
717
|
+
return descriptions
|
|
718
|
+
|
|
719
|
+
def get_highest_generation(self) -> int:
|
|
720
|
+
"""
|
|
721
|
+
Get the highest generation number in the CSV.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Highest generation number (0 if none found)
|
|
725
|
+
"""
|
|
726
|
+
rows = self._read_csv()
|
|
727
|
+
if not rows:
|
|
728
|
+
return 0
|
|
729
|
+
|
|
730
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
731
|
+
start_idx = 1 if has_header else 0
|
|
732
|
+
|
|
733
|
+
max_gen = 0
|
|
734
|
+
for row in rows[start_idx:]:
|
|
735
|
+
if not self.is_valid_candidate_row(row):
|
|
736
|
+
continue
|
|
737
|
+
|
|
738
|
+
candidate_id = row[0].strip().strip('"')
|
|
739
|
+
if '-' in candidate_id:
|
|
740
|
+
gen_part = candidate_id.split('-')[0]
|
|
741
|
+
if gen_part.startswith('gen'):
|
|
742
|
+
try:
|
|
743
|
+
gen_num = int(gen_part[3:])
|
|
744
|
+
max_gen = max(max_gen, gen_num)
|
|
745
|
+
except ValueError:
|
|
746
|
+
pass
|
|
747
|
+
|
|
748
|
+
return max_gen
|
|
749
|
+
|
|
750
|
+
def get_next_id(self, generation: int) -> str:
|
|
751
|
+
"""
|
|
752
|
+
Get the next available ID for a generation.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
generation: Generation number (e.g., 1 for gen01)
|
|
756
|
+
|
|
757
|
+
Returns:
|
|
758
|
+
Next ID string (e.g., "gen01-005")
|
|
759
|
+
"""
|
|
760
|
+
rows = self._read_csv()
|
|
761
|
+
gen_prefix = f"gen{generation:02d}"
|
|
762
|
+
|
|
763
|
+
max_id = 0
|
|
764
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
765
|
+
start_idx = 1 if has_header else 0
|
|
766
|
+
|
|
767
|
+
for row in rows[start_idx:]:
|
|
768
|
+
if not self.is_valid_candidate_row(row):
|
|
769
|
+
continue
|
|
770
|
+
|
|
771
|
+
candidate_id = row[0].strip().strip('"')
|
|
772
|
+
if candidate_id.startswith(gen_prefix + '-'):
|
|
773
|
+
try:
|
|
774
|
+
id_num = int(candidate_id.split('-')[1])
|
|
775
|
+
max_id = max(max_id, id_num)
|
|
776
|
+
except (ValueError, IndexError):
|
|
777
|
+
pass
|
|
778
|
+
|
|
779
|
+
return f"{gen_prefix}-{max_id + 1:03d}"
|
|
780
|
+
|
|
781
|
+
def get_next_ids(self, generation: int, count: int) -> List[str]:
|
|
782
|
+
"""
|
|
783
|
+
Get multiple next available IDs for a generation.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
generation: Generation number
|
|
787
|
+
count: Number of IDs to generate
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
List of ID strings
|
|
791
|
+
"""
|
|
792
|
+
rows = self._read_csv()
|
|
793
|
+
gen_prefix = f"gen{generation:02d}"
|
|
794
|
+
|
|
795
|
+
max_id = 0
|
|
796
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
797
|
+
start_idx = 1 if has_header else 0
|
|
798
|
+
|
|
799
|
+
for row in rows[start_idx:]:
|
|
800
|
+
if not self.is_valid_candidate_row(row):
|
|
801
|
+
continue
|
|
802
|
+
|
|
803
|
+
candidate_id = row[0].strip().strip('"')
|
|
804
|
+
if candidate_id.startswith(gen_prefix + '-'):
|
|
805
|
+
try:
|
|
806
|
+
id_num = int(candidate_id.split('-')[1])
|
|
807
|
+
max_id = max(max_id, id_num)
|
|
808
|
+
except (ValueError, IndexError):
|
|
809
|
+
pass
|
|
810
|
+
|
|
811
|
+
return [f"{gen_prefix}-{max_id + 1 + i:03d}" for i in range(count)]
|
|
812
|
+
|
|
813
|
+
def append_candidates(self, candidates: List[Dict[str, str]]) -> int:
|
|
814
|
+
"""
|
|
815
|
+
Append multiple candidates to the CSV.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
candidates: List of dicts with keys: id, basedOnId, description
|
|
819
|
+
Optional keys: performance, status, idea-LLM, run-LLM
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
Number of candidates appended
|
|
823
|
+
"""
|
|
824
|
+
if not candidates:
|
|
825
|
+
return 0
|
|
826
|
+
|
|
827
|
+
rows = self._read_csv()
|
|
828
|
+
|
|
829
|
+
# Ensure header exists
|
|
830
|
+
if not rows:
|
|
831
|
+
rows = [['id', 'basedOnId', 'description', 'performance', 'status', 'idea-LLM', 'run-LLM']]
|
|
832
|
+
elif rows[0][0].lower() != 'id':
|
|
833
|
+
# Add header if missing
|
|
834
|
+
rows.insert(0, ['id', 'basedOnId', 'description', 'performance', 'status', 'idea-LLM', 'run-LLM'])
|
|
835
|
+
|
|
836
|
+
# Append candidates
|
|
837
|
+
for candidate in candidates:
|
|
838
|
+
row = [
|
|
839
|
+
candidate.get('id', ''),
|
|
840
|
+
candidate.get('basedOnId', ''),
|
|
841
|
+
candidate.get('description', ''),
|
|
842
|
+
candidate.get('performance', ''),
|
|
843
|
+
candidate.get('status', 'pending'),
|
|
844
|
+
candidate.get('idea-LLM', ''),
|
|
845
|
+
candidate.get('run-LLM', '')
|
|
846
|
+
]
|
|
847
|
+
rows.append(row)
|
|
848
|
+
|
|
849
|
+
self._write_csv(rows)
|
|
850
|
+
return len(candidates)
|
|
851
|
+
|
|
852
|
+
def get_csv_stats(self) -> Dict[str, int]:
|
|
853
|
+
"""
|
|
854
|
+
Get overall CSV statistics.
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
Dict with total, pending, complete, failed counts
|
|
858
|
+
"""
|
|
859
|
+
rows = self._read_csv()
|
|
860
|
+
if not rows:
|
|
861
|
+
return {'total': 0, 'pending': 0, 'complete': 0, 'failed': 0, 'running': 0}
|
|
862
|
+
|
|
863
|
+
has_header = rows and rows[0] and rows[0][0].lower() == 'id'
|
|
864
|
+
start_idx = 1 if has_header else 0
|
|
865
|
+
|
|
866
|
+
stats = {'total': 0, 'pending': 0, 'complete': 0, 'failed': 0, 'running': 0}
|
|
867
|
+
|
|
868
|
+
for row in rows[start_idx:]:
|
|
869
|
+
if not self.is_valid_candidate_row(row):
|
|
870
|
+
continue
|
|
871
|
+
|
|
872
|
+
stats['total'] += 1
|
|
873
|
+
status = row[4].strip().lower() if len(row) > 4 else ''
|
|
874
|
+
|
|
875
|
+
if status == 'complete':
|
|
876
|
+
stats['complete'] += 1
|
|
877
|
+
elif status == 'running':
|
|
878
|
+
stats['running'] += 1
|
|
879
|
+
elif status.startswith('failed'):
|
|
880
|
+
stats['failed'] += 1
|
|
881
|
+
else:
|
|
882
|
+
stats['pending'] += 1
|
|
883
|
+
|
|
884
|
+
return stats
|
|
885
|
+
|
|
561
886
|
|
|
562
887
|
def main():
|
|
563
888
|
"""Command line interface for testing."""
|