ReproAgent

    def get_stats(self):
        return {}

@dataclass
class ReproAgentArgs(GenericAgentArgs):
    # starting with "_" will prevent from being part of the index in the load_results function
    _repro_dir: str = None

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)

class ReproAgent(GenericAgent):
    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry=4,
        repro_dir=None,
    ):
        self.exp_result = ExpResult(repro_dir)
        super().__init__(chat_model_args, flags, max_retry)

    def get_action(self, obs):
        # replace the chat model with a reproducible chat that will mimic the
        # same answers
        step = len(self.actions)
        step_info = self.exp_result.get_step_info(step)
        old_chat_messages = step_info.agent_info.get("chat_messages", None)  # type: Discussion

        if old_chat_messages is None:
            err_msg = self.exp_result.summary_info["err_msg"]

            agent_info = AgentInfo(
                markdown_page=f"Agent had no chat messages. Perhaps there was an error. err_msg:\n{err_msg}",
            )
            return None, agent_info

        # an old bug prevented the response from being saved.
        if len(old_chat_messages) == 2:
            recorded_action = step_info.action
            if recorded_action:
                # Recreate the 3rd message based on the recorded action
                assistant_message = make_assistant_message(f"<action>{recorded_action}</action>")
                old_chat_messages.append(assistant_message)

        self.chat_llm = ReproChatModel(old_chat_messages)
        action, agent_info = super().get_action(obs)

        return _make_agent_stats(
            action, agent_info, step_info, old_chat_messages, self.chat_llm.new_messages
        )

def _make_agent_stats(action, agent_info, step_info, old_chat_messages, new_chat_messages):
    if isinstance(agent_info, dict):
        agent_info = AgentInfo(**agent_info)

    old_msg_str = _format_messages(old_chat_messages)
    new_msg_str = _format_messages(new_chat_messages)

    agent_info.html_page = _make_diff(old_str=old_msg_str, new_str=new_msg_str)
    agent_info.stats.update(_diff_stats(old_msg_str, new_msg_str))

    return action, agent_info

def _format_messages(messages: list[dict]):
    if isinstance(messages, Discussion):
        return messages.to_string()
    messages = messages_to_dict(messages)
    return "\n".join(f"{m['role']} message:\n{m['content']}\n" for m in messages)


                markdown_page=f"Agent had no chat messages. Perhaps there was an error. err_msg:\n{err_msg}",
            )
            return None, agent_info

        # an old bug prevented the response from being saved.
        if len(old_chat_messages) == 2:
            recorded_action = step_info.action
            if recorded_action:
                # Recreate the 3rd message based on the recorded action
                assistant_message = make_assistant_message(f"<action>{recorded_action}</action>")
                old_chat_messages.append(assistant_message)

        self.chat_llm = ReproChatModel(old_chat_messages)
        action, agent_info = super().get_action(obs)

        return _make_agent_stats(
            action, agent_info, step_info, old_chat_messages, self.chat_llm.new_messages
        )

def _make_agent_stats(action, agent_info, step_info, old_chat_messages, new_chat_messages):
    if isinstance(agent_info, dict):
        agent_info = AgentInfo(**agent_info)

    old_msg_str = _format_messages(old_chat_messages)
    new_msg_str = _format_messages(new_chat_messages)

    agent_info.html_page = _make_diff(old_str=old_msg_str, new_str=new_msg_str)
    agent_info.stats.update(_diff_stats(old_msg_str, new_msg_str))

    return action, agent_info

def _format_messages(messages: list[dict]):
    if isinstance(messages, Discussion):
        return messages.to_string()
    messages = messages_to_dict(messages)
    return "\n".join(f"{m['role']} message:\n{m['content']}\n" for m in messages)

def _make_backward_compatible(agent_args: GenericAgentArgs):
    action_set = agent_args.flags.action.action_set
    if isinstance(action_set, (str, list)):
        if isinstance(action_set, str):
            action_set = action_set.split("+")

        agent_args.flags.action.action_set = HighLevelActionSetArgs(
            subsets=action_set,
            multiaction=agent_args.flags.action.multi_actions,
        )


        action, agent_info = super().get_action(obs)

        return _make_agent_stats(
            action, agent_info, step_info, old_chat_messages, self.chat_llm.new_messages
        )

def _make_agent_stats(action, agent_info, step_info, old_chat_messages, new_chat_messages):
    if isinstance(agent_info, dict):
        agent_info = AgentInfo(**agent_info)

    old_msg_str = _format_messages(old_chat_messages)
    new_msg_str = _format_messages(new_chat_messages)

    agent_info.html_page = _make_diff(old_str=old_msg_str, new_str=new_msg_str)
    agent_info.stats.update(_diff_stats(old_msg_str, new_msg_str))

    return action, agent_info

def _format_messages(messages: list[dict]):
    if isinstance(messages, Discussion):
        return messages.to_string()
    messages = messages_to_dict(messages)
    return "\n".join(f"{m['role']} message:\n{m['content']}\n" for m in messages)

def _make_backward_compatible(agent_args: GenericAgentArgs):
    action_set = agent_args.flags.action.action_set
    if isinstance(action_set, (str, list)):
        if isinstance(action_set, str):
            action_set = action_set.split("+")

        agent_args.flags.action.action_set = HighLevelActionSetArgs(
            subsets=action_set,
            multiaction=agent_args.flags.action.multi_actions,
        )

    return agent_args

def reproduce_study(original_study_dir: Path | str, log_level=logging.INFO):
    """Reproduce a study by running the same experiments with the same agent."""

    original_study_dir = Path(original_study_dir)


def _make_agent_stats(action, agent_info, step_info, old_chat_messages, new_chat_messages):
    if isinstance(agent_info, dict):
        agent_info = AgentInfo(**agent_info)

    old_msg_str = _format_messages(old_chat_messages)
    new_msg_str = _format_messages(new_chat_messages)

    agent_info.html_page = _make_diff(old_str=old_msg_str, new_str=new_msg_str)
    agent_info.stats.update(_diff_stats(old_msg_str, new_msg_str))

    return action, agent_info

def _format_messages(messages: list[dict]):
    if isinstance(messages, Discussion):
        return messages.to_string()
    messages = messages_to_dict(messages)
    return "\n".join(f"{m['role']} message:\n{m['content']}\n" for m in messages)

def _make_backward_compatible(agent_args: GenericAgentArgs):
    action_set = agent_args.flags.action.action_set
    if isinstance(action_set, (str, list)):
        if isinstance(action_set, str):
            action_set = action_set.split("+")

        agent_args.flags.action.action_set = HighLevelActionSetArgs(
            subsets=action_set,
            multiaction=agent_args.flags.action.multi_actions,
        )

    return agent_args

def reproduce_study(original_study_dir: Path | str, log_level=logging.INFO):
    """Reproduce a study by running the same experiments with the same agent."""

    original_study_dir = Path(original_study_dir)

    exp_args_list: list[ExpArgs] = []
    for exp_result in yield_all_exp_results(original_study_dir, progress_fn=None):
        agent_args = _make_backward_compatible(exp_result.exp_args.agent_args)
        agent_args = make_repro_agent(agent_args, exp_dir=exp_result.exp_dir)
        exp_args_list.append(
            ExpArgs(
                agent_args=agent_args,
                env_args=exp_result.exp_args.env_args,
                logging_level=log_level,
            )
        )

    # infer benchmark name from task list for backward compatible


    if isinstance(messages, Discussion):
        return messages.to_string()
    messages = messages_to_dict(messages)
    return "\n".join(f"{m['role']} message:\n{m['content']}\n" for m in messages)

def _make_backward_compatible(agent_args: GenericAgentArgs):
    action_set = agent_args.flags.action.action_set
    if isinstance(action_set, (str, list)):
        if isinstance(action_set, str):
            action_set = action_set.split("+")

        agent_args.flags.action.action_set = HighLevelActionSetArgs(
            subsets=action_set,
            multiaction=agent_args.flags.action.multi_actions,
        )

    return agent_args

def reproduce_study(original_study_dir: Path | str, log_level=logging.INFO):
    """Reproduce a study by running the same experiments with the same agent."""

    original_study_dir = Path(original_study_dir)

    exp_args_list: list[ExpArgs] = []
    for exp_result in yield_all_exp_results(original_study_dir, progress_fn=None):
        agent_args = _make_backward_compatible(exp_result.exp_args.agent_args)
        agent_args = make_repro_agent(agent_args, exp_dir=exp_result.exp_dir)
        exp_args_list.append(
            ExpArgs(
                agent_args=agent_args,
                env_args=exp_result.exp_args.env_args,
                logging_level=log_level,
            )
        )

    # infer benchmark name from task list for backward compatible
    benchmark_name = exp_args_list[0].env_args.task_name.split(".")[0]

    study = Study(
        benchmark=benchmark_name,
        agent_args=[agent_args],
    )
    # this exp_args_list has a different agent_args for each experiment as repro_agent takes the exp_dir as argument
    # so we overwrite exp_args_list with the one we created above
    study.exp_args_list = exp_args_list
    return study

def make_repro_agent(agent_args: AgentArgs, exp_dir: Path | str):
    """Create a reproducibility agent from an existing agent.

    Note, if a new flag was added, it was not saved in the original pickle. When
    loading the pickle it silently adds the missing flag and set it to its
    default value. The new repro agent_args will thus have the new flag set to
    its default value.

    Args:
        agent_args (AgentArgs): The original agent args.
        exp_dir (Path | str): The directory where the experiment was saved.

    Returns:
        ReproAgentArgs: The new agent args.
    """
    exp_dir = Path(exp_dir)
    assert isinstance(agent_args, GenericAgentArgs)
    assert exp_dir.exists()  # sanity check


            ExpArgs(
                agent_args=agent_args,
                env_args=exp_result.exp_args.env_args,
                logging_level=log_level,
            )
        )

    # infer benchmark name from task list for backward compatible
    benchmark_name = exp_args_list[0].env_args.task_name.split(".")[0]

    study = Study(
        benchmark=benchmark_name,
        agent_args=[agent_args],
    )
    # this exp_args_list has a different agent_args for each experiment as repro_agent takes the exp_dir as argument
    # so we overwrite exp_args_list with the one we created above
    study.exp_args_list = exp_args_list
    return study

def make_repro_agent(agent_args: AgentArgs, exp_dir: Path | str):
    """Create a reproducibility agent from an existing agent.

    Note, if a new flag was added, it was not saved in the original pickle. When
    loading the pickle it silently adds the missing flag and set it to its
    default value. The new repro agent_args will thus have the new flag set to
    its default value.

    Args:
        agent_args (AgentArgs): The original agent args.
        exp_dir (Path | str): The directory where the experiment was saved.

    Returns:
        ReproAgentArgs: The new agent args.
    """
    exp_dir = Path(exp_dir)
    assert isinstance(agent_args, GenericAgentArgs)
    assert exp_dir.exists()  # sanity check

    return ReproAgentArgs(
        agent_name=f"Repro_{agent_args.agent_name}",
        chat_model_args=agent_args.chat_model_args,
        flags=agent_args.flags,
        max_retry=agent_args.max_retry,
        _repro_dir=exp_dir,
    )

def _make_diff(old_str, new_str):
    page = difflib.HtmlDiff().make_file(
        old_str.splitlines(), new_str.splitlines(), fromdesc="Old Version", todesc="New Version"
    )
    page = page.replace('nowrap="nowrap"', "")  # Remove nowrap attribute
    page = _set_style(page, DIFF_STYLE)
    return page

def _diff_stats(str1: str, str2: str):
    """Try some kind of metrics to make stats about the amount of diffs between two strings."""
    lines1 = str1.splitlines()
    lines2 = str2.splitlines()

    diff = list(difflib.Differ().compare(lines1, lines2))

    # Count added and removed lines
    added = sum(1 for line in diff if line.startswith("+ "))


    Args:
        agent_args (AgentArgs): The original agent args.
        exp_dir (Path | str): The directory where the experiment was saved.

    Returns:
        ReproAgentArgs: The new agent args.
    """
    exp_dir = Path(exp_dir)
    assert isinstance(agent_args, GenericAgentArgs)
    assert exp_dir.exists()  # sanity check

    return ReproAgentArgs(
        agent_name=f"Repro_{agent_args.agent_name}",
        chat_model_args=agent_args.chat_model_args,
        flags=agent_args.flags,
        max_retry=agent_args.max_retry,
        _repro_dir=exp_dir,
    )

def _make_diff(old_str, new_str):
    page = difflib.HtmlDiff().make_file(
        old_str.splitlines(), new_str.splitlines(), fromdesc="Old Version", todesc="New Version"
    )
    page = page.replace('nowrap="nowrap"', "")  # Remove nowrap attribute
    page = _set_style(page, DIFF_STYLE)
    return page

def _diff_stats(str1: str, str2: str):
    """Try some kind of metrics to make stats about the amount of diffs between two strings."""
    lines1 = str1.splitlines()
    lines2 = str2.splitlines()

    diff = list(difflib.Differ().compare(lines1, lines2))

    # Count added and removed lines
    added = sum(1 for line in diff if line.startswith("+ "))
    removed = sum(1 for line in diff if line.startswith("- "))

    # Calculate difference ratio
    difference_ratio = (added + removed) / (2 * max(len(lines1), len(lines2)))

    return dict(lines_added=added, lines_removed=removed, difference_ratio=difference_ratio)

def _set_style(html_str: str, style: str, prepend_previous_style: bool = False):


    assert exp_dir.exists()  # sanity check

    return ReproAgentArgs(
        agent_name=f"Repro_{agent_args.agent_name}",
        chat_model_args=agent_args.chat_model_args,
        flags=agent_args.flags,
        max_retry=agent_args.max_retry,
        _repro_dir=exp_dir,
    )

def _make_diff(old_str, new_str):
    page = difflib.HtmlDiff().make_file(
        old_str.splitlines(), new_str.splitlines(), fromdesc="Old Version", todesc="New Version"
    )
    page = page.replace('nowrap="nowrap"', "")  # Remove nowrap attribute
    page = _set_style(page, DIFF_STYLE)
    return page

def _diff_stats(str1: str, str2: str):
    """Try some kind of metrics to make stats about the amount of diffs between two strings."""
    lines1 = str1.splitlines()
    lines2 = str2.splitlines()

    diff = list(difflib.Differ().compare(lines1, lines2))

    # Count added and removed lines
    added = sum(1 for line in diff if line.startswith("+ "))
    removed = sum(1 for line in diff if line.startswith("- "))

    # Calculate difference ratio
    difference_ratio = (added + removed) / (2 * max(len(lines1), len(lines2)))

    return dict(lines_added=added, lines_removed=removed, difference_ratio=difference_ratio)

def _set_style(html_str: str, style: str, prepend_previous_style: bool = False):
    """Add a style tag to an HTML string."""

    soup = BeautifulSoup(html_str, "html.parser")
    style_tag = soup.find("style")

    if not style_tag:
        style_tag = soup.new_tag("style")
        soup.head.append(style_tag)

    current_style = style_tag.string or ""

    if prepend_previous_style:
        style = f"{style}\n{current_style}"
    else:
        style = f"{current_style}\n{style}"

    style_tag.string = style


    return page

def _diff_stats(str1: str, str2: str):
    """Try some kind of metrics to make stats about the amount of diffs between two strings."""
    lines1 = str1.splitlines()
    lines2 = str2.splitlines()

    diff = list(difflib.Differ().compare(lines1, lines2))

    # Count added and removed lines
    added = sum(1 for line in diff if line.startswith("+ "))
    removed = sum(1 for line in diff if line.startswith("- "))

    # Calculate difference ratio
    difference_ratio = (added + removed) / (2 * max(len(lines1), len(lines2)))

    return dict(lines_added=added, lines_removed=removed, difference_ratio=difference_ratio)

def _set_style(html_str: str, style: str, prepend_previous_style: bool = False):
    """Add a style tag to an HTML string."""

    soup = BeautifulSoup(html_str, "html.parser")
    style_tag = soup.find("style")

    if not style_tag:
        style_tag = soup.new_tag("style")
        soup.head.append(style_tag)

    current_style = style_tag.string or ""

    if prepend_previous_style:
        style = f"{style}\n{current_style}"
    else:
        style = f"{current_style}\n{style}"

    style_tag.string = style

    return str(soup)

# this is the style to adjust the diff table inside gradio
DIFF_STYLE = """
    table.diff {
        font-size: 10px;
        font-family: Courier;
        border: medium;
        width: 100%;
        max-width: 100%; /* Ensure table does not exceed its container */
        table-layout: auto; /* Adjust column sizes dynamically */
        word-wrap: break-word;
        overflow-wrap: break-word;
    }
    /* Constrain the max-width of the 3rd and 6th columns */
    td:nth-child(3), td:nth-child(6) {
        max-width: 200px; /* Adjust this value to suit your content */
        white-space: normal; /* Allow wrapping in content columns */
        overflow-wrap: break-word; /* Break long words/content */
    }


        return {}

@dataclass
class ReproAgentArgs(GenericAgentArgs):
    # starting with "_" will prevent from being part of the index in the load_results function
    _repro_dir: str = None

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)

class ReproAgent(GenericAgent):
    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry=4,
        repro_dir=None,
    ):
        self.exp_result = ExpResult(repro_dir)
        super().__init__(chat_model_args, flags, max_retry)

    def get_action(self, obs):
        # replace the chat model with a reproducible chat that will mimic the
        # same answers
        step = len(self.actions)
        step_info = self.exp_result.get_step_info(step)
        old_chat_messages = step_info.agent_info.get("chat_messages", None)  # type: Discussion

        if old_chat_messages is None:
            err_msg = self.exp_result.summary_info["err_msg"]

            agent_info = AgentInfo(
                markdown_page=f"Agent had no chat messages. Perhaps there was an error. err_msg:\n{err_msg}",
            )
            return None, agent_info

        # an old bug prevented the response from being saved.
        if len(old_chat_messages) == 2:
            recorded_action = step_info.action
            if recorded_action:


from agentlab.experiments.loop import ExpArgs, ExpResult, yield_all_exp_results
from agentlab.experiments.study import Study
from agentlab.llm.chat_api import make_assistant_message
from agentlab.llm.llm_utils import Discussion, messages_to_dict

from .generic_agent import GenericAgent, GenericAgentArgs

class ReproChatModel:
    """A chat model that reproduces a conversation.

    Args:
        messages (list): A list of messages previously executed.
        delay (int): A delay to simulate the time it takes to generate a response.
    """

    def __init__(self, old_messages, delay=1) -> None:
        self.old_messages = old_messages
        self.delay = delay

    def __call__(self, messages: list | Discussion):
        self.new_messages = copy(messages)

        if len(messages) >= len(self.old_messages):
            # if for some reason the llm response was not saved
            return make_assistant_message("""<action>None</action>""")

        old_response = self.old_messages[len(messages)]
        self.new_messages.append(old_response)
        time.sleep(self.delay)
        # return the next message in the list
        return old_response

    def get_stats(self):
        return {}

@dataclass
class ReproAgentArgs(GenericAgentArgs):
    # starting with "_" will prevent from being part of the index in the load_results function
    _repro_dir: str = None

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)


        delay (int): A delay to simulate the time it takes to generate a response.
    """

    def __init__(self, old_messages, delay=1) -> None:
        self.old_messages = old_messages
        self.delay = delay

    def __call__(self, messages: list | Discussion):
        self.new_messages = copy(messages)

        if len(messages) >= len(self.old_messages):
            # if for some reason the llm response was not saved
            return make_assistant_message("""<action>None</action>""")

        old_response = self.old_messages[len(messages)]
        self.new_messages.append(old_response)
        time.sleep(self.delay)
        # return the next message in the list
        return old_response

    def get_stats(self):
        return {}

@dataclass
class ReproAgentArgs(GenericAgentArgs):
    # starting with "_" will prevent from being part of the index in the load_results function
    _repro_dir: str = None

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)

class ReproAgent(GenericAgent):
    def __init__(


        if len(messages) >= len(self.old_messages):
            # if for some reason the llm response was not saved
            return make_assistant_message("""<action>None</action>""")

        old_response = self.old_messages[len(messages)]
        self.new_messages.append(old_response)
        time.sleep(self.delay)
        # return the next message in the list
        return old_response

    def get_stats(self):
        return {}

@dataclass
class ReproAgentArgs(GenericAgentArgs):
    # starting with "_" will prevent from being part of the index in the load_results function
    _repro_dir: str = None

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)

class ReproAgent(GenericAgent):
    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry=4,
        repro_dir=None,
    ):
        self.exp_result = ExpResult(repro_dir)
        super().__init__(chat_model_args, flags, max_retry)

    def get_action(self, obs):
        # replace the chat model with a reproducible chat that will mimic the
        # same answers
        step = len(self.actions)


        time.sleep(self.delay)
        # return the next message in the list
        return old_response

    def get_stats(self):
        return {}

@dataclass
class ReproAgentArgs(GenericAgentArgs):
    # starting with "_" will prevent from being part of the index in the load_results function
    _repro_dir: str = None

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)

class ReproAgent(GenericAgent):
    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry=4,
        repro_dir=None,
    ):
        self.exp_result = ExpResult(repro_dir)
        super().__init__(chat_model_args, flags, max_retry)

    def get_action(self, obs):
        # replace the chat model with a reproducible chat that will mimic the
        # same answers
        step = len(self.actions)
        step_info = self.exp_result.get_step_info(step)
        old_chat_messages = step_info.agent_info.get("chat_messages", None)  # type: Discussion


            super().__post_init__()
            self.agent_name = f"Repro_{self.agent_name}"
        except AttributeError:
            pass

    def make_agent(self):
        return ReproAgent(self.chat_model_args, self.flags, self.max_retry, self._repro_dir)

class ReproAgent(GenericAgent):
    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry=4,
        repro_dir=None,
    ):
        self.exp_result = ExpResult(repro_dir)
        super().__init__(chat_model_args, flags, max_retry)

    def get_action(self, obs):
        # replace the chat model with a reproducible chat that will mimic the
        # same answers
        step = len(self.actions)
        step_info = self.exp_result.get_step_info(step)
        old_chat_messages = step_info.agent_info.get("chat_messages", None)  # type: Discussion

        if old_chat_messages is None:
            err_msg = self.exp_result.summary_info["err_msg"]

            agent_info = AgentInfo(
                markdown_page=f"Agent had no chat messages. Perhaps there was an error. err_msg:\n{err_msg}",
            )
            return None, agent_info

        # an old bug prevented the response from being saved.
        if len(old_chat_messages) == 2:
            recorded_action = step_info.action
            if recorded_action:
                # Recreate the 3rd message based on the recorded action
                assistant_message = make_assistant_message(f"<action>{recorded_action}</action>")
                old_chat_messages.append(assistant_message)

        self.chat_llm = ReproChatModel(old_chat_messages)
        action, agent_info = super().get_action(obs)

        return _make_agent_stats(
            action, agent_info, step_info, old_chat_messages, self.chat_llm.new_messages
        )

def _make_agent_stats(action, agent_info, step_info, old_chat_messages, new_chat_messages):
    if isinstance(agent_info, dict):
        agent_info = AgentInfo(**agent_info)

    old_msg_str = _format_messages(old_chat_messages)
    new_msg_str = _format_messages(new_chat_messages)

    agent_info.html_page = _make_diff(old_str=old_msg_str, new_str=new_msg_str)
    agent_info.stats.update(_diff_stats(old_msg_str, new_msg_str))

    return action, agent_info

def _format_messages(messages: list[dict]):
    if isinstance(messages, Discussion):
        return messages.to_string()
    messages = messages_to_dict(messages)
    return "\n".join(f"{m['role']} message:\n{m['content']}\n" for m in messages)


"""
GenericAgent implementation for AgentLab

This module defines a `GenericAgent` class and its associated arguments for use in the AgentLab framework. \
The `GenericAgent` class is designed to interact with a chat-based model to determine actions based on \
observations. It includes methods for preprocessing observations, generating actions, and managing internal \
state such as plans, memories, and thoughts. The `GenericAgentArgs` class provides configuration options for \
the agent, including model arguments and flags for various behaviors.
"""

from copy import deepcopy
from dataclasses import asdict, dataclass
from functools import partial
from warnings import warn

import bgym
from bgym import Benchmark
from browsergym.experiments.agent import Agent, AgentInfo

from agentlab.agents import dynamic_prompting as dp
from agentlab.agents.agent_args import AgentArgs
from agentlab.llm.chat_api import BaseModelArgs
from agentlab.llm.llm_utils import Discussion, ParseError, SystemMessage, retry
from agentlab.llm.tracking import cost_tracker_decorator

from .generic_agent_prompt import GenericPromptFlags, MainPrompt

@dataclass
class GenericAgentArgs(AgentArgs):
    chat_model_args: BaseModelArgs = None
    flags: GenericPromptFlags = None
    max_retry: int = 4

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            self.agent_name = f"GenericAgent-{self.chat_model_args.model_name}".replace("/", "_")
        except AttributeError:
            pass

    def set_benchmark(self, benchmark: Benchmark, demo_mode):
        """Override Some flags based on the benchmark."""
        if benchmark.name.startswith("miniwob"):
            self.flags.obs.use_html = True

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()
        self.reset(seed=None)

    def obs_preprocessor(self, obs: dict) -> dict:
        return self._obs_preprocessor(obs)

    @cost_tracker_decorator
    def get_action(self, obs):

        self.obs_history.append(obs)
        main_prompt = MainPrompt(
            action_set=self.action_set,
            obs_history=self.obs_history,
            actions=self.actions,
            memories=self.memories,
            thoughts=self.thoughts,
            previous_plan=self.plan,
            step=self.plan_step,
            flags=self.flags,
        )

        max_prompt_tokens, max_trunc_itr = self._get_maxes()

        system_prompt = SystemMessage(dp.SystemPrompt().prompt)

        human_prompt = dp.fit_tokens(
            shrinkable=main_prompt,
            max_prompt_tokens=max_prompt_tokens,
            model_name=self.chat_model_args.model_name,
            max_iterations=max_trunc_itr,
            additional_prompts=system_prompt,
        )
        try:
            # TODO, we would need to further shrink the prompt if the retry
            # cause it to be too long

            chat_messages = Discussion([system_prompt, human_prompt])
            ans_dict = retry(
                self.chat_llm,
                chat_messages,
                n_retry=self.max_retry,
                parser=main_prompt._parse_answer,
            )
            ans_dict["busted_retry"] = 0
            # inferring the number of retries, TODO: make this less hacky
            ans_dict["n_retry"] = (len(chat_messages) - 3) / 2
        except ParseError as e:
            ans_dict = dict(
                action=None,
                n_retry=self.max_retry + 1,
                busted_retry=1,
            )

        stats = self.chat_llm.get_stats()
        stats["n_retry"] = ans_dict["n_retry"]
        stats["busted_retry"] = ans_dict["busted_retry"]

        self.plan = ans_dict.get("plan", self.plan)
        self.plan_step = ans_dict.get("step", self.plan_step)
        self.actions.append(ans_dict["action"])
        self.memories.append(ans_dict.get("memory", None))
        self.thoughts.append(ans_dict.get("think", None))

        agent_info = AgentInfo(
            think=ans_dict.get("think", None),
            chat_messages=chat_messages,
            stats=stats,
            extra_info={"chat_model_args": asdict(self.chat_model_args)},
        )
        return ans_dict["action"], agent_info

    def reset(self, seed=None):
        self.seed = seed
        self.plan = "No plan yet"
        self.plan_step = -1
        self.memories = []
        self.thoughts = []
        self.actions = []
        self.obs_history = []

    def _check_flag_constancy(self):
        flags = self.flags
        if flags.obs.use_som:
            if not flags.obs.use_screenshot:
                warn(
                    """
Warning: use_som=True requires use_screenshot=True. Disabling use_som."""
                )
                flags.obs.use_som = False
        if flags.obs.use_screenshot:
            if not self.chat_model_args.vision_support:
                warn(
                    """
Warning: use_screenshot is set to True, but the chat model \
does not support vision. Disabling use_screenshot."""
                )
                flags.obs.use_screenshot = False
        return flags

    def _get_maxes(self):
        maxes = (
            self.flags.max_prompt_tokens,
            self.chat_model_args.max_total_tokens,
            self.chat_model_args.max_input_tokens,
        )
        maxes = [m for m in maxes if m is not None]
        max_prompt_tokens = min(maxes) if maxes else None
        max_trunc_itr = (
            self.flags.max_trunc_itr
            if self.flags.max_trunc_itr
            else 20  # dangerous to change the default value here?
        )
        return max_prompt_tokens, max_trunc_itr


GenericAgentArgs

from copy import deepcopy
from dataclasses import asdict, dataclass
from functools import partial
from warnings import warn

import bgym
from bgym import Benchmark
from browsergym.experiments.agent import Agent, AgentInfo

from agentlab.agents import dynamic_prompting as dp
from agentlab.agents.agent_args import AgentArgs
from agentlab.llm.chat_api import BaseModelArgs
from agentlab.llm.llm_utils import Discussion, ParseError, SystemMessage, retry
from agentlab.llm.tracking import cost_tracker_decorator

from .generic_agent_prompt import GenericPromptFlags, MainPrompt

@dataclass
class GenericAgentArgs(AgentArgs):
    chat_model_args: BaseModelArgs = None
    flags: GenericPromptFlags = None
    max_retry: int = 4

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            self.agent_name = f"GenericAgent-{self.chat_model_args.model_name}".replace("/", "_")
        except AttributeError:
            pass

    def set_benchmark(self, benchmark: Benchmark, demo_mode):
        """Override Some flags based on the benchmark."""
        if benchmark.name.startswith("miniwob"):
            self.flags.obs.use_html = True

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()


GenericAgent

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()
        self.reset(seed=None)

    def obs_preprocessor(self, obs: dict) -> dict:
        return self._obs_preprocessor(obs)

    @cost_tracker_decorator
    def get_action(self, obs):

        self.obs_history.append(obs)
        main_prompt = MainPrompt(
            action_set=self.action_set,
            obs_history=self.obs_history,
            actions=self.actions,
            memories=self.memories,
            thoughts=self.thoughts,
            previous_plan=self.plan,
            step=self.plan_step,
            flags=self.flags,
        )

        max_prompt_tokens, max_trunc_itr = self._get_maxes()

        system_prompt = SystemMessage(dp.SystemPrompt().prompt)

        human_prompt = dp.fit_tokens(
            shrinkable=main_prompt,
            max_prompt_tokens=max_prompt_tokens,
            model_name=self.chat_model_args.model_name,
            max_iterations=max_trunc_itr,
            additional_prompts=system_prompt,
        )
        try:
            # TODO, we would need to further shrink the prompt if the retry
            # cause it to be too long

            chat_messages = Discussion([system_prompt, human_prompt])
            ans_dict = retry(
                self.chat_llm,
                chat_messages,
                n_retry=self.max_retry,
                parser=main_prompt._parse_answer,
            )
            ans_dict["busted_retry"] = 0
            # inferring the number of retries, TODO: make this less hacky
            ans_dict["n_retry"] = (len(chat_messages) - 3) / 2
        except ParseError as e:
            ans_dict = dict(
                action=None,
                n_retry=self.max_retry + 1,
                busted_retry=1,
            )

        stats = self.chat_llm.get_stats()
        stats["n_retry"] = ans_dict["n_retry"]
        stats["busted_retry"] = ans_dict["busted_retry"]

        self.plan = ans_dict.get("plan", self.plan)
        self.plan_step = ans_dict.get("step", self.plan_step)
        self.actions.append(ans_dict["action"])
        self.memories.append(ans_dict.get("memory", None))
        self.thoughts.append(ans_dict.get("think", None))

        agent_info = AgentInfo(
            think=ans_dict.get("think", None),
            chat_messages=chat_messages,
            stats=stats,
            extra_info={"chat_model_args": asdict(self.chat_model_args)},
        )
        return ans_dict["action"], agent_info

    def reset(self, seed=None):
        self.seed = seed
        self.plan = "No plan yet"
        self.plan_step = -1
        self.memories = []
        self.thoughts = []
        self.actions = []
        self.obs_history = []

    def _check_flag_constancy(self):
        flags = self.flags
        if flags.obs.use_som:
            if not flags.obs.use_screenshot:
                warn(
                    """
Warning: use_som=True requires use_screenshot=True. Disabling use_som."""
                )
                flags.obs.use_som = False
        if flags.obs.use_screenshot:
            if not self.chat_model_args.vision_support:
                warn(
                    """
Warning: use_screenshot is set to True, but the chat model \
does not support vision. Disabling use_screenshot."""
                )
                flags.obs.use_screenshot = False
        return flags

    def _get_maxes(self):
        maxes = (
            self.flags.max_prompt_tokens,
            self.chat_model_args.max_total_tokens,
            self.chat_model_args.max_input_tokens,
        )
        maxes = [m for m in maxes if m is not None]
        max_prompt_tokens = min(maxes) if maxes else None
        max_trunc_itr = (
            self.flags.max_trunc_itr
            if self.flags.max_trunc_itr
            else 20  # dangerous to change the default value here?
        )
        return max_prompt_tokens, max_trunc_itr


import bgym
from bgym import Benchmark
from browsergym.experiments.agent import Agent, AgentInfo

from agentlab.agents import dynamic_prompting as dp
from agentlab.agents.agent_args import AgentArgs
from agentlab.llm.chat_api import BaseModelArgs
from agentlab.llm.llm_utils import Discussion, ParseError, SystemMessage, retry
from agentlab.llm.tracking import cost_tracker_decorator

from .generic_agent_prompt import GenericPromptFlags, MainPrompt

@dataclass
class GenericAgentArgs(AgentArgs):
    chat_model_args: BaseModelArgs = None
    flags: GenericPromptFlags = None
    max_retry: int = 4

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            self.agent_name = f"GenericAgent-{self.chat_model_args.model_name}".replace("/", "_")
        except AttributeError:
            pass

    def set_benchmark(self, benchmark: Benchmark, demo_mode):
        """Override Some flags based on the benchmark."""
        if benchmark.name.startswith("miniwob"):
            self.flags.obs.use_html = True

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):


from agentlab.agents.agent_args import AgentArgs
from agentlab.llm.chat_api import BaseModelArgs
from agentlab.llm.llm_utils import Discussion, ParseError, SystemMessage, retry
from agentlab.llm.tracking import cost_tracker_decorator

from .generic_agent_prompt import GenericPromptFlags, MainPrompt

@dataclass
class GenericAgentArgs(AgentArgs):
    chat_model_args: BaseModelArgs = None
    flags: GenericPromptFlags = None
    max_retry: int = 4

    def __post_init__(self):
        try:  # some attributes might be temporarily args.CrossProd for hyperparameter generation
            self.agent_name = f"GenericAgent-{self.chat_model_args.model_name}".replace("/", "_")
        except AttributeError:
            pass

    def set_benchmark(self, benchmark: Benchmark, demo_mode):
        """Override Some flags based on the benchmark."""
        if benchmark.name.startswith("miniwob"):
            self.flags.obs.use_html = True

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,


            pass

    def set_benchmark(self, benchmark: Benchmark, demo_mode):
        """Override Some flags based on the benchmark."""
        if benchmark.name.startswith("miniwob"):
            self.flags.obs.use_html = True

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,


prepare

        """Override Some flags based on the benchmark."""
        if benchmark.name.startswith("miniwob"):
            self.flags.obs.use_html = True

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()


close

        self.flags.obs.use_tabs = benchmark.is_multi_tab
        self.flags.action.action_set = deepcopy(benchmark.high_level_action_set_args)

        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry


        # for backward compatibility with old traces
        if self.flags.action.multi_actions is not None:
            self.flags.action.action_set.multiaction = self.flags.action.multi_actions
        if self.flags.action.is_strict is not None:
            self.flags.action.action_set.strict = self.flags.action.is_strict

        # verify if we can remove this
        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()


        if demo_mode:
            self.flags.action.action_set.demo_mode = "all_blue"

    def set_reproducibility_mode(self):
        self.chat_model_args.temperature = 0

    def prepare(self):
        return self.chat_model_args.prepare_server()

    def close(self):
        return self.chat_model_args.close_server()

    def make_agent(self):
        return GenericAgent(
            chat_model_args=self.chat_model_args, flags=self.flags, max_retry=self.max_retry
        )

class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()
        self.reset(seed=None)

    def obs_preprocessor(self, obs: dict) -> dict:
        return self._obs_preprocessor(obs)

    @cost_tracker_decorator
    def get_action(self, obs):

        self.obs_history.append(obs)
        main_prompt = MainPrompt(
            action_set=self.action_set,
            obs_history=self.obs_history,
            actions=self.actions,
            memories=self.memories,
            thoughts=self.thoughts,
            previous_plan=self.plan,
            step=self.plan_step,
            flags=self.flags,
        )

        max_prompt_tokens, max_trunc_itr = self._get_maxes()


class GenericAgent(Agent):

    def __init__(
        self,
        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()
        self.reset(seed=None)

    def obs_preprocessor(self, obs: dict) -> dict:
        return self._obs_preprocessor(obs)

    @cost_tracker_decorator
    def get_action(self, obs):

        self.obs_history.append(obs)
        main_prompt = MainPrompt(
            action_set=self.action_set,
            obs_history=self.obs_history,
            actions=self.actions,
            memories=self.memories,
            thoughts=self.thoughts,
            previous_plan=self.plan,
            step=self.plan_step,
            flags=self.flags,
        )

        max_prompt_tokens, max_trunc_itr = self._get_maxes()

        system_prompt = SystemMessage(dp.SystemPrompt().prompt)


        chat_model_args: BaseModelArgs,
        flags: GenericPromptFlags,
        max_retry: int = 4,
    ):

        self.chat_llm = chat_model_args.make_model()
        self.chat_model_args = chat_model_args
        self.max_retry = max_retry

        self.flags = flags
        self.action_set = self.flags.action.action_set.make_action_set()
        self._obs_preprocessor = dp.make_obs_preprocessor(flags.obs)

        self._check_flag_constancy()
        self.reset(seed=None)

    def obs_preprocessor(self, obs: dict) -> dict:
        return self._obs_preprocessor(obs)

    @cost_tracker_decorator
    def get_action(self, obs):

        self.obs_history.append(obs)
        main_prompt = MainPrompt(
            action_set=self.action_set,
            obs_history=self.obs_history,
            actions=self.actions,
            memories=self.memories,
            thoughts=self.thoughts,
            previous_plan=self.plan,
            step=self.plan_step,
            flags=self.flags,
        )

        max_prompt_tokens, max_trunc_itr = self._get_maxes()

        system_prompt = SystemMessage(dp.SystemPrompt().prompt)

        human_prompt = dp.fit_tokens(
            shrinkable=main_prompt,
            max_prompt_tokens=max_prompt_tokens,
            model_name=self.chat_model_args.model_name,
            max_iterations=max_trunc_itr,
            additional_prompts=system_prompt,
        )
        try:
            # TODO, we would need to further shrink the prompt if the retry
            # cause it to be too long

            chat_messages = Discussion([system_prompt, human_prompt])
            ans_dict = retry(
                self.chat_llm,
                chat_messages,
                n_retry=self.max_retry,
                parser=main_prompt._parse_answer,
            )
            ans_dict["busted_retry"] = 0
            # inferring the number of retries, TODO: make this less hacky
            ans_dict["n_retry"] = (len(chat_messages) - 3) / 2
        except ParseError as e:
            ans_dict = dict(
                action=None,
                n_retry=self.max_retry + 1,
                busted_retry=1,
            )

        stats = self.chat_llm.get_stats()
        stats["n_retry"] = ans_dict["n_retry"]
        stats["busted_retry"] = ans_dict["busted_retry"]

        self.plan = ans_dict.get("plan", self.plan)
        self.plan_step = ans_dict.get("step", self.plan_step)
        self.actions.append(ans_dict["action"])
        self.memories.append(ans_dict.get("memory", None))
        self.thoughts.append(ans_dict.get("think", None))

        agent_info = AgentInfo(
            think=ans_dict.get("think", None),
            chat_messages=chat_messages,
            stats=stats,
            extra_info={"chat_model_args": asdict(self.chat_model_args)},
        )
        return ans_dict["action"], agent_info

    def reset(self, seed=None):
        self.seed = seed
        self.plan = "No plan yet"
        self.plan_step = -1
        self.memories = []
        self.thoughts = []
        self.actions = []
        self.obs_history = []

    def _check_flag_constancy(self):
        flags = self.flags
        if flags.obs.use_som:
            if not flags.obs.use_screenshot:
                warn(
                    """
Warning: use_som=True requires use_screenshot=True. Disabling use_som."""
                )
                flags.obs.use_som = False
        if flags.obs.use_screenshot:


reset

            )

        stats = self.chat_llm.get_stats()
        stats["n_retry"] = ans_dict["n_retry"]
        stats["busted_retry"] = ans_dict["busted_retry"]

        self.plan = ans_dict.get("plan", self.plan)
        self.plan_step = ans_dict.get("step", self.plan_step)
        self.actions.append(ans_dict["action"])
        self.memories.append(ans_dict.get("memory", None))
        self.thoughts.append(ans_dict.get("think", None))

        agent_info = AgentInfo(
            think=ans_dict.get("think", None),
            chat_messages=chat_messages,
            stats=stats,
            extra_info={"chat_model_args": asdict(self.chat_model_args)},
        )
        return ans_dict["action"], agent_info

    def reset(self, seed=None):
        self.seed = seed
        self.plan = "No plan yet"
        self.plan_step = -1
        self.memories = []
        self.thoughts = []
        self.actions = []
        self.obs_history = []

    def _check_flag_constancy(self):
        flags = self.flags
        if flags.obs.use_som:
            if not flags.obs.use_screenshot:
                warn(
                    """
Warning: use_som=True requires use_screenshot=True. Disabling use_som."""
                )
                flags.obs.use_som = False
        if flags.obs.use_screenshot:
            if not self.chat_model_args.vision_support:
                warn(
                    """
Warning: use_screenshot is set to True, but the chat model \
does not support vision. Disabling use_screenshot."""
                )
                flags.obs.use_screenshot = False
        return flags


        self.memories.append(ans_dict.get("memory", None))
        self.thoughts.append(ans_dict.get("think", None))

        agent_info = AgentInfo(
            think=ans_dict.get("think", None),
            chat_messages=chat_messages,
            stats=stats,
            extra_info={"chat_model_args": asdict(self.chat_model_args)},
        )
        return ans_dict["action"], agent_info

    def reset(self, seed=None):
        self.seed = seed
        self.plan = "No plan yet"
        self.plan_step = -1
        self.memories = []
        self.thoughts = []
        self.actions = []
        self.obs_history = []

    def _check_flag_constancy(self):
        flags = self.flags
        if flags.obs.use_som:
            if not flags.obs.use_screenshot:
                warn(
                    """
Warning: use_som=True requires use_screenshot=True. Disabling use_som."""
                )
                flags.obs.use_som = False
        if flags.obs.use_screenshot:
            if not self.chat_model_args.vision_support:
                warn(
                    """
Warning: use_screenshot is set to True, but the chat model \
does not support vision. Disabling use_screenshot."""
                )
                flags.obs.use_screenshot = False
        return flags

    def _get_maxes(self):
        maxes = (
            self.flags.max_prompt_tokens,
            self.chat_model_args.max_total_tokens,
            self.chat_model_args.max_input_tokens,
        )
        maxes = [m for m in maxes if m is not None]
        max_prompt_tokens = min(maxes) if maxes else None
        max_trunc_itr = (
            self.flags.max_trunc_itr
            if self.flags.max_trunc_itr
            else 20  # dangerous to change the default value here?
        )
        return max_prompt_tokens, max_trunc_itr


    def _check_flag_constancy(self):
        flags = self.flags
        if flags.obs.use_som:
            if not flags.obs.use_screenshot:
                warn(
                    """
Warning: use_som=True requires use_screenshot=True. Disabling use_som."""
                )
                flags.obs.use_som = False
        if flags.obs.use_screenshot:
            if not self.chat_model_args.vision_support:
                warn(
                    """
Warning: use_screenshot is set to True, but the chat model \
does not support vision. Disabling use_screenshot."""
                )
                flags.obs.use_screenshot = False
        return flags

    def _get_maxes(self):
        maxes = (
            self.flags.max_prompt_tokens,
            self.chat_model_args.max_total_tokens,
            self.chat_model_args.max_input_tokens,
        )
        maxes = [m for m in maxes if m is not None]
        max_prompt_tokens = min(maxes) if maxes else None
        max_trunc_itr = (
            self.flags.max_trunc_itr
            if self.flags.max_trunc_itr
            else 20  # dangerous to change the default value here?
        )
        return max_prompt_tokens, max_trunc_itr


from typing_extensions import Protocol

from agentlab.agents.agent_args import AgentArgs

class MultiCandidateAgent(Protocol):
    """
    Protocol for agents that generate multiple candidates for get_action.

    This protocol defines the contract for agents that can generate
    multiple candidate actions and allow selection of one of them for execution.
    """

    def get_candidate_generations(
        self, obs: dict, hint: list[str] | None = None, n_candidates: int = 3
    ) -> "list[dict]":
        """
        Generate multiple candidate actions for the given observation.

        You can pass extra info in agent_info to update internal state of the
        agent based on the selected candidate. Your internal state management
        should be robust to multiple calls to the get_candidate_generations method
        in a single step.

        Args:
            obs: The current observation dictionary containing environment state
            hint: Optional list of hint strings to guide candidate generation
            n_candidates: Number of candidate actions to generate
        """
        ...

    def update_agent_state_from_selected_candidate(self, output: dict):
        """
        Update the agent's internal state based on the selected candidate.
        This can include any memory or planning updates.

        Args:
            output: The selected candidate action dictionary
        """
        pass

class MultiCandidateAgentArgs(AgentArgs):
    def make_agent(self) -> MultiCandidateAgent: ...

    def __post_init__(self):
        """Prefix subagent name with 'MC-'."""
        super().__post_init__()
        if hasattr(self, "agent_name") and self.agent_name:
            self.agent_name = "MC-" + self.agent_name


MultiCandidateAgentArgs

        in a single step.

        Args:
            obs: The current observation dictionary containing environment state
            hint: Optional list of hint strings to guide candidate generation
            n_candidates: Number of candidate actions to generate
        """
        ...

    def update_agent_state_from_selected_candidate(self, output: dict):
        """
        Update the agent's internal state based on the selected candidate.
        This can include any memory or planning updates.

        Args:
            output: The selected candidate action dictionary
        """
        pass

class MultiCandidateAgentArgs(AgentArgs):
    def make_agent(self) -> MultiCandidateAgent: ...

    def __post_init__(self):
        """Prefix subagent name with 'MC-'."""
        super().__post_init__()
        if hasattr(self, "agent_name") and self.agent_name:
            self.agent_name = "MC-" + self.agent_name


    """

    def get_candidate_generations(
        self, obs: dict, hint: list[str] | None = None, n_candidates: int = 3
    ) -> "list[dict]":
        """
        Generate multiple candidate actions for the given observation.

        You can pass extra info in agent_info to update internal state of the
        agent based on the selected candidate. Your internal state management
        should be robust to multiple calls to the get_candidate_generations method
        in a single step.

        Args:
            obs: The current observation dictionary containing environment state
            hint: Optional list of hint strings to guide candidate generation
            n_candidates: Number of candidate actions to generate
        """
        ...

    def update_agent_state_from_selected_candidate(self, output: dict):
        """
        Update the agent's internal state based on the selected candidate.
        This can include any memory or planning updates.

        Args:
            output: The selected candidate action dictionary
        """
        pass

class MultiCandidateAgentArgs(AgentArgs):
    def make_agent(self) -> MultiCandidateAgent: ...

    def __post_init__(self):
        """Prefix subagent name with 'MC-'."""
        super().__post_init__()
        if hasattr(self, "agent_name") and self.agent_name:
            self.agent_name = "MC-" + self.agent_name


        Args:
            obs: The current observation dictionary containing environment state
            hint: Optional list of hint strings to guide candidate generation
            n_candidates: Number of candidate actions to generate
        """
        ...

    def update_agent_state_from_selected_candidate(self, output: dict):
        """
        Update the agent's internal state based on the selected candidate.
        This can include any memory or planning updates.

        Args:
            output: The selected candidate action dictionary
        """
        pass

class MultiCandidateAgentArgs(AgentArgs):
    def make_agent(self) -> MultiCandidateAgent: ...

    def __post_init__(self):
        """Prefix subagent name with 'MC-'."""
        super().__post_init__()
        if hasattr(self, "agent_name") and self.agent_name:
            self.agent_name = "MC-" + self.agent_name


            obs: The current observation dictionary containing environment state
            hint: Optional list of hint strings to guide candidate generation
            n_candidates: Number of candidate actions to generate
        """
        ...

    def update_agent_state_from_selected_candidate(self, output: dict):
        """
        Update the agent's internal state based on the selected candidate.
        This can include any memory or planning updates.

        Args:
            output: The selected candidate action dictionary
        """
        pass

class MultiCandidateAgentArgs(AgentArgs):
    def make_agent(self) -> MultiCandidateAgent: ...

    def __post_init__(self):
        """Prefix subagent name with 'MC-'."""
        super().__post_init__()
        if hasattr(self, "agent_name") and self.agent_name:
            self.agent_name = "MC-" + self.agent_name


"""
Console launcher for the Human-in-the-Loop Generic Agent UI.

Usage (installed entry point):
    agentlab-mentor --benchmark miniwob --task-name miniwob.book-flight --seed 123 --no-headless

This will run a Study with the MultipleProposalGenericAgent and the selected task.
"""

from __future__ import annotations

import argparse
import logging
from pathlib import Path

import bgym

from agentlab.agents.hitl_agent.generic_human_guided_agent import get_base_agent
from agentlab.experiments.exp_utils import RESULTS_DIR
from agentlab.experiments.study import Study

def build_benchmark(benchmark_name: str, task_name: str, seed: int, headless: bool):
    # Instantiate benchmark by name using BrowserGym registry
    try:
        benchmark = bgym.DEFAULT_BENCHMARKS[benchmark_name.lower()]()
    except KeyError as e:
        choices = ", ".join(sorted(bgym.DEFAULT_BENCHMARKS.keys()))
        raise SystemExit(f"Unknown benchmark '{benchmark_name}'. Choose one of: {choices}") from e

    filtered_env_args = [
        env_args for env_args in benchmark.env_args_list if env_args.task_name == task_name
    ]
    if not filtered_env_args:
        raise SystemExit(f'No tasks found matching "{task_name}"')
    filtered_env_args = filtered_env_args[:1]  # take the first one
    benchmark.env_args_list = filtered_env_args

    # Reasonable defaults for interactive UI
    for env_args in benchmark.env_args_list:
        env_args.task_seed = seed
        env_args.max_steps = env_args.max_steps or 200
        env_args.headless = headless

    return benchmark

def extract_hints_from_experiment_trace(exp_dir):
    """Extracts hints from every step of each episode in a exp_dir and returns a df with each row containing a hint.

    Args:
        exp_dir: Path-like to a study/experiment directory whose results should be scanned.

    Returns:
        pandas.DataFrame: One row per hint with metadata columns.
    """
    import pandas as pd

    from agentlab.analyze import inspect_results
    from agentlab.experiments.exp_utils import RESULTS_DIR
    from agentlab.experiments.loop import ExpResult

    output = []
    # Use provided exp_dir if set; otherwise default to <$AGENTLAB_EXP_ROOT>/agentlab_mentor
    result_df = inspect_results.load_result_df(exp_dir or (RESULTS_DIR / "agentlab_mentor"))
    if result_df is None:
        # No results to parse; return empty dataframe with expected columns
        return pd.DataFrame(
            columns=[
                "exp_id",
                "agent_name",
                "benchmark",
                "task_name",
                "episode_reward",
                "hint",
            ]
        )
    result_df = result_df.reset_index()
    for _, row in result_df.iterrows():
        result = ExpResult(row.exp_dir)
        episode = result.steps_info
        episode_reward = max([step.reward for step in episode])
        for step_info in episode:
            step_hints = step_info.agent_info.get("extra_info", {}).get("step_hints", None)
            if step_hints:
                for hint in step_hints:
                    output.append(
                        {
                            "exp_id": row["exp_id"],
                            "agent_name": row["agent.agent_name"],
                            "benchmark": row["env.task_name"].split(".")[0],
                            "task_name": row["env.task_name"],
                            "episode_reward": episode_reward,
                            "hint": hint,
                        }
                    )
    output = pd.DataFrame(output)
    output = output.dropna()
    return output

def parse_args():
    p = argparse.ArgumentParser(description="Run HITL Generic Agent UI on a benchmark task")
    p.add_argument(
        "--benchmark",
        required=False,
        help="Benchmark name as registered in BrowserGym, e.g., miniwob, workarena_l1, webarena, visualwebarena",
    )
    p.add_argument(
        "--task-name",
        dest="task_name",
        required=False,
        help="Exact task name within the benchmark (e.g., 'miniwob.book-flight')",
    )
    p.add_argument(
        "--seed",
        type=int,
        required=False,
        help="Task seed to use for the selected task.",
    )
    p.add_argument(
        "--llm-config",
        dest="llm_config",
        default="openai/gpt-5-mini-2025-08-07",
        help="LLM configuration to use for the agent (e.g., 'azure/gpt-5-mini-2025-08-07').",
    )
    p.add_argument(
        "--headless",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="Run the browser headless (default: True). Use --no-headless to show the browser.",
    )
    p.add_argument(
        "--download-hints",
        nargs="?",
        const="extracted_hints.csv",
        required=False,
        default=None,
        metavar="[OUTPUT_CSV]",
        help=(
            "Extract hints from the default study directory and save to OUTPUT_CSV. "
            "If OUTPUT_CSV is omitted, saves to 'extracted_hints.csv'. When provided, other args are ignored."
        ),
    )
    return p.parse_args()

def main():
    args = parse_args()
    save_dir = RESULTS_DIR / "agentlab_mentor"
    if args.download_hints:
        df = extract_hints_from_experiment_trace(save_dir)
        out_path = Path(args.download_hints)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        df.to_csv(out_path, index=False)
        print(str(out_path))
        return
    # Validate required args only when not downloading hints
    if not args.benchmark or not args.task_name or args.seed is None:
        raise SystemExit(
            "--benchmark, --task-name, and --seed are required unless using --download-hints"
        )
    benchmark = build_benchmark(args.benchmark, args.task_name, args.seed, args.headless)
    agent_configs = [get_base_agent(args.llm_config)]
    # study is needed to run the 'set_benchmark' method which sets appropriate agent parameters.
    study = Study(agent_args=agent_configs, benchmark=benchmark, logging_level=logging.WARNING)
    study.run(
        n_jobs=1,
        parallel_backend="sequential",
        n_relaunch=1,
        exp_root=save_dir,
    )

if __name__ == "__main__":
    main()


Usage (installed entry point):
    agentlab-mentor --benchmark miniwob --task-name miniwob.book-flight --seed 123 --no-headless

This will run a Study with the MultipleProposalGenericAgent and the selected task.
"""

from __future__ import annotations

import argparse
import logging
from pathlib import Path

import bgym

from agentlab.agents.hitl_agent.generic_human_guided_agent import get_base_agent
from agentlab.experiments.exp_utils import RESULTS_DIR
from agentlab.experiments.study import Study

def build_benchmark(benchmark_name: str, task_name: str, seed: int, headless: bool):
    # Instantiate benchmark by name using BrowserGym registry
    try:
        benchmark = bgym.DEFAULT_BENCHMARKS[benchmark_name.lower()]()
    except KeyError as e:
        choices = ", ".join(sorted(bgym.DEFAULT_BENCHMARKS.keys()))
        raise SystemExit(f"Unknown benchmark '{benchmark_name}'. Choose one of: {choices}") from e

    filtered_env_args = [
        env_args for env_args in benchmark.env_args_list if env_args.task_name == task_name
    ]
    if not filtered_env_args:
        raise SystemExit(f'No tasks found matching "{task_name}"')
    filtered_env_args = filtered_env_args[:1]  # take the first one
    benchmark.env_args_list = filtered_env_args

    # Reasonable defaults for interactive UI
    for env_args in benchmark.env_args_list:
        env_args.task_seed = seed
        env_args.max_steps = env_args.max_steps or 200
        env_args.headless = headless

    return benchmark

def extract_hints_from_experiment_trace(exp_dir):
    """Extracts hints from every step of each episode in a exp_dir and returns a df with each row containing a hint.

    Args:
        exp_dir: Path-like to a study/experiment directory whose results should be scanned.

    Returns:
        pandas.DataFrame: One row per hint with metadata columns.
    """
    import pandas as pd

    from agentlab.analyze import inspect_results
    from agentlab.experiments.exp_utils import RESULTS_DIR
    from agentlab.experiments.loop import ExpResult

    output = []
    # Use provided exp_dir if set; otherwise default to <$AGENTLAB_EXP_ROOT>/agentlab_mentor
    result_df = inspect_results.load_result_df(exp_dir or (RESULTS_DIR / "agentlab_mentor"))


        choices = ", ".join(sorted(bgym.DEFAULT_BENCHMARKS.keys()))
        raise SystemExit(f"Unknown benchmark '{benchmark_name}'. Choose one of: {choices}") from e

    filtered_env_args = [
        env_args for env_args in benchmark.env_args_list if env_args.task_name == task_name
    ]
    if not filtered_env_args:
        raise SystemExit(f'No tasks found matching "{task_name}"')
    filtered_env_args = filtered_env_args[:1]  # take the first one
    benchmark.env_args_list = filtered_env_args

    # Reasonable defaults for interactive UI
    for env_args in benchmark.env_args_list:
        env_args.task_seed = seed
        env_args.max_steps = env_args.max_steps or 200
        env_args.headless = headless

    return benchmark

def extract_hints_from_experiment_trace(exp_dir):
    """Extracts hints from every step of each episode in a exp_dir and returns a df with each row containing a hint.

    Args:
        exp_dir: Path-like to a study/experiment directory whose results should be scanned.

    Returns:
        pandas.DataFrame: One row per hint with metadata columns.
    """
    import pandas as pd

    from agentlab.analyze import inspect_results
    from agentlab.experiments.exp_utils import RESULTS_DIR
    from agentlab.experiments.loop import ExpResult

    output = []
    # Use provided exp_dir if set; otherwise default to <$AGENTLAB_EXP_ROOT>/agentlab_mentor
    result_df = inspect_results.load_result_df(exp_dir or (RESULTS_DIR / "agentlab_mentor"))
    if result_df is None:
        # No results to parse; return empty dataframe with expected columns
        return pd.DataFrame(
            columns=[
                "exp_id",
                "agent_name",
                "benchmark",
                "task_name",
                "episode_reward",
                "hint",
            ]
        )
    result_df = result_df.reset_index()
    for _, row in result_df.iterrows():
        result = ExpResult(row.exp_dir)
        episode = result.steps_info
        episode_reward = max([step.reward for step in episode])
        for step_info in episode:
            step_hints = step_info.agent_info.get("extra_info", {}).get("step_hints", None)
            if step_hints:
                for hint in step_hints:
                    output.append(
                        {
                            "exp_id": row["exp_id"],
                            "agent_name": row["agent.agent_name"],
                            "benchmark": row["env.task_name"].split(".")[0],
                            "task_name": row["env.task_name"],
                            "episode_reward": episode_reward,
                            "hint": hint,
                        }
                    )
    output = pd.DataFrame(output)
    output = output.dropna()
    return output

def parse_args():
    p = argparse.ArgumentParser(description="Run HITL Generic Agent UI on a benchmark task")
    p.add_argument(
        "--benchmark",
        required=False,
        help="Benchmark name as registered in BrowserGym, e.g., miniwob, workarena_l1, webarena, visualwebarena",
    )
    p.add_argument(
        "--task-name",
        dest="task_name",
        required=False,
        help="Exact task name within the benchmark (e.g., 'miniwob.book-flight')",
    )
    p.add_argument(
        "--seed",
        type=int,
        required=False,
        help="Task seed to use for the selected task.",


        episode_reward = max([step.reward for step in episode])
        for step_info in episode:
            step_hints = step_info.agent_info.get("extra_info", {}).get("step_hints", None)
            if step_hints:
                for hint in step_hints:
                    output.append(
                        {
                            "exp_id": row["exp_id"],
                            "agent_name": row["agent.agent_name"],
                            "benchmark": row["env.task_name"].split(".")[0],
                            "task_name": row["env.task_name"],
                            "episode_reward": episode_reward,
                            "hint": hint,
                        }
                    )
    output = pd.DataFrame(output)
    output = output.dropna()
    return output

def parse_args():
    p = argparse.ArgumentParser(description="Run HITL Generic Agent UI on a benchmark task")
    p.add_argument(
        "--benchmark",
        required=False,
        help="Benchmark name as registered in BrowserGym, e.g., miniwob, workarena_l1, webarena, visualwebarena",
    )
    p.add_argument(
        "--task-name",
        dest="task_name",
        required=False,
        help="Exact task name within the benchmark (e.g., 'miniwob.book-flight')",
    )
    p.add_argument(
        "--seed",
        type=int,
        required=False,
        help="Task seed to use for the selected task.",
    )
    p.add_argument(
        "--llm-config",
        dest="llm_config",
        default="openai/gpt-5-mini-2025-08-07",
        help="LLM configuration to use for the agent (e.g., 'azure/gpt-5-mini-2025-08-07').",
    )
    p.add_argument(
        "--headless",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="Run the browser headless (default: True). Use --no-headless to show the browser.",
    )
    p.add_argument(
        "--download-hints",
        nargs="?",
        const="extracted_hints.csv",
        required=False,
        default=None,
        metavar="[OUTPUT_CSV]",
        help=(
            "Extract hints from the default study directory and save to OUTPUT_CSV. "
            "If OUTPUT_CSV is omitted, saves to 'extracted_hints.csv'. When provided, other args are ignored."
        ),
    )
    return p.parse_args()

def main():
    args = parse_args()
    save_dir = RESULTS_DIR / "agentlab_mentor"
    if args.download_hints:
        df = extract_hints_from_experiment_trace(save_dir)
        out_path = Path(args.download_hints)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        df.to_csv(out_path, index=False)
        print(str(out_path))
        return
    # Validate required args only when not downloading hints
    if not args.benchmark or not args.task_name or args.seed is None:
        raise SystemExit(
            "--benchmark, --task-name, and --seed are required unless using --download-hints"
        )
    benchmark = build_benchmark(args.benchmark, args.task_name, args.seed, args.headless)
    agent_configs = [get_base_agent(args.llm_config)]
    # study is needed to run the 'set_benchmark' method which sets appropriate agent parameters.


main

        "--headless",
        action=argparse.BooleanOptionalAction,
        default=True,
        help="Run the browser headless (default: True). Use --no-headless to show the browser.",
    )
    p.add_argument(
        "--download-hints",
        nargs="?",
        const="extracted_hints.csv",
        required=False,
        default=None,
        metavar="[OUTPUT_CSV]",
        help=(
            "Extract hints from the default study directory and save to OUTPUT_CSV. "
            "If OUTPUT_CSV is omitted, saves to 'extracted_hints.csv'. When provided, other args are ignored."
        ),
    )
    return p.parse_args()

def main():
    args = parse_args()
    save_dir = RESULTS_DIR / "agentlab_mentor"
    if args.download_hints:
        df = extract_hints_from_experiment_trace(save_dir)
        out_path = Path(args.download_hints)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        df.to_csv(out_path, index=False)
        print(str(out_path))
        return
    # Validate required args only when not downloading hints
    if not args.benchmark or not args.task_name or args.seed is None:
        raise SystemExit(
            "--benchmark, --task-name, and --seed are required unless using --download-hints"
        )
    benchmark = build_benchmark(args.benchmark, args.task_name, args.seed, args.headless)
    agent_configs = [get_base_agent(args.llm_config)]
    # study is needed to run the 'set_benchmark' method which sets appropriate agent parameters.
    study = Study(agent_args=agent_configs, benchmark=benchmark, logging_level=logging.WARNING)
    study.run(
        n_jobs=1,
        parallel_backend="sequential",
        n_relaunch=1,
        exp_root=save_dir,
    )

if __name__ == "__main__":
    main()


import re
from dataclasses import asdict, dataclass
from typing import Dict, List

from browsergym.experiments.agent import AgentInfo

from agentlab.agents import dynamic_prompting as dp
from agentlab.agents.generic_agent.generic_agent import GenericAgent, GenericAgentArgs
from agentlab.agents.generic_agent.generic_agent_prompt import MainPrompt
from agentlab.llm.llm_utils import Discussion, HumanMessage, SystemMessage

class CandidatesGeneration(dp.PromptElement):
    # Ask for multiple alternatives; each candidate must contain <think> and <action>.
    def __init__(self, hint: list[str] | None = None, n_candidates=3) -> None:
        self.hint = hint
        self.n_candidates = n_candidates
        self.hint_prompt = "\n".join(f"{i}. {c}" for i, c in enumerate(hint, 1)) if hint else ""
        super().__init__(True)
        self._prompt = [
            dict(
                type="text",
                text=f"""
    You are a web agent. Propose {self.n_candidates} alternative next steps for the current page.
    {('Use the Hints:' + self.hint_prompt) if self.hint else ""}\n
    Return EACH candidate wrapped as numbered tags:
    <candidate_generation_1>...</candidate_generation_1>
    <candidate_generation_2>...</candidate_generation_2>

    Inside every candidate you MUST include:
    <think>...why this action is appropriate now...</think>
    <action>...ONE atomic, executable action string...</action>

    Do not include any extra text outside the candidate tags.
    Use this format:
    <candidate_generation_1>
    <think>Explain why Candidate One is chosen</think>
    <action>Candidate One Action</action>
    </candidate_generation_1>

    <candidate_generation_2>
    <think>Explain why Candidate Two is chosen</think>
    <action>Candidate Two Action</action>
    </candidate_generation_2>
    # Example
    <candidate_generation_1>
    <think>The login button is visible and proceeding will reveal the auth form.</think>
    <action>click(role="button", name="Log in")</action>
    </candidate_generation_1>

    <candidate_generation_2>
    <think>User might need to enter email first; the email field is focused and visible.</think>
    <action>fill(bid="a112", text="user@example.com")</action>
    </candidate_generation_2>
    """,
            )
        ]

    # Regex patterns for numbered candidates only
    _NUM_BLOCK = re.compile(
        r"<\s*candidate[_ ]generation[_ ](?P<idx>[0-9]+)\s*>(?P<body>.*?)<\s*/\s*candidate[_ ]generation[_ ](?P=idx)\s*>",
        flags=re.IGNORECASE | re.DOTALL,
    )
    _THINK_PATTERN = re.compile(
        r"<\s*think\s*>(?P<think>.*?)<\s*/\s*think\s*>",
        flags=re.IGNORECASE | re.DOTALL,
    )
    _ACTION_PATTERN = re.compile(
        r"<\s*action\s*>(?P<action>.*?)<\s*/\s*action\s*>",
        flags=re.IGNORECASE | re.DOTALL,
    )

    def _parse_answer(self, text_answer: str) -> Dict[str, Dict[str, str]]:
        """Extract up to n_candidates candidates, using numbered tags only.

        Args:
            text_answer: The text response containing candidate generation tags.

        Returns:
            Dictionary mapping candidate names to their think and action content.
            Format: {"candidate_generation_1": {"think": "...", "action": "..."}, ...}
        """
        result = {
            f"candidate_generation_{i+1}": {"think": "", "action": ""}
            for i in range(self.n_candidates)
        }

        if not isinstance(text_answer, str):
            return result

        matches: List[re.Match] = list(self._NUM_BLOCK.finditer(text_answer))
        # Sort by numeric index
        matches_sorted = sorted(matches, key=lambda m: int(m.group("idx")))
        for i, m in enumerate(matches_sorted[: self.n_candidates]):
            body = m.group("body").strip()
            think_m = self._THINK_PATTERN.search(body)
            action_m = self._ACTION_PATTERN.search(body)
            result[f"candidate_generation_{i+1}"] = {
                "think": (think_m.group("think").strip() if think_m else ""),
                "action": (action_m.group("action").strip() if action_m else ""),
            }

        return result

class MultiCandidateGenericAgent(GenericAgent):

    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry: int = 4,
    ):
        super().__init__(chat_model_args, flags, max_retry)

    def get_candidate_generations(
        self,
        obs,
        hint: list[str] | None = None,
        n_candidates=3,
    ) -> list[dict]:
        # Append obs to history only if it's not already the last entry
        # Important to handle cases when get_candidate_generation is called multiple times in a single step.
        if not self.obs_history or self.obs_history[-1] is not obs:
            self.obs_history.append(obs)

        main_prompt = MainPrompt(
            action_set=self.action_set,
            obs_history=self.obs_history,
            actions=self.actions,
            memories=self.memories,
            thoughts=self.thoughts,
            previous_plan=self.plan,
            step=self.plan_step,
            flags=self.flags,
        )
        max_prompt_tokens, max_trunc_itr = self._get_maxes()

        system_prompt = SystemMessage(dp.SystemPrompt().prompt)

        human_prompt = dp.fit_tokens(
            shrinkable=main_prompt,
            max_prompt_tokens=max_prompt_tokens,
            model_name=self.chat_model_args.model_name,
            max_iterations=max_trunc_itr,
            additional_prompts=system_prompt,
        )

        cg = CandidatesGeneration(hint=hint, n_candidates=n_candidates)
        candidates_prompt = HumanMessage(cg.prompt)
        chat_messages = Discussion([system_prompt, human_prompt, candidates_prompt])
        output = self.chat_llm(chat_messages)
        candidates = cg._parse_answer(output["content"])
        # Not adding the generate candidate prompt to xray.
        msg_to_add_to_xray = Discussion([system_prompt, human_prompt])
        suggestions = [
            {
                "action": candidate["action"],
                "think": candidate["think"],
            }
            for key, candidate in candidates.items()
        ]
        output = []
        for candidate in suggestions:
            agent_info = AgentInfo(
                think=candidate.get("think", None),
                chat_messages=msg_to_add_to_xray,
                stats=self.chat_llm.get_stats(),
                extra_info={
                    "chat_model_args": asdict(self.chat_model_args),
                    "think": candidate.get("think", None),
                    "plan": candidate.get("plan", None),
                    "step": candidate.get("step", None),
                    "memory": candidate.get("memory", None),
                },
            )
            output.append({"action": candidate["action"], "agent_info": agent_info})

        return output

    def update_agent_state_from_selected_candidate(self, output):
        """Updates the agent's internal state based on the selected candidate from human feedback.

        Args:
            output: Dictionary containing 'action' and 'agent_info' keys from selected candidate.
        """
        action, agent_info = output["action"], output["agent_info"]
        self.plan = agent_info.extra_info.get("plan", self.plan)
        self.plan_step = agent_info.extra_info.get("step", self.plan_step)
        self.memories.append(agent_info.extra_info.get("memory", None))
        self.thoughts.append(agent_info.extra_info.get("think", None))
        self.actions.append(action)

    def get_action(self, obs):
        """Generates multiple candidates and always returns the first one.
        This allows to use this agent as a drop-in replacement for a single-candidate agent.

        Args:
            obs: The observation from the environment.

        Returns:
            tuple: A tuple containing (action, agent_info).
        """
        candidates = self.get_candidate_generations(obs, hint=None, n_candidates=2)
        selection = candidates[0]  # always select the first option.
        self.update_agent_state_from_selected_candidate(selection)
        action, agent_info = selection["action"], selection["agent_info"]

        return action, agent_info

@dataclass
class MultiCandidateGenericAgentArgs(GenericAgentArgs):
    def make_agent(self):
        return MultiCandidateGenericAgent(
            chat_model_args=self.chat_model_args,
            flags=self.flags,
            max_retry=self.max_retry,
        )

    def __post_init__(self):
        """Prefix subagent name with 'MC-'."""
        super().__post_init__()
        if hasattr(self, "agent_name") and self.agent_name:
            self.agent_name = "MC-" + self.agent_name


CandidatesGeneration

import re
from dataclasses import asdict, dataclass
from typing import Dict, List

from browsergym.experiments.agent import AgentInfo

from agentlab.agents import dynamic_prompting as dp
from agentlab.agents.generic_agent.generic_agent import GenericAgent, GenericAgentArgs
from agentlab.agents.generic_agent.generic_agent_prompt import MainPrompt
from agentlab.llm.llm_utils import Discussion, HumanMessage, SystemMessage

class CandidatesGeneration(dp.PromptElement):
    # Ask for multiple alternatives; each candidate must contain <think> and <action>.
    def __init__(self, hint: list[str] | None = None, n_candidates=3) -> None:
        self.hint = hint
        self.n_candidates = n_candidates
        self.hint_prompt = "\n".join(f"{i}. {c}" for i, c in enumerate(hint, 1)) if hint else ""
        super().__init__(True)
        self._prompt = [
            dict(
                type="text",
                text=f"""
    You are a web agent. Propose {self.n_candidates} alternative next steps for the current page.
    {('Use the Hints:' + self.hint_prompt) if self.hint else ""}\n
    Return EACH candidate wrapped as numbered tags:
    <candidate_generation_1>...</candidate_generation_1>
    <candidate_generation_2>...</candidate_generation_2>

    Inside every candidate you MUST include:
    <think>...why this action is appropriate now...</think>
    <action>...ONE atomic, executable action string...</action>

    Do not include any extra text outside the candidate tags.
    Use this format:
    <candidate_generation_1>
    <think>Explain why Candidate One is chosen</think>
    <action>Candidate One Action</action>
    </candidate_generation_1>

    <candidate_generation_2>
    <think>Explain why Candidate Two is chosen</think>
    <action>Candidate Two Action</action>
    </candidate_generation_2>
    # Example
    <candidate_generation_1>
    <think>The login button is visible and proceeding will reveal the auth form.</think>
    <action>click(role="button", name="Log in")</action>
    </candidate_generation_1>

    <candidate_generation_2>
    <think>User might need to enter email first; the email field is focused and visible.</think>
    <action>fill(bid="a112", text="user@example.com")</action>
    </candidate_generation_2>
    """,
            )
        ]

    # Regex patterns for numbered candidates only
    _NUM_BLOCK = re.compile(
        r"<\s*candidate[_ ]generation[_ ](?P<idx>[0-9]+)\s*>(?P<body>.*?)<\s*/\s*candidate[_ ]generation[_ ](?P=idx)\s*>",
        flags=re.IGNORECASE | re.DOTALL,
    )
    _THINK_PATTERN = re.compile(
        r"<\s*think\s*>(?P<think>.*?)<\s*/\s*think\s*>",
        flags=re.IGNORECASE | re.DOTALL,
    )
    _ACTION_PATTERN = re.compile(
        r"<\s*action\s*>(?P<action>.*?)<\s*/\s*action\s*>",
        flags=re.IGNORECASE | re.DOTALL,
    )

    def _parse_answer(self, text_answer: str) -> Dict[str, Dict[str, str]]:
        """Extract up to n_candidates candidates, using numbered tags only.

        Args:
            text_answer: The text response containing candidate generation tags.

        Returns:
            Dictionary mapping candidate names to their think and action content.
            Format: {"candidate_generation_1": {"think": "...", "action": "..."}, ...}
        """
        result = {
            f"candidate_generation_{i+1}": {"think": "", "action": ""}
            for i in range(self.n_candidates)
        }

        if not isinstance(text_answer, str):
            return result

        matches: List[re.Match] = list(self._NUM_BLOCK.finditer(text_answer))
        # Sort by numeric index
        matches_sorted = sorted(matches, key=lambda m: int(m.group("idx")))
        for i, m in enumerate(matches_sorted[: self.n_candidates]):
            body = m.group("body").strip()
            think_m = self._THINK_PATTERN.search(body)
            action_m = self._ACTION_PATTERN.search(body)
            result[f"candidate_generation_{i+1}"] = {
                "think": (think_m.group("think").strip() if think_m else ""),
                "action": (action_m.group("action").strip() if action_m else ""),
            }

        return result

class MultiCandidateGenericAgent(GenericAgent):

    def __init__(
        self,
        chat_model_args,
        flags,
        max_retry: int = 4,
    ):
        super().__init__(chat_model_args, flags, max_retry)

    def get_candidate_generations(
        self,
        obs,
        hint: list[str] | None = None,
        n_candidates=3,
    ) -> list[dict]:
        # Append obs to history only if it's not already the last entry
        # Important to handle cases when get_candidate_generation is called multiple times in a single step.