# tess successfully running. # (GPT) if it aligns with the spec & rationality of the spec. import json import re from dataclasses import dataclass from pathlib import Path import pandas as pd from rdagent.app.data_science.conf import DS_RD_SETTING from rdagent.components.agent.context7 import Agent as DocAgent from rdagent.components.coder.CoSTEER import CoSTEERMultiFeedback from rdagent.components.coder.CoSTEER.evaluators import ( CoSTEEREvaluator, CoSTEERSingleFeedback, ) from rdagent.components.coder.CoSTEER.knowledge_management import ( CoSTEERQueriedKnowledgeV2, ) from rdagent.components.coder.data_science.conf import get_clear_ws_cmd, get_ds_env from rdagent.components.coder.data_science.share.notebook import NotebookConverter from rdagent.components.coder.data_science.utils import remove_eda_part from rdagent.core.experiment import FBWorkspace, Task from rdagent.log import rdagent_logger as logger from rdagent.scenarios.data_science.test_eval import get_test_eval from rdagent.utils.agent.tpl import T from rdagent.utils.agent.workflow import build_cls_from_json_with_retry DIRNAME = Path(__file__).absolute().resolve().parent @dataclass class DSCoderFeedback(CoSTEERSingleFeedback): """ Feedback for Data Science CoSTEER evaluation. This feedback is used to evaluate the code and execution of the Data Science CoSTEER task. """ requires_documentation_search: bool | None = None # Keep None means the feature is disabled error_message: str | None = None @staticmethod def val_and_update_init_dict(data: dict) -> dict: # First call parent class validation method to handle base fields data = CoSTEERSingleFeedback.val_and_update_init_dict(data) # Validate new fields if "requires_documentation_search" in data: if isinstance(data["requires_documentation_search"], str): if data["requires_documentation_search"] == "false" or data["requires_documentation_search"] == "False": data["requires_documentation_search"] = False elif data["requires_documentation_search"] == "true" or data["requires_documentation_search"] == "True": data["requires_documentation_search"] = True else: raise ValueError( f"'requires_documentation_search' string value must be 'true', 'True', 'false', or 'False', not '{data['requires_documentation_search']}'" ) elif data["requires_documentation_search"] is not None and not isinstance( data["requires_documentation_search"], bool ): raise ValueError( f"'requires_documentation_search' must be a boolean, string, or None, not {type(data['requires_documentation_search'])}" ) if "error_message" in data: if data["error_message"] is not None and not isinstance(data["error_message"], str): raise ValueError(f"'error_message' must be a string or None, not {type(data['error_message'])}") return data def __str__(self) -> str: base_str = super().__str__() if self.requires_documentation_search is not None: base_str += f"-------------------Documentation Search Required------------------\n{self.requires_documentation_search}\n" if self.error_message is not None: # Check if error_message contains Context7 documentation results if "### API Documentation Reference:" in self.error_message: base_str += f"-------------------Error Analysis & Documentation Search Results ------------------\n{self.error_message}\n" else: base_str += f"-------------------Error Message------------------\n{self.error_message}\n" return base_str @classmethod def merge(cls, feedback_li: list[CoSTEERSingleFeedback]) -> "DSCoderFeedback": # Call parent class merge method to handle base fields merged_fb = super().merge(feedback_li) # Convert to DSCoderFeedback type if needed if not isinstance(merged_fb, DSCoderFeedback): merged_fb = DSCoderFeedback( execution=merged_fb.execution, return_checking=merged_fb.return_checking, code=merged_fb.code, final_decision=merged_fb.final_decision, ) # Merge error_message fields error_messages = [ fb.error_message for fb in feedback_li if isinstance(fb, DSCoderFeedback) and fb.error_message is not None ] if error_messages: merged_fb.error_message = "\n\n".join(error_messages) # Merge requires_documentation_search fields (True if any is True) requires_search = [ fb.requires_documentation_search for fb in feedback_li if isinstance(fb, DSCoderFeedback) and fb.requires_documentation_search is not None ] if requires_search: merged_fb.requires_documentation_search = any(requires_search) return merged_fb PipelineSingleFeedback = DSCoderFeedback # Only for compatible PipelineMultiFeedback = CoSTEERMultiFeedback class PipelineCoSTEEREvaluator(CoSTEEREvaluator): def evaluate( self, target_task: Task, implementation: FBWorkspace, gt_implementation: FBWorkspace, queried_knowledge: CoSTEERQueriedKnowledgeV2 = None, **kwargs, ) -> PipelineSingleFeedback: target_task_information = target_task.get_task_information() if ( queried_knowledge is not None and target_task_information in queried_knowledge.success_task_to_knowledge_dict ): return queried_knowledge.success_task_to_knowledge_dict[target_task_information].feedback elif queried_knowledge is not None and target_task_information in queried_knowledge.failed_task_info_set: return PipelineSingleFeedback( execution="This task has failed too many times, skip implementation.", return_checking="This task has failed too many times, skip implementation.", code="This task has failed too many times, skip implementation.", error_message="This task has failed too many times, skip implementation.", requires_documentation_search=None, final_decision=False, ) env = get_ds_env( extra_volumes={self.scen.debug_path: T("scenarios.data_science.share:scen.input_path").r()}, running_timeout_period=self.scen.real_debug_timeout(), ) stdout = "" implementation.execute(env=env, entry=get_clear_ws_cmd()) if DS_RD_SETTING.sample_data_by_LLM: # Because coder runs on full data, we need to run debug mode in advance to save time result = implementation.run( env=env, entry=f"strace -e trace=file -f -o trace.log python -m coverage run main.py --debug" ) else: result = implementation.run( env=env, entry=f"strace -e trace=file -f -o trace.log python -m coverage run main.py" ) result_stdout = result.stdout nb_conversion_ret_code = 0 nb_conversion_check_text = "" if DS_RD_SETTING.enable_notebook_conversion: notebook_converter = NotebookConverter() code = implementation.file_dict["main.py"] error_msg = notebook_converter.validate_code_format(code) if error_msg is not None: nb_conversion_check_text = error_msg nb_conversion_ret_code = 1 else: notebook_converter.convert( task=target_task, code=code, stdout=result_stdout, outfile=implementation.workspace_path / "main.ipynb", use_debug_flag=DS_RD_SETTING.sample_data_by_LLM, ) sample_submission_check = True test_eval = get_test_eval() if (sample_submission_file_name := test_eval.get_sample_submission_name(self.scen.competition)) is not None: # check whether code ever opens the sample submission file if (implementation.workspace_path / "trace.log").exists(): opened_trace_lines = [ line for line in (implementation.workspace_path / "trace.log").read_text().splitlines() if "openat" in line and sample_submission_file_name in line ] if len(opened_trace_lines) > 0: stdout += f"Code opened the sample submission file '{sample_submission_file_name}' during execution.\n Reject the implementation!\n" sample_submission_check = False result_stdout = remove_eda_part(result_stdout) if result.exit_code != 0: stdout += f"Code failed to run. Please check the stdout:\n Following the stdout of the debug mode run:\n{result_stdout.strip()}\n" else: stdout += f"Code ran successfully.\n Following the stdout of the debug mode run:\n{result_stdout.strip()}\n" if DS_RD_SETTING.sample_data_by_LLM: debug_time, full_estimated_time = None, None if match := re.search(r"debug_time:\s*(\d+(?:.\d+)?)", result_stdout, re.DOTALL): debug_time = float(match.group(1)) if match := re.search(r"estimated_time:\s*(\d+(?:.\d+)?)", result_stdout, re.DOTALL): full_estimated_time = float(match.group(1)) if debug_time is not None and full_estimated_time is not None: stdout += f"Debug mode ran in {debug_time:.2f} seconds, estimated full run time is {full_estimated_time:.2f} seconds. The estimated time is {full_estimated_time / env.conf.running_timeout_period * 100:.2f}% the debug time." else: stdout += "Debug mode did not provide debug_time or estimated_time, it's a buggy implementation.\n" score_fp = implementation.workspace_path / "scores.csv" score_ret_code = 0 score_check_text = "" if not score_fp.exists(): score_check_text = "[Error] Metrics file (scores.csv) is not generated!" score_ret_code = 1 else: try: score_df = pd.read_csv(score_fp, index_col=0) model_set_in_scores = set(score_df.index) # Check model names (index) if not score_df.index.is_unique: score_check_text += "\n[Error] The file 'scores.csv' contains duplicate model names." score_ret_code = 1 if "ensemble" not in model_set_in_scores: score_check_text += "\n[Error] The file 'scores.csv' doesn't contain the ensemble model." score_ret_code = 1 if score_ret_code != 0: score_check_text += f"The dataframe in file 'scores.csv' is:\n{score_df}" # Check metric name (columns) - case insensitive if [col.lower() for col in score_df.columns.tolist()] != [self.scen.metric_name.lower()]: score_check_text += f"\n[Error] The scores dataframe does not contain the correct column names.\nCorrect columns is: ['{self.scen.metric_name}']\nBut got: {score_df.columns.tolist()}" score_ret_code = 1 # Check if scores contain NaN (values) if score_df.isnull().values.any(): nan_locations = score_df[score_df.isnull().any(axis=1)] score_check_text += f"\n[Error] The scores dataframe contains NaN values at the following locations:\n{nan_locations}" score_ret_code = 1 except Exception as e: score_check_text += f"\n[Error] in checking the scores.csv file: {e}\nscores.csv's content:\n-----\n{score_fp.read_text()}\n-----" score_ret_code = 1 test_eval = get_test_eval() if DS_RD_SETTING.sample_data_by_LLM and test_eval.enabled(self.scen.competition): submission_check_out, submission_ret_code = test_eval.valid(self.scen.competition, implementation) stdout += f"\n### Submission check:\n{submission_check_out}\nIf 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. " elif not test_eval.is_sub_enabled(self.scen.competition): submission_ret_code = 0 else: # Check submission file base_check_code = T(".eval_tests.submission_format_test", ftype="txt").r() implementation.inject_files(**{"test/submission_format_test.py": base_check_code}) # stdout += "----Submission Check 1-----\n" submission_result = implementation.run(env=env, entry="python test/submission_format_test.py") submission_check_out = submission_result.stdout submission_ret_code = submission_result.exit_code stdout += "\n" + submission_check_out if not isinstance(implementation, FBWorkspace): eda_output = None else: eda_output = implementation.file_dict.get("EDA.md", None) # extract enable_mcp_documentation_search from data science configuration enable_mcp_documentation_search = DS_RD_SETTING.enable_mcp_documentation_search queried_similar_successful_knowledge = ( queried_knowledge.task_to_similar_task_successful_knowledge[target_task.get_task_information()] if queried_knowledge is not None else [] ) system_prompt = T(".prompts:pipeline_eval.system").r( is_sub_enabled=test_eval.is_sub_enabled(self.scen.competition), debug_mode=DS_RD_SETTING.sample_data_by_LLM, enable_mcp_documentation_search=enable_mcp_documentation_search, mle_check=DS_RD_SETTING.sample_data_by_LLM, queried_similar_successful_knowledge=queried_similar_successful_knowledge, ) user_prompt = T(".prompts:pipeline_eval.user").r( scenario=self.scen.get_scenario_all_desc(eda_output=eda_output), task_desc=target_task.get_task_information(), stdout=stdout.strip(), spec=T("scenarios.data_science.share:component_spec.Pipeline").r( metric_name=self.scen.metric_name, enable_notebook_conversion=DS_RD_SETTING.enable_notebook_conversion, ), code=implementation.file_dict["main.py"], ) wfb = build_cls_from_json_with_retry( PipelineSingleFeedback, system_prompt=system_prompt, user_prompt=user_prompt, init_kwargs_update_func=PipelineSingleFeedback.val_and_update_init_dict, ) # judge whether we should perform documentation search do_documentation_search = enable_mcp_documentation_search and wfb.requires_documentation_search if do_documentation_search: # Use MCPAgent for clean, user-friendly interface try: # Create agent targeting Context7 service - model config comes from mcp_config.json doc_agent = DocAgent() # Synchronous query - perfect for evaluation context if wfb.error_message: # Type safety check context7_result = doc_agent.query(query=wfb.error_message) if context7_result: logger.info("Context7: Documentation search completed successfully") wfb.error_message += f"\n\n### API Documentation Reference:\nThe following API documentation was retrieved based on the error. This provides factual information about API changes or parameter specifications only:\n\n{context7_result}" else: logger.warning("Context7: Documentation search failed or no results found") else: logger.warning("Context7: No error message to search for") # TODO: confirm what exception will be raised when timeout # except concurrent.futures.TimeoutError: # logger.error("Context7: Query timed out after 180 seconds") except Exception as e: error_msg = str(e) if str(e) else type(e).__name__ logger.error(f"Context7: Query failed - {error_msg}") if score_ret_code != 0 and wfb.final_decision is True: wfb.final_decision = False wfb.return_checking += "\n" + score_check_text if submission_ret_code != 0 and wfb.final_decision is True: wfb.final_decision = False wfb.return_checking += "\nSubmission file check failed." if sample_submission_check is False and wfb.final_decision is True: wfb.final_decision = False wfb.return_checking += ( "\nSample submission file check failed. Code should not open the sample submission file." ) if nb_conversion_ret_code != 0 and wfb.final_decision is True: wfb.final_decision = False wfb.return_checking += "\n" + nb_conversion_check_text return wfb