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.
@@ -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."""