From 07c228ef9fb9f4be8e05ab261da53810f7238b3a Mon Sep 17 00:00:00 2001 From: Xu Yang Date: Thu, 18 Sep 2025 11:10:12 +0800 Subject: [PATCH] feat: add user interaction in data science scenario (#1251) * feat: add interactor classes and user interaction handling for experiments * update code * use fragment retry mechanism instead of rerun() * fix a bug * integrate user instructions into proposal and coder * fix CI * fix CI * feat: add approval option for user instructions submission * feat: enhance user instructions handling in Task and DSExperiment classes * fix CI * add user instructions into hypothesis rewrite * add interface to command line --------- Co-authored-by: Bowen Xian --- rdagent/app/cli.py | 9 + rdagent/app/data_science/conf.py | 5 + .../coder/data_science/pipeline/prompts.yaml | 3 +- rdagent/components/interactor/__init__.py | 17 ++ rdagent/components/workflow/conf.py | 1 + rdagent/core/experiment.py | 36 +++- rdagent/core/interactor.py | 26 +++ rdagent/log/ui/ds_user_interact.py | 172 ++++++++++++++++++ .../data_science/experiment/experiment.py | 13 +- .../data_science/interactor/__init__.py | 120 ++++++++++++ rdagent/scenarios/data_science/loop.py | 3 + .../proposal/exp_gen/prompts_v2.yaml | 22 ++- .../data_science/proposal/exp_gen/proposal.py | 33 +++- 13 files changed, 448 insertions(+), 12 deletions(-) create mode 100644 rdagent/components/interactor/__init__.py create mode 100644 rdagent/core/interactor.py create mode 100644 rdagent/log/ui/ds_user_interact.py create mode 100644 rdagent/scenarios/data_science/interactor/__init__.py diff --git a/rdagent/app/cli.py b/rdagent/app/cli.py index 81689ac5..dad1920f 100644 --- a/rdagent/app/cli.py +++ b/rdagent/app/cli.py @@ -61,6 +61,14 @@ def server_ui(port=19899): subprocess.run(["python", "rdagent/log/server/app.py", f"--port={port}"]) +def ds_user_interact(port=19900): + """ + start web app to show the log traces in real time + """ + commands = ["streamlit", "run", "rdagent/log/ui/ds_user_interact.py", f"--server.port={port}"] + subprocess.run(commands) + + app.command(name="fin_factor")(fin_factor) app.command(name="fin_model")(fin_model) app.command(name="fin_quant")(fin_quant) @@ -72,6 +80,7 @@ app.command(name="ui")(ui) app.command(name="server_ui")(server_ui) app.command(name="health_check")(health_check) app.command(name="collect_info")(collect_info) +app.command(name="ds_user_interact")(ds_user_interact) if __name__ == "__main__": diff --git a/rdagent/app/data_science/conf.py b/rdagent/app/data_science/conf.py index cdcdaae7..ce7dbe62 100644 --- a/rdagent/app/data_science/conf.py +++ b/rdagent/app/data_science/conf.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Literal from pydantic_settings import SettingsConfigDict @@ -20,6 +21,7 @@ class DataScienceBasePropSetting(KaggleBasePropSetting): planner: str = "rdagent.scenarios.data_science.proposal.exp_gen.planner.DSExpPlannerHandCraft" hypothesis_gen: str = "rdagent.scenarios.data_science.proposal.exp_gen.router.ParallelMultiTraceExpGen" + interactor: str = "rdagent.components.interactor.SkipInteractor" trace_scheduler: str = "rdagent.scenarios.data_science.proposal.exp_gen.trace_scheduler.RoundRobinScheduler" """Hypothesis generation class""" @@ -182,6 +184,9 @@ class DataScienceBasePropSetting(KaggleBasePropSetting): ensemble_time_upper_bound: bool = False + user_interaction_wait_seconds: int = 6000 # seconds to wait for user interaction + user_interaction_mid_folder: Path = Path.cwd() / "git_ignore_folder" / "RD-Agent_user_interaction" + DS_RD_SETTING = DataScienceBasePropSetting() diff --git a/rdagent/components/coder/data_science/pipeline/prompts.yaml b/rdagent/components/coder/data_science/pipeline/prompts.yaml index b92d03a9..55b13ce5 100644 --- a/rdagent/components/coder/data_science/pipeline/prompts.yaml +++ b/rdagent/components/coder/data_science/pipeline/prompts.yaml @@ -169,7 +169,8 @@ pipeline_coder: 8. **Try-except blocks are ONLY allowed when reading files. If no files are successfully read, it indicates incorrect file paths or reading methods, not a try-except issue. Try-except is PROHIBITED elsewhere in the code. Assert statements are PROHIBITED throughout the entire code.** 9. ATTENTION: ALWAYS use the best saved model (not necessarily final epoch) for predictions. **NEVER create dummy/placeholder submissions (e.g., all 1s, random values)**. If training fails, report failure honestly rather than generating fake submission files. 10. You should ALWAYS generate the complete code rather than partial code. - 11. Strictly follow all specifications and general guidelines described above. + 11. If the task contains any user instructions, you must strictly follow them. User instructions have the highest priority and should be followed even if they conflict with other specifications or guidelines. + 12. Strictly follow all specifications and general guidelines described above. ### Output Format {% if out_spec %} diff --git a/rdagent/components/interactor/__init__.py b/rdagent/components/interactor/__init__.py new file mode 100644 index 00000000..ec129d4c --- /dev/null +++ b/rdagent/components/interactor/__init__.py @@ -0,0 +1,17 @@ +from rdagent.core.experiment import ASpecificExp +from rdagent.core.interactor import Interactor +from rdagent.core.proposal import Trace + + +class SkipInteractor(Interactor[ASpecificExp]): + + def interact(self, exp: ASpecificExp, trace: Trace) -> ASpecificExp: + """ + Interact with the user to get feedback or confirmation. + + Responsibilities: + - Present the current state of the experiment to the user. + - Collect user input to guide the next steps in the experiment. + - Rewrite the experiment based on user feedback. + """ + return exp diff --git a/rdagent/components/workflow/conf.py b/rdagent/components/workflow/conf.py index d6e87960..0f37c6e1 100644 --- a/rdagent/components/workflow/conf.py +++ b/rdagent/components/workflow/conf.py @@ -11,6 +11,7 @@ class BasePropSetting(ExtendedBaseSettings): knowledge_base: str = "" knowledge_base_path: str = "" hypothesis_gen: str = "" + interactor: str = "" hypothesis2experiment: str = "" coder: str = "" runner: str = "" diff --git a/rdagent/core/experiment.py b/rdagent/core/experiment.py index 43c79dc6..a84159be 100644 --- a/rdagent/core/experiment.py +++ b/rdagent/core/experiment.py @@ -13,7 +13,7 @@ from collections.abc import Sequence from copy import deepcopy from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, List, TypeVar from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.evaluation import Feedback @@ -48,13 +48,28 @@ class AbsTask(ABC): """ +class UserInstructions(List[str]): + def __str__(self) -> str: + if self: + return ("\nUser Instructions (Top priority!):\n" + "\n".join(f"- {ui}" for ui in self)) if self else "" + else: + return "" + + class Task(AbsTask): - def __init__(self, name: str, version: int = 1, description: str = "") -> None: + def __init__( + self, + name: str, + version: int = 1, + description: str = "", + user_instructions: UserInstructions | None = None, + ) -> None: super().__init__(name, version) self.description = description + self.user_instructions = user_instructions def get_task_information(self) -> str: - return f"Task Name: {self.name}\nDescription: {self.description}" + return f"Task Name: {self.name}\nDescription: {self.description}{str(self.user_instructions)}" def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.name}>" @@ -410,6 +425,21 @@ class Experiment( self.plan: ExperimentPlan | None = ( None # To store the planning information for this experiment, should be generated inside exp_gen.gen ) + self.user_instructions: UserInstructions | None = None # To store the user instructions for this experiment + + def set_user_instructions(self, user_instructions: UserInstructions | None) -> None: + if user_instructions is None: + return + if not isinstance(user_instructions, UserInstructions) and isinstance(user_instructions, list): + user_instructions = UserInstructions(user_instructions) + self.user_instructions = user_instructions + for ws in self.sub_workspace_list: + if ws is not None: + ws.target_task.user_instructions = user_instructions # type: ignore[union-attr] + for task in self.sub_tasks: + task.user_instructions = user_instructions + if self.experiment_workspace is not None and self.experiment_workspace.target_task is not None: + self.experiment_workspace.target_task.user_instructions = user_instructions @property def result(self) -> object: diff --git a/rdagent/core/interactor.py b/rdagent/core/interactor.py new file mode 100644 index 00000000..e4044db2 --- /dev/null +++ b/rdagent/core/interactor.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from trace import Trace +from typing import TYPE_CHECKING, Generic + +from rdagent.core.experiment import ASpecificExp + +if TYPE_CHECKING: + from rdagent.core.scenario import Scenario + + +class Interactor(ABC, Generic[ASpecificExp]): + def __init__(self, scen: Scenario) -> None: + self.scen: Scenario = scen + + @abstractmethod + def interact(self, exp: ASpecificExp, trace: Trace | None = None) -> ASpecificExp: + """ + Interact with the experiment to get feedback or confirmation. + + Responsibilities: + - Present the current state of the experiment. + - Collect input to guide the next steps in the experiment. + - Rewrite the experiment based on feedback. + """ diff --git a/rdagent/log/ui/ds_user_interact.py b/rdagent/log/ui/ds_user_interact.py new file mode 100644 index 00000000..c277ac83 --- /dev/null +++ b/rdagent/log/ui/ds_user_interact.py @@ -0,0 +1,172 @@ +import json +import pickle +import time +from datetime import datetime, timedelta +from pathlib import Path + +import streamlit as st +from streamlit import session_state as state + +from rdagent.app.data_science.conf import DS_RD_SETTING + +st.set_page_config(layout="wide", page_title="RD-Agent_user_interact", page_icon="🎓", initial_sidebar_state="expanded") + +# 初始化session state +if "sessions" not in state: + state.sessions = {} +if "selected_session_name" not in state: + state.selected_session_name = None + + +def render_main_content(): + """渲染主要内容区域""" + if state.selected_session_name is not None and state.selected_session_name in state.sessions: + selected_session_data = state.sessions[state.selected_session_name] + if selected_session_data is not None: + st.title( + f"Session: {state.selected_session_name[:4]} with competition {selected_session_data['competition']}" + ) + st.title("Contextual Information:") + st.subheader("Competition scenario:", divider=True) + scenario = st.code(selected_session_data["scenario_description"], language="yaml") + st.subheader("Former attempts summary:", divider=True) + scenario = st.code(selected_session_data["ds_trace_desc"], language="yaml") + if selected_session_data["current_code"] != "": + st.subheader("Current SOTA code", divider=True) + scenario = st.code( + body=selected_session_data["current_code"], + language="python", + ) + + st.subheader("Hypothesis candidates:", divider=True) + hypothesis_candidates = selected_session_data["hypothesis_candidates"] + tabs = st.tabs( + [ + f"{'✅' if i == selected_session_data['target_hypothesis_index'] or selected_session_data['target_hypothesis_index'] == -1 else ''}Hypothesis {i+1}" + for i in range(len(hypothesis_candidates)) + ] + ) + for index, hypothesis in enumerate(hypothesis_candidates): + with tabs[index]: + st.code(str(hypothesis), language="yaml") + st.text("✅ means picked as target hypothesis") + + st.title("Decisions to make:") + + with st.form(key="user_form"): + st.caption("Please modify the fields below and submit to provide your feedback.") + target_hypothesis = st.text_area( + "Target hypothesis: (you can copy from candidates)", + value=(original_hypothesis := selected_session_data["target_hypothesis"].hypothesis), + height="content", + ) + target_task = st.text_area( + "Target task description:", + value=(original_task_desc := selected_session_data["task"].description), + height="content", + ) + original_user_instruction = selected_session_data.get("user_instruction") + user_instruction_list = [] + if selected_session_data.get("former_user_instructions") is not None: + st.caption( + "Former user instructions, you can modify or delete the content to remove certain instruction." + ) + for user_instruction in selected_session_data.get("former_user_instructions"): + user_instruction_list.append( + st.text_area("Former user instruction", value=user_instruction, height="content") + ) + user_instruction_list.append(st.text_area("Add new user instruction", value="", height="content")) + submit = st.form_submit_button("Submit") + approve = st.form_submit_button("Approve without changes") + + if submit or approve: + if approve: + submit_dict = { + "action": "confirm", + } + else: + user_instruction_str_list = [ui for ui in user_instruction_list if ui.strip() != ""] + user_instruction_str_list = ( + None if len(user_instruction_str_list) == 0 else user_instruction_str_list + ) + action = ( + "confirm" + if target_hypothesis == original_hypothesis + and target_task == original_task_desc + and user_instruction_str_list == original_user_instruction + else "rewrite" + ) + submit_dict = { + "target_hypothesis": target_hypothesis, + "task_description": target_task, + "user_instruction": user_instruction_str_list, + "action": action, + } + json.dump( + submit_dict, + open( + DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}_RET.json", "w" + ), + ) + Path(DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}.pkl").unlink( + missing_ok=True + ) + st.success("Your feedback has been submitted. Thank you!") + time.sleep(5) + state.selected_session_name = None + + if st.button("Extend expiration by 60s"): + session_data = pickle.load( + open(DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}.pkl", "rb") + ) + session_data["expired_datetime"] = session_data["expired_datetime"] + timedelta(seconds=60) + pickle.dump( + session_data, + open(DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}.pkl", "wb"), + ) + else: + st.warning("Please select a session from the sidebar.") + + +# 每秒更新一次sessions +@st.fragment(run_every=1) +def update_sessions(): + log_folder = Path(DS_RD_SETTING.user_interaction_mid_folder) + state.sessions = {} + for session_file in log_folder.glob("*.pkl"): + try: + session_data = pickle.load(open(session_file, "rb")) + if session_data["expired_datetime"] > datetime.now(): + state.sessions[session_file.stem] = session_data + else: + session_file.unlink(missing_ok=True) + ret_file = log_folder / f"{session_file.stem}_RET.json" + ret_file.unlink(missing_ok=True) + except Exception as e: + continue + render_main_content() + + +@st.fragment(run_every=1) +def render_sidebar(): + st.title("R&D-Agent User Interaction Portal") + if state.sessions: + st.header("Active Sessions") + st.caption("Click a session to view:") + session_names = [name for name in state.sessions] + for session_name in session_names: + with st.container(border=True): + remaining = state.sessions[session_name]["expired_datetime"] - datetime.now() + total_sec = int(remaining.total_seconds()) + label = f"{total_sec}s to expire" if total_sec > 0 else "Expired" + if st.button(f"session id:{session_name[:4]}", key=f"session_btn_{session_name}"): + state.selected_session_name = session_name + state.data = state.sessions[session_name] + st.markdown(f"⏳ {label}") + else: + st.warning("No active sessions available. Please wait.") + + +update_sessions() +with st.sidebar: + render_sidebar() diff --git a/rdagent/scenarios/data_science/experiment/experiment.py b/rdagent/scenarios/data_science/experiment/experiment.py index 07f0f4c8..209effda 100644 --- a/rdagent/scenarios/data_science/experiment/experiment.py +++ b/rdagent/scenarios/data_science/experiment/experiment.py @@ -3,13 +3,13 @@ from typing import Literal import pandas as pd -from rdagent.core.experiment import Experiment, FBWorkspace, Task +from rdagent.core.experiment import Experiment, FBWorkspace, Task, UserInstructions COMPONENT = Literal["DataLoadSpec", "FeatureEng", "Model", "Ensemble", "Workflow", "Pipeline"] class DSExperiment(Experiment[Task, FBWorkspace, FBWorkspace]): - def __init__(self, pending_tasks_list: list, *args, **kwargs) -> None: + def __init__(self, pending_tasks_list: list, hypothesis_candidates: list | None = None, *args, **kwargs) -> None: super().__init__(sub_tasks=[], *args, **kwargs) # Status # - Initial: blank; @@ -18,11 +18,20 @@ class DSExperiment(Experiment[Task, FBWorkspace, FBWorkspace]): # the initial workspace or the successful new version after coding self.experiment_workspace = FBWorkspace() self.pending_tasks_list = pending_tasks_list + self.hypothesis_candidates = hypothesis_candidates self.format_check_result = None # this field is optional. It is not none only when we have a format checker. Currently, only following cases are supported. # - mle-bench + def set_user_instructions(self, user_instructions: UserInstructions | None): + super().set_user_instructions(user_instructions) + if user_instructions is None: + return + for task_list in self.pending_tasks_list: + for task in task_list: + task.user_instructions = user_instructions + def is_ready_to_run(self) -> bool: """ ready to run does not indicate the experiment is runnable diff --git a/rdagent/scenarios/data_science/interactor/__init__.py b/rdagent/scenarios/data_science/interactor/__init__.py new file mode 100644 index 00000000..b1ee080f --- /dev/null +++ b/rdagent/scenarios/data_science/interactor/__init__.py @@ -0,0 +1,120 @@ +import json +import pickle +import time +import uuid +from abc import abstractmethod +from datetime import datetime, timedelta +from pathlib import Path + +from rdagent.app.data_science.conf import DS_RD_SETTING +from rdagent.core.experiment import Task +from rdagent.core.interactor import Interactor +from rdagent.scenarios.data_science.experiment.experiment import DSExperiment +from rdagent.scenarios.data_science.proposal.exp_gen.base import DSHypothesis, DSTrace +from rdagent.utils.agent.tpl import T + + +class DSInteractor(Interactor[DSExperiment]): + @abstractmethod + def dump_and_wait_for_user_input( + self, + scenario_description: str, + ds_trace_desc: str, + current_code: str, + hypothesis_candidates: list[str], + target_hypothesis: DSHypothesis, + target_hypothesis_index: int, + task_description: Task, + exp: DSExperiment, + ) -> DSExperiment: + raise NotImplementedError + + def interact(self, exp: DSExperiment, trace: DSTrace) -> DSExperiment: + """ + Interact with the experiment to get feedback or confirmation. + + Responsibilities: + - Present the current state of the experiment. + - Collect input to guide the next steps in the experiment. + - Rewrite the experiment based on feedback. + """ + scenario_description = self.scen.get_scenario_all_desc( + eda_output=exp.experiment_workspace.file_dict.get("EDA.md", None) + ) + ds_trace_desc = T("scenarios.data_science.share:describe.trace").r( + exp_and_feedback_list=trace.experiment_and_feedback_list_after_init(return_type="all"), + type="all", + pipeline=DS_RD_SETTING.coder_on_whole_pipeline, + ) + current_code = exp.experiment_workspace.file_dict.get("main.py", "") + target_hypothesis = exp.hypothesis + + hypothesis_str_candidates = [hypo.hypothesis for hypo in exp.hypothesis_candidates] + target_hypothesis_index = ( + hypothesis_str_candidates.index(target_hypothesis.hypothesis) + if target_hypothesis.hypothesis in hypothesis_str_candidates and not trace.is_selection_new_tree() + else -1 + ) + return self.dump_and_wait_for_user_input( + scenario_description=scenario_description, + ds_trace_desc=ds_trace_desc, + current_code=current_code, + hypothesis_candidates=exp.hypothesis_candidates, + target_hypothesis=target_hypothesis, + target_hypothesis_index=target_hypothesis_index, + task=exp.pending_tasks_list[0][0], + exp=exp, + ) + + +class FBDSInteractor(DSInteractor): + def dump_and_wait_for_user_input( + self, + scenario_description: str, + ds_trace_desc: str, + current_code: str, + hypothesis_candidates: list[DSHypothesis], + target_hypothesis: DSHypothesis, + target_hypothesis_index: int, + task: Task, + exp: DSExperiment, + ) -> DSExperiment: + information_to_user = { + "competition": DS_RD_SETTING.competition, + "scenario_description": scenario_description, + "ds_trace_desc": ds_trace_desc, + "current_code": current_code, + "hypothesis_candidates": hypothesis_candidates, + "target_hypothesis": ( + hypothesis_candidates[target_hypothesis_index] if target_hypothesis_index != -1 else target_hypothesis + ), + "target_hypothesis_index": target_hypothesis_index, + "task": task, + "expired_datetime": datetime.now() + timedelta(seconds=DS_RD_SETTING.user_interaction_wait_seconds), + "former_user_instructions": exp.user_instructions, + } + session_id = uuid.uuid4().hex + DS_RD_SETTING.user_interaction_mid_folder.mkdir(parents=True, exist_ok=True) + pickle.dump(information_to_user, open(DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}.pkl", "wb")) + while ( + Path(DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}.pkl").exists() + and pickle.load(open(DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}.pkl", "rb"))[ + "expired_datetime" + ] + > datetime.now() + and not (DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}_RET.json").exists() + ): + time.sleep(5) + Path(DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}.pkl").unlink(missing_ok=True) + if not (DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}_RET.json").exists(): + return exp + else: + user_feedback = json.load(open(DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}_RET.json")) + if user_feedback["action"] == "confirm": + return exp + elif user_feedback["action"] == "rewrite": + exp.hypothesis.hypothesis = user_feedback["target_hypothesis"] + exp.pending_tasks_list[0][0].description = user_feedback["task_description"] + exp.set_user_instructions(user_feedback["user_instruction"]) + Path(DS_RD_SETTING.user_interaction_mid_folder / f"{session_id}_RET.json").unlink(missing_ok=True) + return exp diff --git a/rdagent/scenarios/data_science/loop.py b/rdagent/scenarios/data_science/loop.py index 7dfcb830..fdd69dc4 100644 --- a/rdagent/scenarios/data_science/loop.py +++ b/rdagent/scenarios/data_science/loop.py @@ -105,6 +105,8 @@ class DataScienceRDLoop(RDLoop): self.sota_exp_selector = import_class(PROP_SETTING.sota_exp_selector_name)() self.exp_gen: ExpGen = import_class(PROP_SETTING.hypothesis_gen)(scen) + self.interactor = import_class(PROP_SETTING.interactor)(scen) + # coders self.data_loader_coder = DataLoaderCoSTEER(scen) self.feature_coder = FeatureCoSTEER(scen) @@ -140,6 +142,7 @@ class DataScienceRDLoop(RDLoop): # in parallel + multi-trace mode, the above global "trace.current_selection" will not be used # instead, we will use the "local_selection" attached to each exp to in async_gen(). exp = await self.exp_gen.async_gen(self.trace, self) + exp = self.interactor.interact(exp, self.trace) logger.log_object(exp) return exp diff --git a/rdagent/scenarios/data_science/proposal/exp_gen/prompts_v2.yaml b/rdagent/scenarios/data_science/proposal/exp_gen/prompts_v2.yaml index fd14762a..d7a5567b 100644 --- a/rdagent/scenarios/data_science/proposal/exp_gen/prompts_v2.yaml +++ b/rdagent/scenarios/data_science/proposal/exp_gen/prompts_v2.yaml @@ -196,6 +196,12 @@ hypothesis_gen: 5. **Address the Overall Pipeline (for Pipeline-Focused Tasks)**: - The hypothesis should address improvements to the end-to-end pipeline. - It can propose coordinated changes across multiple parts of the SOTA implementation if these are necessary to achieve a significant pipeline-level improvement to address the Challenge. (Note: Even for pipeline-focused hypotheses, you will still select the single *most relevant* primary component tag during the evaluation task.) + + {% if former_user_instructions_str is not none %} + ## 1.3. Mandatory Consideration of Past User Instructions + The user has provided specific instructions in previous experiments. These instructions may contain critical insights or constraints that must be considered when formulating your hypotheses. Carefully review the following past user instructions and ensure that your proposed hypotheses align with these directives: + {{ former_user_instructions_str }} + {% endif %} # Task 2: Hypothesis Evaluation After proposing one hypothesis for each relevant Identified Challenge, evaluate each one. @@ -424,7 +430,13 @@ hypothesis_rewrite: {% endfor %} Your rewritten hypotheses **MUST** guide the agent towards different approaches, for example, different backbone models, different feature engineering methods, different ensemble strategies, different workflow optimizations, focus on efficiency etc. Avoid proposing hypotheses that are similar to those listed above. {% endif %} - + + {% if former_user_instructions_str is not none %} + # Mandatory Consideration of Past User Instructions + The user has provided specific instructions in previous experiments. These instructions may contain critical insights or constraints that must be considered when rewriting your hypotheses. Carefully review the following past user instructions and ensure that your rewritten hypotheses align with these directives: + {{ former_user_instructions_str }} + {% endif %} + {% if rewrite_output_format is not none %} ## Output Format {{ rewrite_output_format }} @@ -760,7 +772,13 @@ task_gen: If you are confident in a specific value based on strong evidence, prior experiments, or clear rationale, specify the value clearly. {% include "scenarios.data_science.share:spec.hyperparameter" %} - + {% if former_user_instructions_str is not none %} + # Mandatory Consideration of Past User Instructions + The user has provided specific instructions in previous experiments. These instructions may contain critical insights or constraints that must be considered in your sketch. + Carefully review and integrate these instructions into your design to ensure alignment with user expectations and requirements. + {{ former_user_instructions_str }} + {% endif %} + {% if task_output_format is not none %} # Output Format diff --git a/rdagent/scenarios/data_science/proposal/exp_gen/proposal.py b/rdagent/scenarios/data_science/proposal/exp_gen/proposal.py index 46514a0e..0cac34e6 100644 --- a/rdagent/scenarios/data_science/proposal/exp_gen/proposal.py +++ b/rdagent/scenarios/data_science/proposal/exp_gen/proposal.py @@ -15,6 +15,7 @@ from rdagent.components.coder.data_science.model.exp import ModelTask from rdagent.components.coder.data_science.pipeline.exp import PipelineTask from rdagent.components.coder.data_science.raw_data_loader.exp import DataLoaderTask from rdagent.components.coder.data_science.workflow.exp import WorkflowTask +from rdagent.core.experiment import UserInstructions from rdagent.core.proposal import ExpGen from rdagent.core.scenario import Scenario from rdagent.log import rdagent_logger as logger @@ -617,6 +618,7 @@ class DSProposalV2ExpGen(ExpGen): inject_diverse: bool = False, exp_gen_plan: Optional[Dict] = None, sibling_exp: List[DSExperiment] | None = None, + former_user_instructions: UserInstructions | None = None, ) -> Dict: problem_formatted_str = "" for i, (problem_name, problem_dict) in enumerate(problems.items()): @@ -641,6 +643,7 @@ class DSProposalV2ExpGen(ExpGen): generate_unique_hypothesis=DS_RD_SETTING.enable_generate_unique_hypothesis and is_new_tree, enable_simple_hypothesis=DS_RD_SETTING.enable_simple_hypothesis, sibling_hypotheses=sibling_hypotheses, + former_user_instructions_str=str(former_user_instructions) if former_user_instructions else None, ) user_prompt = T(".prompts_v2:hypothesis_gen.user").r( scenario_desc=scenario_desc, @@ -765,6 +768,7 @@ class DSProposalV2ExpGen(ExpGen): sota_exp_desc: str, exp_feedback_list_desc: str, sibling_exp: List[DSExperiment] | None = None, + former_user_instructions: UserInstructions | None = None, ) -> Dict: """ Generate improved hypotheses based on critique feedback for each original hypothesis. @@ -799,6 +803,7 @@ class DSProposalV2ExpGen(ExpGen): ), enable_scale_check=DS_RD_SETTING.enable_scale_check, sibling_hypotheses=sibling_hypotheses, + former_user_instructions_str=str(former_user_instructions) if former_user_instructions else None, ) user_prompt = T(".prompts_v2:hypothesis_rewrite.user").r( scenario_desc=scenario_desc, @@ -1172,10 +1177,12 @@ class DSProposalV2ExpGen(ExpGen): sota_exp_desc: str, sota_exp: DSExperiment, hypotheses: list[DSHypothesis], + hypotheses_candidates: list[DSHypothesis], pipeline: bool, failed_exp_feedback_list_desc: str, fb_to_sota_exp: ExperimentFeedback | None = None, sibling_exp: List[DSExperiment] | None = None, + former_user_instructions: UserInstructions = None, ) -> DSExperiment: if pipeline: component_info = get_component("Pipeline") @@ -1192,6 +1199,7 @@ class DSProposalV2ExpGen(ExpGen): metric_name=self.scen.metric_name, sibling_tasks=sibling_tasks, fix_seed_and_data_split=DS_RD_SETTING.fix_seed_and_data_split, + former_user_instructions_str=str(former_user_instructions) if former_user_instructions else None, ) user_prompt = T(".prompts_v2:task_gen.user").r( scenario_desc=scenario_desc, @@ -1242,7 +1250,9 @@ class DSProposalV2ExpGen(ExpGen): # Persist for later stages task.package_info = get_packages(pkgs) - exp = DSExperiment(pending_tasks_list=[[task]], hypothesis=hypotheses[0]) + exp = DSExperiment( + pending_tasks_list=[[task]], hypothesis=hypotheses[0], hypothesis_candidates=hypotheses_candidates + ) if sota_exp is not None: exp.experiment_workspace.inject_code_from_file_dict(sota_exp.experiment_workspace) @@ -1253,6 +1263,10 @@ class DSProposalV2ExpGen(ExpGen): description=task_dict.get("workflow_update", "No update needed"), ) exp.pending_tasks_list.append([workflow_task]) + + # 4) set user instructions + if former_user_instructions is not None: + exp.set_user_instructions(former_user_instructions) return exp def get_all_hypotheses(self, problem_dict: dict, hypothesis_dict: dict) -> list[DSHypothesis]: @@ -1315,12 +1329,17 @@ class DSProposalV2ExpGen(ExpGen): ) # all failed exp and feedbacks + failed_exp_feedback_list = trace.experiment_and_feedback_list_after_init(return_type="failed") failed_exp_feedback_list_desc = T("scenarios.data_science.share:describe.trace").r( - exp_and_feedback_list=trace.experiment_and_feedback_list_after_init(return_type="failed"), + exp_and_feedback_list=failed_exp_feedback_list, type="failed", pipeline=pipeline, ) - # + if len(failed_exp_feedback_list) == 0: + former_user_instructions = None + else: + former_user_instructions = failed_exp_feedback_list[-1][0].user_instructions + # NOTE: we currently don't support inject diverse problems for the parallel + multi-trace mode, if DS_RD_SETTING.enable_inject_diverse and len(trace.hist) > 0: if len(trace.current_selection) == 0: @@ -1371,6 +1390,7 @@ class DSProposalV2ExpGen(ExpGen): exp_gen_plan=plan.get("exp_gen") if plan else None, is_new_tree=is_new_tree, sibling_exp=sibling_exp, + former_user_instructions=former_user_instructions, ) if not pipeline: sota_exp_model_file_count = len( @@ -1415,6 +1435,7 @@ class DSProposalV2ExpGen(ExpGen): sota_exp_desc=sota_exp_desc, exp_feedback_list_desc=exp_feedback_list_desc, sibling_exp=sibling_exp, + former_user_instructions=former_user_instructions, ) logger.info(f"Successfully completed hypothesis critique and rewrite process") except Exception as e: @@ -1454,10 +1475,14 @@ class DSProposalV2ExpGen(ExpGen): sota_exp_desc=sota_exp_desc, sota_exp=sota_exp, hypotheses=( - [new_hypothesis] if len(trace.hist) > 0 else self.get_all_hypotheses(all_problems, hypothesis_dict) + [new_hypothesis] + if not trace.is_selection_new_tree() + else self.get_all_hypotheses(all_problems, hypothesis_dict) ), + hypotheses_candidates=self.get_all_hypotheses(all_problems, hypothesis_dict), pipeline=pipeline, failed_exp_feedback_list_desc=failed_exp_feedback_list_desc, fb_to_sota_exp=fb_to_sota_exp, sibling_exp=sibling_exp, + former_user_instructions=former_user_instructions, )