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 typing import Literal, NoReturn
from rich.console import Console
from rich.rule import Rule
from minisweagent.agents.default import AgentConfig, DefaultAgent
from minisweagent.agents.utils.prompt_user import _multiline_prompt, prompt_session
from minisweagent.exceptions import LimitsExceeded, Submitted, UserInterruption
from minisweagent.models.utils.content_string import get_content_string
console = Console(highlight=False)
class InteractiveAgentConfig(AgentConfig):
mode: Literal["human", "confirm", "yolo"] = "confirm"
"""Whether to confirm actions."""
whitelist_actions: list[str] = []
"""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 _interrupt(self, content: str, *, itype: str = "UserInterruption") -> NoReturn:
raise UserInterruption({"role": "user", "content": content, "extra": {"interrupt_type": itype}})
def add_messages(self, *messages: dict) -> list[dict]:
# Extend supermethod to print messages
for msg in messages:
role, content = msg.get("role") or msg.get("type", "unknown"), get_content_string(msg)
if role == "assistant":
console.print(
f"\n[red][bold]mini-swe-agent[/bold] (step [bold]{self.n_calls}[/bold], [bold]${self.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)
return super().add_messages(*messages)
def query(self) -> dict:
# Extend supermethod to handle human mode
if self.config.mode == "human":
match command := self._prompt_and_handle_slash_commands("[bold yellow]>[/bold yellow] "):
case "/y" | "/c":
pass
case _:
msg = {
"role": "user",
"content": f"User command: \n```bash\n{command}\n```",
"extra": {"actions": [{"command": command}]},
}
self.add_messages(msg)
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.n_calls} steps, ${self.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) -> list[dict]:
# Override the step method to handle user interruption
try:
console.print(Rule())
return super().step()
except KeyboardInterrupt:
interruption_message = self._prompt_and_handle_slash_commands(
"\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."
self._interrupt(f"Interrupted by user: {interruption_message}")
def execute_actions(self, message: dict) -> list[dict]:
# Override to handle user confirmation and confirm_exit, with try/finally to preserve partial outputs
actions = message.get("extra", {}).get("actions", [])
commands = [action["command"] for action in actions]
outputs = []
try:
self._ask_confirmation_or_interrupt(commands)
for action in actions:
outputs.append(self.env.execute(action))
except Submitted as e:
self._check_for_new_task_or_submit(e)
finally:
result = self.add_messages(
*self.model.format_observation_messages(message, outputs, self.get_template_vars())
)
return result
def _add_observation_messages(self, message: dict, outputs: list[dict]) -> list[dict]:
return self.add_messages(*self.model.format_observation_messages(message, outputs, self.get_template_vars()))
def _check_for_new_task_or_submit(self, e: Submitted) -> NoReturn:
"""Check if user wants to add a new task or submit."""
if self.config.confirm_exit:
message = (
"[bold yellow]Agent wants to finish.[/bold yellow] "
"[bold green]Type new task[/bold green] or [bold]Enter[/bold] to quit "
"([bold]/h[/bold] for commands)\n"
"[bold yellow]>[/bold yellow] "
)
user_input = self._prompt_and_handle_slash_commands(message).strip()
if user_input == "/u": # directly continue
self._interrupt("Switched to human mode.")
elif user_input in self._MODE_COMMANDS_MAPPING: # ask again
return self._check_for_new_task_or_submit(e)
elif user_input:
self._interrupt(f"The user added a new task: {user_input}", itype="UserNewTask")
raise e
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_or_interrupt(self, commands: list[str]) -> None:
if not any(self._should_ask_confirmation(c) for c in commands):
return
prompt = (
f"[bold yellow]Execute {len(commands)} action(s)?[/] [green][bold]Enter[/] to confirm[/], "
"[red]type [bold]comment[/] to reject[/], or [blue][bold]/h[/] to show available commands[/]\n"
"[bold yellow]>[/bold yellow] "
)
match user_input := self._prompt_and_handle_slash_commands(prompt).strip():
case "" | "/y":
pass # confirmed, do nothing
case "/u": # Skip execution action and get back to query
self._interrupt("Commands not executed. Switching to human mode", itype="UserRejection")
case _:
self._interrupt(
f"Commands not executed. The user rejected your commands with the following message: {user_input}",
itype="UserRejection",
)
def _prompt_and_handle_slash_commands(self, prompt: str, *, _multiline: bool = False) -> str:
"""Prompts the user, takes care of /h (followed by requery) and sets the mode. Returns the user input."""
console.print(prompt, end="")
if _multiline:
return _multiline_prompt()
user_input = prompt_session.prompt("")
if user_input == "/m":
return self._prompt_and_handle_slash_commands(prompt, _multiline=True)
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"
f"[bold green]/m[/bold green] to enter multiline comment",
)
return self._prompt_and_handle_slash_commands(prompt)
if user_input in self._MODE_COMMANDS_MAPPING:
if self.config.mode == self._MODE_COMMANDS_MAPPING[user_input]:
return self._prompt_and_handle_slash_commands(
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
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)
InteractiveAgentConfig
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] = []
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
35 36 37 | |
cost_last_confirmed
instance-attribute
cost_last_confirmed = 0.0
add_messages
add_messages(*messages: dict) -> list[dict]
Source code in src/minisweagent/agents/interactive.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
query
query() -> dict
Source code in src/minisweagent/agents/interactive.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
step
step() -> list[dict]
Source code in src/minisweagent/agents/interactive.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
execute_actions
execute_actions(message: dict) -> list[dict]
Source code in src/minisweagent/agents/interactive.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |