Skip to content

Interactive

InteractiveAgent class

Full source code
"""A small generalization of the default agent that puts the user in the loop.

There are three modes:
- human: commands issued by the user are executed immediately
- confirm: commands issued by the LM but not whitelisted are confirmed by the user
- yolo: commands issued by the LM are executed immediately without confirmation
"""

import re
from dataclasses import dataclass, field
from typing import Literal

from prompt_toolkit.history import FileHistory
from prompt_toolkit.shortcuts import PromptSession
from rich.console import Console
from rich.rule import Rule

from minisweagent import global_config_dir
from minisweagent.agents.default import AgentConfig, DefaultAgent, LimitsExceeded, NonTerminatingException, Submitted

console = Console(highlight=False)
prompt_session = PromptSession(history=FileHistory(global_config_dir / "interactive_history.txt"))


@dataclass
class InteractiveAgentConfig(AgentConfig):
    mode: Literal["human", "confirm", "yolo"] = "confirm"
    """Whether to confirm actions."""
    whitelist_actions: list[str] = field(default_factory=list)
    """Never confirm actions that match these regular expressions."""
    confirm_exit: bool = True
    """If the agent wants to finish, do we ask for confirmation from user?"""


class InteractiveAgent(DefaultAgent):
    _MODE_COMMANDS_MAPPING = {"/u": "human", "/c": "confirm", "/y": "yolo"}

    def __init__(self, *args, config_class=InteractiveAgentConfig, **kwargs):
        super().__init__(*args, config_class=config_class, **kwargs)
        self.cost_last_confirmed = 0.0

    def add_message(self, role: str, content: str, **kwargs):
        # Extend supermethod to print messages
        super().add_message(role, content, **kwargs)
        if role == "assistant":
            console.print(
                f"\n[red][bold]mini-swe-agent[/bold] (step [bold]{self.model.n_calls}[/bold], [bold]${self.model.cost:.2f}[/bold]):[/red]\n",
                end="",
                highlight=False,
            )
        else:
            console.print(f"\n[bold green]{role.capitalize()}[/bold green]:\n", end="", highlight=False)
        console.print(content, highlight=False, markup=False)

    def query(self) -> dict:
        # Extend supermethod to handle human mode
        if self.config.mode == "human":
            match command := self._prompt_and_handle_special("[bold yellow]>[/bold yellow] "):
                case "/y" | "/c":  # Just go to the super query, which queries the LM for the next action
                    pass
                case _:
                    msg = {"content": f"\n```bash\n{command}\n```"}
                    self.add_message("assistant", msg["content"])
                    return msg
        try:
            with console.status("Waiting for the LM to respond..."):
                return super().query()
        except LimitsExceeded:
            console.print(
                f"Limits exceeded. Limits: {self.config.step_limit} steps, ${self.config.cost_limit}.\n"
                f"Current spend: {self.model.n_calls} steps, ${self.model.cost:.2f}."
            )
            self.config.step_limit = int(input("New step limit: "))
            self.config.cost_limit = float(input("New cost limit: "))
            return super().query()

    def step(self) -> dict:
        # Override the step method to handle user interruption
        try:
            console.print(Rule())
            return super().step()
        except KeyboardInterrupt:
            # We always add a message about the interrupt and then just proceed to the next step
            interruption_message = self._prompt_and_handle_special(
                "\n\n[bold yellow]Interrupted.[/bold yellow] "
                "[green]Type a comment/command[/green] (/h for available commands)"
                "\n[bold yellow]>[/bold yellow] "
            ).strip()
            if not interruption_message or interruption_message in self._MODE_COMMANDS_MAPPING:
                interruption_message = "Temporary interruption caught."
            raise NonTerminatingException(f"Interrupted by user: {interruption_message}")

    def execute_action(self, action: dict) -> dict:
        # Override the execute_action method to handle user confirmation
        if self.should_ask_confirmation(action["action"]):
            self.ask_confirmation()
        return super().execute_action(action)

    def should_ask_confirmation(self, action: str) -> bool:
        return self.config.mode == "confirm" and not any(re.match(r, action) for r in self.config.whitelist_actions)

    def ask_confirmation(self) -> None:
        prompt = (
            "[bold yellow]Execute?[/bold yellow] [green][bold]Enter[/bold] to confirm[/green], "
            "or [green]Type a comment/command[/green] (/h for available commands)\n"
            "[bold yellow]>[/bold yellow] "
        )
        match user_input := self._prompt_and_handle_special(prompt).strip():
            case "" | "/y":
                pass  # confirmed, do nothing
            case "/u":  # Skip execution action and get back to query
                raise NonTerminatingException("Command not executed. Switching to human mode")
            case _:
                raise NonTerminatingException(
                    f"Command not executed. The user rejected your command with the following message: {user_input}"
                )

    def _prompt_and_handle_special(self, prompt: str) -> str:
        """Prompts the user, takes care of /h (followed by requery) and sets the mode. Returns the user input."""
        console.print(prompt, end="")
        user_input = prompt_session.prompt("")
        if user_input == "/h":
            console.print(
                f"Current mode: [bold green]{self.config.mode}[/bold green]\n"
                f"[bold green]/y[/bold green] to switch to [bold yellow]yolo[/bold yellow] mode (execute LM commands without confirmation)\n"
                f"[bold green]/c[/bold green] to switch to [bold yellow]confirmation[/bold yellow] mode (ask for confirmation before executing LM commands)\n"
                f"[bold green]/u[/bold green] to switch to [bold yellow]human[/bold yellow] mode (execute commands issued by the user)\n"
            )
            return self._prompt_and_handle_special(prompt)
        if user_input in self._MODE_COMMANDS_MAPPING:
            if self.config.mode == self._MODE_COMMANDS_MAPPING[user_input]:
                return self._prompt_and_handle_special(
                    f"[bold red]Already in {self.config.mode} mode.[/bold red]\n{prompt}"
                )
            self.config.mode = self._MODE_COMMANDS_MAPPING[user_input]
            console.print(f"Switched to [bold green]{self.config.mode}[/bold green] mode.")
            return user_input
        return user_input

    def has_finished(self, output: dict[str, str]):
        try:
            return super().has_finished(output)
        except Submitted as e:
            if self.config.confirm_exit:
                console.print(
                    "[bold green]Agent wants to finish.[/bold green] "
                    "[green]Type a comment to give it a new task or press enter to quit.\n"
                    "[bold yellow]>[/bold yellow] ",
                    end="",
                )
                if new_task := self._prompt_and_handle_special("").strip():
                    raise NonTerminatingException(f"The user added a new task: {new_task}")
            raise e

See also

  • This agent subclass builds on top of the default agent, make sure to read that first.
  • This class powers the mini command line tool, see usage for more details.

minisweagent.agents.interactive

A small generalization of the default agent that puts the user in the loop.

There are three modes: - human: commands issued by the user are executed immediately - confirm: commands issued by the LM but not whitelisted are confirmed by the user - yolo: commands issued by the LM are executed immediately without confirmation

console module-attribute

console = Console(highlight=False)

prompt_session module-attribute

prompt_session = PromptSession(
    history=FileHistory(
        global_config_dir / "interactive_history.txt"
    )
)

InteractiveAgentConfig dataclass

InteractiveAgentConfig(
    system_template: str = "You are a helpful assistant that can do anything.",
    instance_template: str = "Your task: {{task}}. Please reply with a single shell command in triple backticks. To finish, the first line of the output of the shell command must be 'COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT'.",
    timeout_template: str = "The last command <command>{{action['action']}}</command> timed out and has been killed.\nThe output of the command was:\n <output>\n{{output}}\n</output>\nPlease try another command and make sure to avoid those requiring interactive input.",
    format_error_template: str = "Please always provide EXACTLY ONE action in triple backticks.",
    action_observation_template: str = "Observation: {{output}}",
    step_limit: int = 0,
    cost_limit: float = 3.0,
    mode: Literal["human", "confirm", "yolo"] = "confirm",
    whitelist_actions: list[str] = list(),
    confirm_exit: bool = True,
)

Bases: AgentConfig

mode class-attribute instance-attribute

mode: Literal['human', 'confirm', 'yolo'] = 'confirm'

Whether to confirm actions.

whitelist_actions class-attribute instance-attribute

whitelist_actions: list[str] = field(default_factory=list)

Never confirm actions that match these regular expressions.

confirm_exit class-attribute instance-attribute

confirm_exit: bool = True

If the agent wants to finish, do we ask for confirmation from user?

InteractiveAgent

InteractiveAgent(
    *args, config_class=InteractiveAgentConfig, **kwargs
)

Bases: DefaultAgent

Source code in src/minisweagent/agents/interactive.py
38
39
40
def __init__(self, *args, config_class=InteractiveAgentConfig, **kwargs):
    super().__init__(*args, config_class=config_class, **kwargs)
    self.cost_last_confirmed = 0.0

cost_last_confirmed instance-attribute

cost_last_confirmed = 0.0

add_message

add_message(role: str, content: str, **kwargs)
Source code in src/minisweagent/agents/interactive.py
42
43
44
45
46
47
48
49
50
51
52
53
def add_message(self, role: str, content: str, **kwargs):
    # Extend supermethod to print messages
    super().add_message(role, content, **kwargs)
    if role == "assistant":
        console.print(
            f"\n[red][bold]mini-swe-agent[/bold] (step [bold]{self.model.n_calls}[/bold], [bold]${self.model.cost:.2f}[/bold]):[/red]\n",
            end="",
            highlight=False,
        )
    else:
        console.print(f"\n[bold green]{role.capitalize()}[/bold green]:\n", end="", highlight=False)
    console.print(content, highlight=False, markup=False)

query

query() -> dict
Source code in src/minisweagent/agents/interactive.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def query(self) -> dict:
    # Extend supermethod to handle human mode
    if self.config.mode == "human":
        match command := self._prompt_and_handle_special("[bold yellow]>[/bold yellow] "):
            case "/y" | "/c":  # Just go to the super query, which queries the LM for the next action
                pass
            case _:
                msg = {"content": f"\n```bash\n{command}\n```"}
                self.add_message("assistant", msg["content"])
                return msg
    try:
        with console.status("Waiting for the LM to respond..."):
            return super().query()
    except LimitsExceeded:
        console.print(
            f"Limits exceeded. Limits: {self.config.step_limit} steps, ${self.config.cost_limit}.\n"
            f"Current spend: {self.model.n_calls} steps, ${self.model.cost:.2f}."
        )
        self.config.step_limit = int(input("New step limit: "))
        self.config.cost_limit = float(input("New cost limit: "))
        return super().query()

step

step() -> dict
Source code in src/minisweagent/agents/interactive.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def step(self) -> dict:
    # Override the step method to handle user interruption
    try:
        console.print(Rule())
        return super().step()
    except KeyboardInterrupt:
        # We always add a message about the interrupt and then just proceed to the next step
        interruption_message = self._prompt_and_handle_special(
            "\n\n[bold yellow]Interrupted.[/bold yellow] "
            "[green]Type a comment/command[/green] (/h for available commands)"
            "\n[bold yellow]>[/bold yellow] "
        ).strip()
        if not interruption_message or interruption_message in self._MODE_COMMANDS_MAPPING:
            interruption_message = "Temporary interruption caught."
        raise NonTerminatingException(f"Interrupted by user: {interruption_message}")

execute_action

execute_action(action: dict) -> dict
Source code in src/minisweagent/agents/interactive.py
93
94
95
96
97
def execute_action(self, action: dict) -> dict:
    # Override the execute_action method to handle user confirmation
    if self.should_ask_confirmation(action["action"]):
        self.ask_confirmation()
    return super().execute_action(action)

should_ask_confirmation

should_ask_confirmation(action: str) -> bool
Source code in src/minisweagent/agents/interactive.py
 99
100
def should_ask_confirmation(self, action: str) -> bool:
    return self.config.mode == "confirm" and not any(re.match(r, action) for r in self.config.whitelist_actions)

ask_confirmation

ask_confirmation() -> None
Source code in src/minisweagent/agents/interactive.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def ask_confirmation(self) -> None:
    prompt = (
        "[bold yellow]Execute?[/bold yellow] [green][bold]Enter[/bold] to confirm[/green], "
        "or [green]Type a comment/command[/green] (/h for available commands)\n"
        "[bold yellow]>[/bold yellow] "
    )
    match user_input := self._prompt_and_handle_special(prompt).strip():
        case "" | "/y":
            pass  # confirmed, do nothing
        case "/u":  # Skip execution action and get back to query
            raise NonTerminatingException("Command not executed. Switching to human mode")
        case _:
            raise NonTerminatingException(
                f"Command not executed. The user rejected your command with the following message: {user_input}"
            )

has_finished

has_finished(output: dict[str, str])
Source code in src/minisweagent/agents/interactive.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def has_finished(self, output: dict[str, str]):
    try:
        return super().has_finished(output)
    except Submitted as e:
        if self.config.confirm_exit:
            console.print(
                "[bold green]Agent wants to finish.[/bold green] "
                "[green]Type a comment to give it a new task or press enter to quit.\n"
                "[bold yellow]>[/bold yellow] ",
                end="",
            )
            if new_task := self._prompt_and_handle_special("").strip():
                raise NonTerminatingException(f"The user added a new task: {new_task}")
        raise e