2025-02-11 20:50:19 +08:00
import json
2025-02-26 22:35:44 +08:00
import re
2025-07-31 17:53:18 +08:00
from dataclasses import dataclass
2025-08-12 21:12:54 +08:00
from datetime import timedelta
2025-02-11 20:50:19 +08:00
from pathlib import Path
2025-03-17 23:12:59 +08:00
import pandas as pd
2025-02-11 20:50:19 +08:00
from rdagent.app.data_science.conf import DS_RD_SETTING
from rdagent.components.coder.CoSTEER.evaluators import (
CoSTEEREvaluator ,
CoSTEERSingleFeedback ,
)
2025-04-09 23:24:12 +08:00
from rdagent.components.coder.data_science.conf import get_clear_ws_cmd , get_ds_env
2025-04-17 16:19:26 +08:00
from rdagent.components.coder.data_science.utils import remove_eda_part
2025-02-11 20:50:19 +08:00
from rdagent.core.evolving_framework import QueriedKnowledge
from rdagent.core.experiment import FBWorkspace , Task
from rdagent.log import rdagent_logger as logger
2025-08-12 21:12:54 +08:00
from rdagent.log.timer import RD_Agent_TIMER_wrapper
2025-09-10 10:02:49 +08:00
from rdagent.scenarios.data_science.dev.runner import DSRunnerCoSTEERSettings
2025-05-06 16:00:13 +08:00
from rdagent.scenarios.data_science.test_eval import (
MLETestEval ,
NoTestEvalError ,
get_test_eval ,
)
2025-02-11 20:50:19 +08:00
from rdagent.utils.agent.tpl import T
from rdagent.utils.agent.workflow import build_cls_from_json_with_retry
from rdagent.utils.fmt import shrink_text
DIRNAME = Path ( __file__ ) . absolute () . resolve () . parent
2025-07-08 15:22:39 +08:00
2025-07-31 17:53:18 +08:00
@dataclass
class DSRunnerFeedback ( CoSTEERSingleFeedback ):
2025-07-08 15:22:39 +08:00
"""
Feedback for Data Science CoSTEER evaluation.
This feedback is used to evaluate the code and execution of the Data Science CoSTEER task.
"""
2025-07-31 17:53:18 +08:00
acceptable : bool | None = None
hyperparameter_tuning_decision : bool | None = None
hyperparameter_tuning_suggestion : str | None = None
2025-08-04 17:38:27 +08:00
score : str | None = None
2025-02-11 20:50:19 +08:00
2025-07-31 17:53:18 +08:00
def is_acceptable ( self ) -> bool :
if self . acceptable is not None :
return self . acceptable
return super () . is_acceptable ()
2025-02-11 20:50:19 +08:00
2025-08-04 17:38:27 +08:00
def __str__ ( self ) -> str :
parts = [
"### Execution" ,
str ( self . execution ),
"### Return Check" ,
self . return_checking if self . return_checking is not None else "No return checking" ,
"### Code" ,
str ( self . code ),
"### Validation Score" ,
f " { self . score } " if self . score else "Not available" ,
"### Final Decision" ,
f "This implementation is { 'PASSED' if self . acceptable else 'FAILED' } ." ,
]
if self . hyperparameter_tuning_decision :
parts . append ( "### Hyperparameter Tuning Suggestion" )
parts . append ( str ( self . hyperparameter_tuning_suggestion ))
return " \n " . join ( parts )
2025-07-31 17:53:18 +08:00
2025-08-04 12:40:35 +08:00
DSCoSTEEREvalFeedback = DSRunnerFeedback # FIXME: Alias for backward compatibility
2025-07-31 17:53:18 +08:00
class DSRunnerEvaluator ( CoSTEEREvaluator ):
2025-02-11 20:50:19 +08:00
def evaluate (
self ,
target_task : Task ,
implementation : FBWorkspace ,
gt_implementation : FBWorkspace ,
queried_knowledge : QueriedKnowledge = None ,
** kwargs ,
2025-07-31 17:53:18 +08:00
) -> DSRunnerFeedback :
2025-04-10 17:56:57 +08:00
env = get_ds_env (
2025-04-16 18:11:46 +08:00
extra_volumes = {
f " { DS_RD_SETTING . local_data_path } / { self . scen . competition } " : T (
"scenarios.data_science.share:scen.input_path"
) . r ()
},
2025-07-29 14:59:38 +08:00
running_timeout_period = self . scen . real_full_timeout (),
2025-04-10 17:56:57 +08:00
)
2025-02-11 20:50:19 +08:00
stdout = implementation . execute (
2025-04-09 23:24:12 +08:00
env = env , entry = get_clear_ws_cmd ()
2025-02-11 20:50:19 +08:00
) # Remove previous submission and scores files generated by worklfow.
2025-08-04 17:38:27 +08:00
# get previous runner loops
task_info = target_task . get_task_information ()
queried_former_failed_knowledge = (
queried_knowledge . task_to_former_failed_traces [ task_info ] if queried_knowledge is not None else []
)[ 0 ]
2025-02-11 20:50:19 +08:00
# execute workflow
2025-07-02 15:11:18 +08:00
result = implementation . run ( env = env , entry = "python -m coverage run main.py" )
2026-03-02 19:04:10 +08:00
stdout = result . stdout
2025-07-02 23:31:29 +08:00
execute_ret_code = result . exit_code
2025-07-02 15:11:18 +08:00
implementation . running_info . running_time = result . running_time
2025-04-03 13:00:11 +08:00
match = re . search ( r "(.*?)=== Start of EDA part ===(.*)=== End of EDA part ===" , stdout , re . DOTALL )
eda_output = match . groups ()[ 1 ] if match else None
2025-04-17 16:19:26 +08:00
if eda_output is None :
eda_output = "No EDA output."
2025-09-10 10:02:49 +08:00
implementation . inject_files (
** {
"EDA.md" : eda_output ,
"stdout.txt" : result . stdout if DSRunnerCoSTEERSettings () . dump_stdout_type == "full" else stdout ,
}
) # stdout.txt is used for debugging. not used in any other place.
2025-04-17 16:19:26 +08:00
stdout = remove_eda_part ( stdout )
2025-06-23 10:38:27 +08:00
stdout += f "The code executed { 'successfully' if execute_ret_code == 0 else 'failed' } . { 'The EDA output is removed from the stdout. ' if eda_output else '' } "
2025-02-11 20:50:19 +08:00
2025-03-17 23:12:59 +08:00
# Check score file
2025-02-11 20:50:19 +08:00
score_fp = implementation . workspace_path / "scores.csv"
2025-03-17 23:12:59 +08:00
score_ret_code = 0
score_check_text = ""
2025-02-11 20:50:19 +08:00
if not score_fp . exists ():
2025-03-17 23:12:59 +08:00
logger . warning ( "Metrics file (scores.csv) is not generated!" )
score_check_text = "[Error] Metrics file (scores.csv) is not generated!"
score_ret_code = 1
2025-02-11 20:50:19 +08:00
else :
2025-03-17 23:12:59 +08:00
try :
score_df = pd . read_csv ( score_fp , index_col = 0 )
model_set_in_scores = set ( score_df . index )
model_set_in_folder = set (
f [: - 3 ] for f in implementation . file_dict . keys () if re . match ( r "^model_(?!test)\w+\.py$" , f )
)
2025-03-27 19:36:57 +08:00
# Check model names (index)
2025-04-03 13:00:11 +08:00
# in Pipeline task, we only check ensemble in scores.csv
if DS_RD_SETTING . coder_on_whole_pipeline :
2025-04-07 19:06:42 +08:00
if not score_df . index . is_unique :
2025-06-23 10:38:27 +08:00
score_check_text += " \n [Error] The file 'scores.csv' contains duplicate model names."
2025-04-07 19:06:42 +08:00
score_ret_code = 1
2025-04-03 13:00:11 +08:00
if "ensemble" not in model_set_in_scores :
2025-06-23 10:38:27 +08:00
score_check_text += " \n [Error] The file 'scores.csv' doesn't contain the ensemble model."
2025-04-03 13:00:11 +08:00
score_ret_code = 1
2025-04-07 19:06:42 +08:00
if score_ret_code != 0 :
2025-06-23 10:38:27 +08:00
score_check_text += f "The dataframe in file 'scores.csv' is: \n { score_df } "
2025-04-03 13:00:11 +08:00
else :
if model_set_in_scores != model_set_in_folder . union ({ "ensemble" }):
score_check_text += f " \n [Error] The scores dataframe does not contain the correct model names as index. \n correct model names are: { model_set_in_folder . union ({ 'ensemble' }) } \n score_df is: \n { score_df } "
score_ret_code = 1
2025-03-27 19:36:57 +08:00
2025-08-06 20:54:08 +08:00
# Check metric name (columns) - case insensitive
if [ col . lower () for col in score_df . columns . tolist ()] != [ self . scen . metric_name . lower ()]:
2025-03-27 19:36:57 +08:00
score_check_text += f " \n [Error] The scores dataframe does not contain the correct column names. \n Correct columns is: [' { self . scen . metric_name } '] \n But got: { score_df . columns . tolist () } "
score_ret_code = 1
2025-03-17 23:12:59 +08:00
except Exception as e :
logger . error ( f "Error in checking the scores.csv file: { e } " )
score_check_text += f " \n [Error] in checking the scores.csv file: { e } \n scores.csv's content: \n ----- \n { score_fp . read_text () } \n -----"
score_ret_code = 1
2025-02-11 20:50:19 +08:00
2025-03-17 23:12:59 +08:00
# DockerEnv for MLEBench submission validation
2025-04-09 13:34:48 +08:00
submission_check_out = ""
2025-05-06 16:00:13 +08:00
submission_ret_code = 0
test_eval = get_test_eval ()
2025-08-08 13:49:01 +08:00
2025-05-06 16:00:13 +08:00
if test_eval . enabled ( self . scen . competition ):
submission_check_out , submission_ret_code = test_eval . valid ( self . scen . competition , implementation )
2025-08-04 19:53:08 +08:00
stdout += f " \n ### Submission check: \n { submission_check_out } \n If Submission check returns a 'Submission is valid' or similar message, despite some warning messages, you should still consider the submission as valid and give a positive final decision. "
2025-02-11 20:50:19 +08:00
2025-08-12 21:12:54 +08:00
# Whether to enable hyperparameter tuning check
# 1. This is the first loop of evaluation.
2025-08-29 16:59:22 +08:00
if DS_RD_SETTING . only_first_loop_enable_hyperparameter_tuning :
c1 = len ( queried_knowledge . task_to_former_failed_traces [ target_task . get_task_information ()][ 0 ]) == 0
else :
c1 = True
2025-08-12 21:12:54 +08:00
# 2. The current time spent on runner is less than the time limit ratio for runner timeout.
2025-07-31 16:47:19 +08:00
time_spent_ratio = implementation . running_info . running_time / env . conf . running_timeout_period
2025-08-12 21:12:54 +08:00
c2 = time_spent_ratio < DS_RD_SETTING . time_ratio_limit_to_enable_hyperparameter_tuning
# 3. Only enable hyperparameter tuning during the merge stage if configured.
# TODO: it is not restricted in merge stage now for fast implementation.
timer = RD_Agent_TIMER_wrapper . timer
res_time = timer . remain_time ()
if DS_RD_SETTING . only_enable_tuning_in_merge :
c3 = res_time <= timedelta ( hours = DS_RD_SETTING . merge_hours )
else :
c3 = True
# 4. The current time spent on global is less than the time limit ratio for whole timeout.
2025-08-17 16:08:55 +08:00
if timer . all_duration is not None and res_time is not None :
res_ratio = res_time / timer . all_duration
c4 = res_ratio <= DS_RD_SETTING . res_time_ratio_limit_to_enable_hyperparameter_tuning
else :
c4 = True
2025-08-12 21:12:54 +08:00
# Only enable hyperparameter tuning check if all conditions are met
enable_hyperparameter_tuning_check = c1 and c2 and c3 and c4
2025-07-31 16:47:19 +08:00
2025-02-11 20:50:19 +08:00
system_prompt = T ( ".prompts:DSCoSTEER_eval.system" ) . r (
2025-04-09 09:42:30 +08:00
scenario = self . scen . get_scenario_all_desc ( eda_output = implementation . file_dict . get ( "EDA.md" , None )),
2025-02-11 20:50:19 +08:00
task_desc = target_task . get_task_information (),
2025-07-31 13:06:28 +08:00
enable_hyperparameter_tuning_check = enable_hyperparameter_tuning_check ,
2025-02-11 20:50:19 +08:00
)
user_prompt = T ( ".prompts:DSCoSTEER_eval.user" ) . r (
code = implementation . all_codes ,
2025-08-04 19:53:08 +08:00
change_summary = implementation . change_summary ,
2025-02-11 20:50:19 +08:00
stdout = shrink_text ( stdout ),
2025-07-08 15:22:39 +08:00
time_spent = f " { implementation . running_info . running_time : .2f } seconds" ,
timeout = f " { env . conf . running_timeout_period } seconds" ,
2025-07-31 16:47:19 +08:00
percent_of_timeout_used = f " { time_spent_ratio * 100 : .2f } %" ,
2025-08-04 17:38:27 +08:00
queried_former_failed_knowledge = queried_former_failed_knowledge ,
2025-02-11 20:50:19 +08:00
)
2025-02-19 22:03:39 +08:00
feedback = build_cls_from_json_with_retry (
2025-07-31 17:53:18 +08:00
DSRunnerFeedback ,
2025-03-17 19:53:09 +08:00
system_prompt = system_prompt ,
user_prompt = user_prompt ,
2025-08-04 17:38:27 +08:00
# init_kwargs_update_func=DSRunnerFeedback.val_and_update_init_dict,
2025-02-11 20:50:19 +08:00
)
2025-08-06 22:55:25 +08:00
try :
feedback . score = score_df . loc [ "ensemble" ] . iloc [ 0 ] if score_ret_code == 0 else None
except :
logger . error ( "Failed to get the score from scores.csv." )
feedback . score = None
2025-08-04 17:38:27 +08:00
feedback . final_decision = feedback . acceptable and (
not feedback . hyperparameter_tuning_decision
) # If hyperparameter_tuning_decision is None, it's considered as False, so the final_decision dependents on the acceptable
2025-02-19 22:03:39 +08:00
2025-04-03 13:00:11 +08:00
if feedback and not DS_RD_SETTING . coder_on_whole_pipeline :
2025-02-19 22:03:39 +08:00
# remove unused files
2025-03-19 19:00:53 +08:00
implementation . execute ( env = env , entry = "python -m coverage json -o coverage.json" )
2025-03-17 23:12:59 +08:00
coverage_report_path = implementation . workspace_path / "coverage.json"
if coverage_report_path . exists ():
used_files = set ( json . loads ( coverage_report_path . read_text ())[ "files" ] . keys ())
coverage_report_path . unlink ()
logger . info ( f "All used scripts: { used_files } " )
use_one_model = False
for f in used_files :
if f . startswith ( "model_" ) and "test" not in f :
use_one_model = True
break
if not use_one_model :
2025-07-31 17:53:18 +08:00
feedback . acceptable = feedback . final_decision = False
2025-03-17 23:12:59 +08:00
logger . warning ( "No model script is used in `main.py`." )
feedback . code += " \n [Error] No model script is used in `main.py`."
all_python_files = set ( Path ( implementation . workspace_path ) . rglob ( "*.py" ))
must_have_files = [ "load_data.py" , "feature.py" , "ensemble.py" ]
unused_files = [
py_file . name
for py_file in all_python_files
if not ( py_file . name in used_files or py_file . name . endswith ( "test.py" ))
]
if unused_files :
logger . warning ( f "Unused scripts: { unused_files } " )
error_files = set ( unused_files ) . intersection ( set ( must_have_files ))
if error_files :
2025-07-31 17:53:18 +08:00
feedback . acceptable = feedback . final_decision = False
2025-03-17 23:12:59 +08:00
logger . warning ( f " { error_files } must be used in `main.py`." )
feedback . code += f " \n [Error] { error_files } must be used in `main.py`."
elif use_one_model :
logger . info ( "Remove unused scripts." )
implementation . inject_files ( ** { file : implementation . DEL_KEY for file in unused_files })
2025-02-19 22:03:39 +08:00
2025-03-17 23:12:59 +08:00
if score_ret_code != 0 :
2025-07-31 17:53:18 +08:00
feedback . acceptable = feedback . final_decision = False
2025-03-17 23:12:59 +08:00
feedback . return_checking += " \n " + score_check_text
2025-05-06 16:00:13 +08:00
if submission_ret_code != 0 :
2025-07-31 17:53:18 +08:00
feedback . acceptable = feedback . final_decision = False
2025-03-17 23:12:59 +08:00
feedback . return_checking += " \n Submission file check failed."
2025-02-19 22:03:39 +08:00
return feedback