chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user