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
minicommand 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 | |
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 | |
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 | |
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 | |
execute_action
execute_action(action: dict) -> dict
Source code in src/minisweagent/agents/interactive.py
93 94 95 96 97 | |
should_ask_confirmation
should_ask_confirmation(action: str) -> bool
Source code in src/minisweagent/agents/interactive.py
99 100 | |
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 | |
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 | |