1313import time
1414import traceback
1515import uuid
16+ import warnings
1617from pathlib import Path
17- from typing import Any , Callable , Dict , List , Optional , Tuple , Union
18+ from typing import Any , Dict , List , Optional , Tuple , Union , Protocol , cast
1819import traceback
1920
2021from openevolve .config import EvaluatorConfig
2930logger = logging .getLogger (__name__ )
3031
3132
33+ class EvaluationObject (Protocol ):
34+ def evaluate (self , program_path : str ) -> EvaluationResult :
35+ ...
36+
37+
38+ class CascadeEvaluationObject (Protocol ):
39+ def evaluate_stage1 (self , program_path : str ) -> EvaluationResult :
40+ ...
41+
42+ def evaluate_stage2 (self , program_path : str ) -> EvaluationResult :
43+ ...
44+
45+ def evaluate_stage3 (self , program_path : str ) -> EvaluationResult :
46+ ...
47+
48+
3249class Evaluator :
3350 """
3451 Evaluates programs and assigns scores
@@ -41,23 +58,28 @@ def __init__(
4158 self ,
4259 config : EvaluatorConfig ,
4360 evaluation_file : str ,
61+ evaluation_object : Optional [EvaluationObject ] = None ,
4462 llm_ensemble : Optional [LLMEnsemble ] = None ,
4563 prompt_sampler : Optional [PromptSampler ] = None ,
4664 database : Optional [ProgramDatabase ] = None ,
4765 suffix : Optional [str ]= ".py" ,
4866 ):
67+ if evaluation_file and evaluation_object :
68+ warnings .warn ("Both evaluation_file and evaluation_object provided - evaluation_object overrides evaluation_file" )
4969 self .config = config
5070 self .evaluation_file = evaluation_file
5171 self .program_suffix = suffix
72+ self .evaluation_object = evaluation_object
5273 self .llm_ensemble = llm_ensemble
5374 self .prompt_sampler = prompt_sampler
5475 self .database = database
5576
5677 # Create a task pool for parallel evaluation
5778 self .task_pool = TaskPool (max_concurrency = config .parallel_evaluations )
5879
59- # Set up evaluation function if file exists
60- self ._load_evaluation_function ()
80+ if self .evaluation_object is None :
81+ # Set up evaluation module if file exists
82+ self ._load_evaluation_function ()
6183
6284 # Pending artifacts storage for programs
6385 self ._pending_artifacts : Dict [str , Dict [str , Union [str , bytes ]]] = {}
@@ -89,7 +111,7 @@ def _load_evaluation_function(self) -> None:
89111 f"Evaluation file { self .evaluation_file } does not contain an 'evaluate' function"
90112 )
91113
92- self .evaluate_function = module .evaluate
114+ self .evaluation_object = module .evaluate
93115 logger .info (f"Successfully loaded evaluation function from { self .evaluation_file } " )
94116
95117 # Validate cascade configuration
@@ -348,7 +370,7 @@ async def _direct_evaluate(
348370 # Create a coroutine that runs the evaluation function in an executor
349371 async def run_evaluation ():
350372 loop = asyncio .get_event_loop ()
351- return await loop .run_in_executor (None , self .evaluate_function , program_path )
373+ return await loop .run_in_executor (None , self .evaluation_object . evaluate , program_path )
352374
353375 # Run the evaluation with timeout - let exceptions bubble up for retry handling
354376 result = await asyncio .wait_for (run_evaluation (), timeout = self .config .timeout )
@@ -369,31 +391,19 @@ async def _cascade_evaluate(
369391 Returns:
370392 Dictionary of metrics or EvaluationResult with metrics and artifacts
371393 """
372- # Import the evaluation module to get cascade functions if they exist
394+ # This cast just makes static type checkers happy; actual checking is still done using hasattr
395+ evaluation_object = cast (CascadeEvaluationObject , self .evaluation_object )
373396 try :
374- # Add the evaluation file's directory to Python path so it can import local modules
375- eval_dir = os .path .dirname (os .path .abspath (self .evaluation_file ))
376- if eval_dir not in sys .path :
377- sys .path .insert (0 , eval_dir )
378- logger .debug (f"Added { eval_dir } to Python path for cascade evaluation" )
379-
380- spec = importlib .util .spec_from_file_location ("evaluation_module" , self .evaluation_file )
381- if spec is None or spec .loader is None :
382- return await self ._direct_evaluate (program_path )
383-
384- module = importlib .util .module_from_spec (spec )
385- spec .loader .exec_module (module )
386-
387397 # Check if cascade functions exist
388- if not hasattr (module , "evaluate_stage1" ):
398+ if not hasattr (evaluation_object , "evaluate_stage1" ):
389399 return await self ._direct_evaluate (program_path )
390400
391401 # Run first stage with timeout
392402 try :
393403
394404 async def run_stage1 ():
395405 loop = asyncio .get_event_loop ()
396- return await loop .run_in_executor (None , module .evaluate_stage1 , program_path )
406+ return await loop .run_in_executor (None , evaluation_object .evaluate_stage1 , program_path )
397407
398408 stage1_result = await asyncio .wait_for (run_stage1 (), timeout = self .config .timeout )
399409 stage1_eval_result = self ._process_evaluation_result (stage1_result )
@@ -426,15 +436,15 @@ async def run_stage1():
426436 return stage1_eval_result
427437
428438 # Check if second stage exists
429- if not hasattr (module , "evaluate_stage2" ):
439+ if not hasattr (evaluation_object , "evaluate_stage2" ):
430440 return stage1_eval_result
431441
432442 # Run second stage with timeout
433443 try :
434444
435445 async def run_stage2 ():
436446 loop = asyncio .get_event_loop ()
437- return await loop .run_in_executor (None , module .evaluate_stage2 , program_path )
447+ return await loop .run_in_executor (None , evaluation_object .evaluate_stage2 , program_path )
438448
439449 stage2_result = await asyncio .wait_for (run_stage2 (), timeout = self .config .timeout )
440450 stage2_eval_result = self ._process_evaluation_result (stage2_result )
@@ -488,15 +498,15 @@ async def run_stage2():
488498 return merged_result
489499
490500 # Check if third stage exists
491- if not hasattr (module , "evaluate_stage3" ):
501+ if not hasattr (evaluation_object , "evaluate_stage3" ):
492502 return merged_result
493503
494504 # Run third stage with timeout
495505 try :
496506
497507 async def run_stage3 ():
498508 loop = asyncio .get_event_loop ()
499- return await loop .run_in_executor (None , module .evaluate_stage3 , program_path )
509+ return await loop .run_in_executor (None , evaluation_object .evaluate_stage3 , program_path )
500510
501511 stage3_result = await asyncio .wait_for (run_stage3 (), timeout = self .config .timeout )
502512 stage3_eval_result = self ._process_evaluation_result (stage3_result )
0 commit comments