feat: checkpoint selection (#744)

* rebase selection code

* bug-free run: checkpoint selection and dynamic EDA loading

* add prototypes of various selectors, to imp. and test later

* fix EDA write bug

* move selector to from proposal.py tp seletc.py

* auto lint

* fix line-too-long typos

* aligh the design of "selection", rm extra instance check

* make auto-lint

* add non-trival selector: SOTAjump
This commit is contained in:
xuangu-fang
2025-04-09 09:42:30 +08:00
committed by GitHub
parent c3cc763430
commit fd155d1fa8
18 changed files with 353 additions and 49 deletions
+11 -1
View File
@@ -27,6 +27,10 @@ from rdagent.scenarios.data_science.dev.feedback import DSExperiment2Feedback
from rdagent.scenarios.data_science.dev.runner import DSCoSTEERRunner
from rdagent.scenarios.data_science.experiment.experiment import DSExperiment
from rdagent.scenarios.data_science.proposal.exp_gen import DSExpGen, DSTrace
from rdagent.scenarios.data_science.proposal.exp_gen.select import (
LatestCKPSelector,
SOTAJumpCKPSelector,
)
from rdagent.scenarios.kaggle.kaggle_crawler import download_data
@@ -49,6 +53,7 @@ class DataScienceRDLoop(RDLoop):
# 2) task generation from a complete solution
# self.exp_gen: ExpGen = import_class(PROP_SETTING.exp_gen)(scen)
self.ckp_selector = LatestCKPSelector()
self.exp_gen = DSExpGen(scen)
self.data_loader_coder = DataLoaderCoSTEER(scen)
self.feature_coder = FeatureCoSTEER(scen)
@@ -68,7 +73,8 @@ class DataScienceRDLoop(RDLoop):
super(RDLoop, self).__init__()
def direct_exp_gen(self, prev_out: dict[str, Any]):
exp = self.exp_gen.gen(self.trace)
selection = self.ckp_selector.get_selection(self.trace)
exp = self.exp_gen.gen(self.trace, selection)
logger.log_object(exp)
# FIXME: this is for LLM debug webapp, remove this when the debugging is done.
@@ -126,6 +132,10 @@ class DataScienceRDLoop(RDLoop):
return feedback
def record(self, prev_out: dict[str, Any]):
# set the DAG parent for the trace
self.trace.sync_dag_parent_and_hist()
e = prev_out.get(self.EXCEPTION_KEY, None)
if e is None:
self.trace.hist.append((prev_out["running"], prev_out["feedback"]))
@@ -74,7 +74,7 @@ class EnsembleMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
)
# Generate code with knowledge integration
competition_info = self.scen.get_scenario_all_desc()
competition_info = self.scen.get_scenario_all_desc(eda_output=workspace.file_dict.get("EDA.md", None))
system_prompt = T(".prompts:ensemble_coder.system").r(
task_desc=ensemble_information_str,
competition_info=competition_info,
@@ -61,7 +61,7 @@ class FeatureMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
# 2. code
system_prompt = T(".prompts:feature_coder.system").r(
competition_info=self.scen.get_scenario_all_desc(),
competition_info=self.scen.get_scenario_all_desc(eda_output=workspace.file_dict.get("EDA.md", None)),
task_desc=feature_information_str,
data_loader_code=workspace.file_dict.get("load_data.py"),
queried_similar_successful_knowledge=queried_similar_successful_knowledge,
@@ -62,7 +62,7 @@ class ModelMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
# 2. code
system_prompt = T(".prompts:model_coder.system").r(
task_desc=model_information_str,
competition_info=self.scen.get_scenario_all_desc(),
competition_info=self.scen.get_scenario_all_desc(eda_output=workspace.file_dict.get("EDA.md", None)),
data_loader_code=workspace.file_dict.get("load_data.py"),
feature_code=workspace.file_dict["feature.py"],
queried_similar_successful_knowledge=queried_similar_successful_knowledge,
@@ -66,7 +66,7 @@ class PipelineMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
workspace: FBWorkspace | None = None,
prev_task_feedback: CoSTEERSingleFeedback | None = None,
) -> dict[str, str]:
competition_info = self.scen.get_scenario_all_desc()
competition_info = self.scen.get_scenario_all_desc(eda_output=workspace.file_dict.get("EDA.md", None))
runtime_environment = self.scen.get_runtime_environment()
data_folder_info = self.scen.processed_data_folder_description
pipeline_task_info = target_task.get_task_information()
@@ -119,8 +119,10 @@ class PipelineCoSTEEREvaluator(CoSTEEREvaluator):
)
stdout += "\n" + submission_check_out
eda_output = implementation.file_dict.get("EDA.md", None)
system_prompt = T(".prompts:pipeline_eval.system").r(
scenario=self.scen.get_scenario_all_desc(),
scenario=self.scen.get_scenario_all_desc(eda_output=eda_output),
task_desc=target_task.get_task_information(),
spec=T("scenarios.data_science.share:component_spec.Pipeline").r(),
)
@@ -67,7 +67,7 @@ class DataLoaderMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
) -> dict[str, str]:
# return a workspace with "load_data.py", "spec/load_data.md" inside
# assign the implemented code to the new workspace.
competition_info = self.scen.get_scenario_all_desc()
competition_info = self.scen.get_scenario_all_desc(eda_output=workspace.file_dict.get("EDA.md", None))
runtime_environment = self.scen.get_runtime_environment()
data_folder_info = self.scen.processed_data_folder_description
data_loader_task_info = target_task.get_task_information()
@@ -231,5 +231,9 @@ class DataLoaderCoSTEER(CoSTEER):
stdout = new_exp.experiment_workspace.execute(env=env, entry=f"python test/data_loader_test.py")
match = re.search(r"(.*?)=== Start of EDA part ===(.*)=== End of EDA part ===", stdout, re.DOTALL)
eda_output = match.groups()[1] if match else None
self.scen.eda_output = eda_output
if eda_output is not None:
new_exp.experiment_workspace.inject_files(**{"EDA.md": eda_output})
else:
eda_output = "No EDA output."
new_exp.experiment_workspace.inject_files(**{"EDA.md": eda_output})
return new_exp
@@ -59,7 +59,7 @@ class WorkflowMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
# 2. code
system_prompt = T(".prompts:workflow_coder.system").r(
task_desc=workflow_information_str,
competition_info=self.scen.get_scenario_all_desc(),
competition_info=self.scen.get_scenario_all_desc(eda_output=workspace.file_dict.get("EDA.md", None)),
queried_similar_successful_knowledge=queried_similar_successful_knowledge,
queried_former_failed_knowledge=queried_former_failed_knowledge[0],
out_spec=PythonAgentOut.get_spec(),
@@ -127,7 +127,8 @@ class WorkflowGeneralCaseSpecEvaluator(CoSTEEREvaluator):
stdout += "\n" + submission_check_out
system_prompt = T(".prompts:workflow_eval.system").r(
scenario=self.scen.get_scenario_all_desc(),
# here we pass `None` to `eda_output` because we do not have nor need EDA output for workflow.
scenario=self.scen.get_scenario_all_desc(eda_output=None),
task_desc=target_task.get_task_information(),
spec=(
implementation.file_dict["spec/workflow.md"]
+29 -3
View File
@@ -1,4 +1,4 @@
""" """
# TODO: remove `self.scen` if traces will be passed into the instance.
from __future__ import annotations
@@ -112,9 +112,16 @@ ASpecificKB = TypeVar("ASpecificKB", bound=KnowledgeBase)
class Trace(Generic[ASpecificScen, ASpecificKB]):
NodeType = tuple[Experiment, ExperimentFeedback] # Define NodeType as a new type representing the tuple
def __init__(self, scen: ASpecificScen, knowledge_base: ASpecificKB | None = None) -> None:
self.scen: ASpecificScen = scen
self.hist: list[tuple[Experiment, ExperimentFeedback]] = []
self.hist: list[Trace.NodeType] = (
[]
) # List of tuples containing experiments and their feedback, organized over time.
self.dag_parent: list[tuple[int, ...]] = [] # List of tuples representing parent indices in the DAG structure.
# (,) represents no parent; (1,) presents one parent; (1, 2) represents two parents.
# TODO: self.hist is 2-tuple now, remove hypothesis from it, change old code for this later.
self.knowledge_base: ASpecificKB | None = knowledge_base
@@ -128,13 +135,32 @@ class Trace(Generic[ASpecificScen, ASpecificKB]):
return None, None
class CheckpointSelector:
"""
In the trace, we may start from any check point (we'll represent it as a variable `from_checkpoint_idx`)
"""
@abstractmethod
def get_selection(self, trace: Trace) -> tuple[int, ...] | None:
"""
checkpoint_idx represents the place where we want to create a new node.
the return value should be the idx of target node (the parent of the new generating node).
- `(-1, )` represents starting from the latest trial in the trace - default value
- `(idx, )` represents starting from the `idx`-th trial in the trace.
- `None` represents starting from scratch (start a new trace)
- More advanced selection strategies in `select.py`
"""
class ExpGen(ABC):
def __init__(self, scen: Scenario) -> None:
self.scen = scen
@abstractmethod
def gen(self, trace: Trace) -> Experiment:
def gen(self, trace: Trace, selection: tuple[int, ...] = (-1,)) -> Experiment:
"""
Generate the experiment based on the trace.
@@ -86,7 +86,10 @@ class DSExperiment2Feedback(Experiment2Feedback):
decision=False,
)
system_prompt = T(".prompts:exp_feedback.system").r(scenario=self.scen.get_scenario_all_desc())
eda_output = exp.experiment_workspace.file_dict.get("EDA.md", None)
system_prompt = T(".prompts:exp_feedback.system").r(
scenario=self.scen.get_scenario_all_desc(eda_output=eda_output)
)
user_prompt = T(".prompts:exp_feedback.user").r(
sota_desc=sota_desc,
cur_exp=exp,
@@ -124,7 +124,7 @@ class DSCoSTEERCoSTEEREvaluator(CoSTEEREvaluator):
stdout += f"\nMLEBench submission check:\n{submission_check_out}\nIf MLEBench 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. "
system_prompt = T(".prompts:DSCoSTEER_eval.system").r(
scenario=self.scen.get_scenario_all_desc(),
scenario=self.scen.get_scenario_all_desc(eda_output=implementation.file_dict.get("EDA.md", None)),
task_desc=target_task.get_task_information(),
)
user_prompt = T(".prompts:DSCoSTEER_eval.user").r(
@@ -20,7 +20,11 @@ class DSExpGen(ExpGen):
def __init__(self, scen: DataScienceScen) -> None:
super().__init__(scen)
def gen(self, trace: DSTrace) -> DSExperiment:
def gen(self, trace: DSTrace, selection: tuple[int, ...] = (-1,)) -> DSExperiment:
# set the current selection for the trace
# handy design:dynamically change the "current selection" attribute of the trace, and we donot need to pass selection as an argument to other functions
trace.set_current_selection(selection)
if DS_RD_SETTING.proposal_version not in ["v1", "v2"]:
return import_class(DS_RD_SETTING.proposal_version)(scen=self.scen).gen(trace=trace)
@@ -39,51 +39,147 @@ class DSHypothesis(Hypothesis):
class DSTrace(Trace[DataScienceScen, KnowledgeBase]):
def __init__(self, scen: DataScienceScen, knowledge_base: KnowledgeBase | None = None) -> None:
self.scen: DataScienceScen = scen
self.hist: list[tuple[DSExperiment, ExperimentFeedback]] = []
"""
The dag_parent is a list of tuples, each tuple is the parent index of the current node.
The first element of the tuple is the parent index, the rest are the parent indexes of the parent (not implemented yet).
If the current node is the root node without parent, the tuple is empty.
"""
self.dag_parent: list[tuple[int, ...]] = [] # List of tuples representing parent indices in the DAG structure.
# () represents no parent; (1,) presents one parent; (1, 2) represents two parents.
self.knowledge_base = knowledge_base
self.current_selection: tuple[int, ...] = (-1,)
COMPLETE_ORDER = ("DataLoadSpec", "FeatureEng", "Model", "Ensemble", "Workflow")
def next_incomplete_component(self) -> COMPONENT | None:
def get_current_selection(self) -> tuple[int, ...]:
return self.current_selection
def set_current_selection(self, selection: tuple[int, ...]) -> None:
self.current_selection = selection
def sync_dag_parent_and_hist(
self,
) -> None:
"""
Adding corresponding parent index to the dag_parent when the hist is going to be changed.
Should be called when the hist is changed.
"""
if len(self.hist) == 0 or len(self.get_current_selection()) == 0:
# the node we are going to add is the first node of hist / root node of a new sub-trace
self.dag_parent.append(())
else:
current_node_idx = self.current_selection[0]
if current_node_idx == -1:
# the current selection is the latest one
current_node_idx = len(self.hist) - 1
self.dag_parent.append((current_node_idx,))
def retrieve_search_list(
self, search_type: Literal["all", "ancestors"] = "ancestors"
) -> list[tuple[DSExperiment, ExperimentFeedback]]:
"""
Retrieve the search list based on the selection and search_type.
Parameters
----------
search_type : str
One of "all", "ancestors".
- "all": search the whole hist.
- "ancestors": search the trace from root to the selection.
Returns
-------
list[tuple[DSExperiment, ExperimentFeedback]]
The search list.
"""
selection = self.get_current_selection()
if selection is None:
# selection is None, which means we switch to a new trace, which is not implemented yet
return []
return self.collect_all_ancestors(selection) if search_type == "ancestors" else self.hist
def collect_all_ancestors(
self,
selection: tuple[int, ...] = (-1,),
) -> list[tuple[DSExperiment, ExperimentFeedback]]:
"""
Collect all ancestors of the given selection.
The return list follows the order of [root->...->parent->current_node].
"""
if len(self.dag_parent) == 0:
return []
else:
all_ancestors = []
# start from the latest selection
current_node_idx = selection[0]
# add the current node to the list
all_ancestors.insert(0, self.hist[current_node_idx])
parent_idx = self.dag_parent[current_node_idx]
while len(parent_idx) > 0:
all_ancestors.insert(0, self.hist[parent_idx[0]])
parent_idx = self.dag_parent[parent_idx[0]]
return all_ancestors
def next_incomplete_component(
self,
search_type: Literal["all", "ancestors"] = "ancestors",
) -> COMPONENT | None:
"""
NOTE:
- A component will be complete until get True decision feedback !!!
"""
search_list = self.retrieve_search_list(search_type)
for c in self.COMPLETE_ORDER:
if not self.has_component(c):
"""Check if the component is in the ancestors of the selection."""
if not self.has_component(c, search_list):
return c
return None
def has_component(self, component: COMPONENT) -> bool:
for exp, fb in self.hist:
def has_component(
self, component: COMPONENT, search_list: list[tuple[DSExperiment, ExperimentFeedback]] = []
) -> bool:
for exp, fb in search_list:
assert isinstance(exp.hypothesis, DSHypothesis), "Hypothesis should be DSHypothesis (and not None)"
if exp.hypothesis.component == component and fb:
return True
return False
def experiment_and_feedback_list_after_init(
self, return_type: Literal["sota", "failed", "all"]
self,
return_type: Literal["sota", "failed", "all"],
search_type: Literal["all", "ancestors"] = "all",
) -> list[tuple[DSExperiment, ExperimentFeedback]]:
"""
Retrieve a list of experiments and feedbacks based on the return_type.
Parameters
----------
return_type : str
One of "sota", "failed", "all".
Returns
-------
list[tuple[DSExperiment, ExperimentFeedback]]
List of experiments and feedbacks.
"""
search_list = self.retrieve_search_list(search_type)
final_component = self.COMPLETE_ORDER[-1]
has_final_component = True if DS_RD_SETTING.coder_on_whole_pipeline else False
exp_and_feedback_list = []
for exp, fb in self.hist:
for exp, fb in search_list:
if has_final_component:
if return_type == "all":
exp_and_feedback_list.append((exp, fb))
@@ -95,34 +191,63 @@ class DSTrace(Trace[DataScienceScen, KnowledgeBase]):
has_final_component = True
return exp_and_feedback_list
def sota_experiment(self) -> DSExperiment | None:
def sota_experiment(
self,
search_type: Literal["all", "ancestors"] = "ancestors",
) -> DSExperiment | None:
"""
Returns
-------
Experiment or None
The experiment result if found, otherwise None.
"""
search_list = self.retrieve_search_list(search_type)
if DS_RD_SETTING.coder_on_whole_pipeline or self.next_incomplete_component() is None:
for exp, ef in self.hist[::-1]:
for exp, ef in search_list[::-1]:
# the sota exp should be accepted decision and all required components are completed.
if ef.decision:
return exp
return None
def last_successful_exp(self) -> DSExperiment | None:
def last_successful_exp(
self,
search_type: Literal["all", "ancestors"] = "ancestors",
) -> DSExperiment | None:
"""
Access the last successful experiment even part of the components are not completed.
"""
for exp, ef in self.hist[::-1]:
search_list = self.retrieve_search_list(search_type)
for exp, ef in search_list[::-1]:
if ef.decision:
return exp
return None
def last_runnable_exp_fb(self) -> tuple[DSExperiment, ExperimentFeedback] | None:
def last_exp(
self,
search_type: Literal["all", "ancestors"] = "ancestors",
) -> DSExperiment | None:
"""
Access the last experiment
"""
search_list = self.retrieve_search_list(search_type)
for exp, ef in search_list[::-1]:
return exp
return None
def last_runnable_exp_fb(
self,
search_type: Literal["all", "ancestors"] = "ancestors",
) -> tuple[DSExperiment, ExperimentFeedback] | None:
"""
Access the last runnable experiment (no exception, usually not all task failed) and feedback
"""
for exp, ef in self.hist[::-1]:
search_list = self.retrieve_search_list(search_type)
for exp, ef in search_list[::-1]:
if ef.exception is None:
return exp, ef
return None
@@ -63,9 +63,15 @@ class DSDraftExpGen(ExpGen):
scenario_desc: Description of the current scenario
last_successful_exp: Last successful experiment or None
spec_file: Path to specification file if needed
selection: The selection of the node to generate the task
"""
scenario_desc = trace.scen.get_scenario_all_desc()
last_successful_exp = trace.last_successful_exp()
# typecheck on the last successful exp, should be DSExperiment
if not isinstance(last_successful_exp, DSExperiment):
eda_output = None
else:
eda_output = last_successful_exp.experiment_workspace.file_dict.get("EDA.md", None)
scenario_desc = trace.scen.get_scenario_all_desc(eda_output=eda_output)
init_component_config = {
"DataLoadSpec": {"task_cls": DataLoaderTask, "spec_file": None, "component_prompt_key": "data_loader"},
"FeatureEng": {"task_cls": FeatureTask, "spec_file": "spec/feature.md", "component_prompt_key": "feature"},
@@ -78,8 +84,9 @@ class DSDraftExpGen(ExpGen):
component_prompt_key = init_component_config[component].get("component_prompt_key")
former_tasks_desc = ""
if len(trace.hist) > 0:
for exp, fb in reversed(trace.hist):
search_list = trace.retrieve_search_list()
if len(search_list) > 0:
for exp, fb in reversed(search_list):
if exp is not last_successful_exp:
former_task_desc = exp.pending_tasks_list[0][0].get_task_information()
former_task_desc += f"\n\nYou have tried to implement the same component and got the following exception: \n{fb.exception}\n Please try different methods to avoid the same errors and results in an infinite loop"
@@ -67,12 +67,17 @@ class DSProposalV1ExpGen(ExpGen):
# - Previous Feedback
# - Current sota implementation (encourage change based on it)
# - Extra RAG
scenario_desc = trace.scen.get_scenario_all_desc()
sota_exp = trace.sota_experiment()
if not isinstance(sota_exp, DSExperiment):
eda_output = None
else:
eda_output = sota_exp.experiment_workspace.file_dict.get("EDA.md", None)
scenario_desc = trace.scen.get_scenario_all_desc(eda_output=eda_output)
assert sota_exp is not None, "SOTA experiment is not provided."
exp_and_feedback = trace.hist[-1]
last_exp = exp_and_feedback[0]
last_exp = trace.last_exp()
# exp_and_feedback = trace.hist[-1]
# last_exp = exp_and_feedback[0]
# Step 1: Generate component
# Describe current best solution using shared template
@@ -395,7 +400,11 @@ class DSProposalV2ExpGen(ExpGen):
)
sota_exp = trace.sota_experiment()
scenario_desc = trace.scen.get_scenario_all_desc()
if not isinstance(sota_exp, DSExperiment):
eda_output = None
else:
eda_output = sota_exp.experiment_workspace.file_dict.get("EDA.md", None)
scenario_desc = trace.scen.get_scenario_all_desc(eda_output=eda_output)
competition_desc = trace.scen.get_competition_full_desc()
sota_exp_desc = T("scenarios.data_science.share:describe.exp").r(
@@ -0,0 +1,111 @@
from rdagent.core.proposal import CheckpointSelector, Trace
# # TODO: more advanced selector
# # TODO/Discussion: load selector function here or define selector class in `proposal.py`?
class LatestCKPSelector(CheckpointSelector):
"""
-`(-1, )` represents starting from the latest trial in the trace
"""
def get_selection(self, trace: Trace) -> tuple[int, ...]:
return (-1,)
class SOTAJumpCKPSelector(CheckpointSelector):
"""
SOTA jump policy:
if the cumulative SOTA in a window is below a threshold, jump to a new trial
otherwise, continue the current latest trial
"""
def __init__(self) -> None:
self.SOTA_COUNT_WINDOW = 5
self.SOTA_COUNT_THRESHOLD = 1 # start to compute cumulative SOTA ratio after 5 trials
def get_selection(self, trace: Trace) -> tuple[int, ...] | None:
current_trace = trace.retrieve_search_list(search_type="ancestors")
if len(trace.hist) > 0 and len(current_trace) > self.SOTA_COUNT_WINDOW:
all_exp_list = trace.experiment_and_feedback_list_after_init(return_type="all", search_type="ancestors")
# sota_exp_list = trace.experiment_and_feedback_list_after_init(return_type="sota", search_type="ancestors")
exp_list_in_window = all_exp_list[-self.SOTA_COUNT_WINDOW :]
# compute the cumulative SOTA ratio in the window
sota_count = 0
for exp, fb in exp_list_in_window:
if fb.decision:
sota_count += 1
if sota_count < self.SOTA_COUNT_THRESHOLD:
return None
else:
return (-1,)
else:
return (-1,)
# TODO: implement these selectors and more
class GlobalGreedyCKPSelector(CheckpointSelector):
"""
global greedy selector: select the trial with best performance globally (in trace.hist)
consistent with the greedy strategy in AIDE
not implemented yet
"""
def get_selection(self, trace: Trace) -> tuple[int, ...]:
return (-1,)
class LocalGreedyCKPSelector(CheckpointSelector):
"""
local greedy selector: select the trial with best performance locally (in trace.ancestors)
not implemented yet
"""
def get_selection(self, trace: Trace) -> tuple[int, ...]:
return (-1,)
class BugBufferCKPSelector(CheckpointSelector):
"""
bug buffer selector: with limit-size bug buffer size, start a new trace if buffer exceeds.
not implemented yet
"""
def __init__(self) -> None:
self.bug_count = 0
self.BUG_BUFFER_SIZE = 10
def get_selection(self, trace: Trace) -> tuple[int, ...]:
if self.bug_count < self.BUG_BUFFER_SIZE:
return (-1,)
else:
return None
class RandomCKPSelector(CheckpointSelector):
def get_selection(self, trace: Trace) -> tuple[int, ...]:
"""
random selector: select the trial randomly
not implemented yet
"""
return (-1,)
class BuggyCKPSelector(CheckpointSelector):
def get_selection(self, trace: Trace) -> tuple[int, ...]:
"""
buggy selector: select the most recent trial with buggy performance
not implemented yet
"""
return (-1,)
@@ -32,7 +32,6 @@ class DataScienceScen(Scenario):
self.processed_data_folder_description = self._get_data_folder_description()
self._analysis_competition_description()
self.metric_direction = self._get_direction()
self.eda_output = None
def _get_description(self):
if (fp := Path(f"{DS_RD_SETTING.local_data_path}/{self.competition}.json")).exists():
@@ -106,15 +105,18 @@ class DataScienceScen(Scenario):
competition=self.competition,
)
def get_scenario_all_desc(self) -> str:
def get_scenario_all_desc(self, eda_output=None) -> str:
"""
eda_output depends on dynamic .md files from current workspace, not fixed.
"""
return T(".prompts:scenario_description").r(
background=self.background,
submission_specifications=self.submission_specifications,
evaluation=self.metric_description,
metric_name=self.metric_name,
metric_direction=self.metric_direction,
eda_output=self.eda_output,
time_limit=f"{DS_RD_SETTING.full_timeout / 60 / 60 : .2f} hours",
eda_output=eda_output,
)
def get_runtime_environment(self) -> str: