Skip to content

Commit

Permalink
Feat add agent manager (All-Hands-AI#904)
Browse files Browse the repository at this point in the history
* feat: add agent manager to manage all agents;

* extract the host ssh port to prevent conflict.

* clean all containers with prefix is sandbox-

* merge from upstream/main

* merge from upstream/main

* Update frontend/src/state/settingsSlice.ts

* Update opendevin/sandbox/ssh_box.py

* Update opendevin/sandbox/exec_box.py

---------

Co-authored-by: Robert Brennan <[email protected]>
  • Loading branch information
iFurySt and rbren authored Apr 12, 2024
1 parent ded0a76 commit 494a1b6
Show file tree
Hide file tree
Showing 15 changed files with 460 additions and 398 deletions.
18 changes: 9 additions & 9 deletions opendevin/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from opendevin.state import State
from opendevin.llm.llm import LLM


class Agent(ABC):
"""
This abstract base class is an general interface for an agent dedicated to
Expand All @@ -14,11 +15,11 @@ class Agent(ABC):
It tracks the execution status and maintains a history of interactions.
"""

_registry: Dict[str, Type["Agent"]] = {}
_registry: Dict[str, Type['Agent']] = {}

def __init__(
self,
llm: LLM,
self,
llm: LLM,
):
self.llm = llm
self._complete = False
Expand All @@ -34,7 +35,7 @@ def complete(self) -> bool:
return self._complete

@abstractmethod
def step(self, state: "State") -> "Action":
def step(self, state: 'State') -> 'Action':
"""
Starts the execution of the assigned instruction. This method should
be implemented by subclasses to define the specific execution logic.
Expand Down Expand Up @@ -63,7 +64,7 @@ def reset(self) -> None:
self._complete = False

@classmethod
def register(cls, name: str, agent_cls: Type["Agent"]):
def register(cls, name: str, agent_cls: Type['Agent']):
"""
Registers an agent class in the registry.
Expand All @@ -76,7 +77,7 @@ def register(cls, name: str, agent_cls: Type["Agent"]):
cls._registry[name] = agent_cls

@classmethod
def get_cls(cls, name: str) -> Type["Agent"]:
def get_cls(cls, name: str) -> Type['Agent']:
"""
Retrieves an agent class from the registry.
Expand All @@ -91,11 +92,10 @@ def get_cls(cls, name: str) -> Type["Agent"]:
return cls._registry[name]

@classmethod
def listAgents(cls) -> list[str]:
def list_agents(cls) -> list[str]:
"""
Retrieves the list of all agent names from the registry.
"""
if not bool(cls._registry):
raise ValueError("No agent class registered.")
raise ValueError('No agent class registered.')
return list(cls._registry.keys())

124 changes: 63 additions & 61 deletions opendevin/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,87 @@

from termcolor import colored

from opendevin.plan import Plan
from opendevin.state import State
from opendevin.agent import Agent
from opendevin import config
from opendevin.action import (
Action,
NullAction,
AgentFinishAction,
AddTaskAction,
ModifyTaskAction,
)
from opendevin.observation import Observation, AgentErrorObservation, NullObservation
from opendevin import config
from opendevin.agent import Agent
from opendevin.logger import opendevin_logger as logger

from opendevin.observation import Observation, AgentErrorObservation, NullObservation
from opendevin.plan import Plan
from opendevin.state import State
from .command_manager import CommandManager


ColorType = Literal[
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'light_grey',
'dark_grey',
'light_red',
'light_green',
'light_yellow',
'light_blue',
'light_magenta',
'light_cyan',
'white',
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"light_grey",
"dark_grey",
"light_red",
"light_green",
"light_yellow",
"light_blue",
"light_magenta",
"light_cyan",
"white",
]


DISABLE_COLOR_PRINTING = (
config.get_or_default('DISABLE_COLOR', 'false').lower() == 'true'
config.get_or_default("DISABLE_COLOR", "false").lower() == "true"
)
MAX_ITERATIONS = config.get('MAX_ITERATIONS')
MAX_ITERATIONS = config.get("MAX_ITERATIONS")


def print_with_color(text: Any, print_type: str = 'INFO'):
def print_with_color(text: Any, print_type: str = "INFO"):
TYPE_TO_COLOR: Mapping[str, ColorType] = {
'BACKGROUND LOG': 'blue',
'ACTION': 'green',
'OBSERVATION': 'yellow',
'INFO': 'cyan',
'ERROR': 'red',
'PLAN': 'light_magenta',
"BACKGROUND LOG": "blue",
"ACTION": "green",
"OBSERVATION": "yellow",
"INFO": "cyan",
"ERROR": "red",
"PLAN": "light_magenta",
}
color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR['INFO'])
color = TYPE_TO_COLOR.get(print_type.upper(), TYPE_TO_COLOR["INFO"])
if DISABLE_COLOR_PRINTING:
print(f"\n{print_type.upper()}:\n{str(text)}", flush=True)
else:
print(
colored(f"\n{print_type.upper()}:\n", color, attrs=['bold'])
colored(f"\n{print_type.upper()}:\n", color, attrs=["bold"])
+ colored(str(text), color),
flush=True,
)


class AgentController:
id: str
agent: Agent
max_iterations: int
workdir: str
command_manager: CommandManager
callbacks: List[Callable]

def __init__(
self,
agent: Agent,
workdir: str,
id: str = '',
sid: str = "",
max_iterations: int = MAX_ITERATIONS,
container_image: str | None = None,
callbacks: List[Callable] = [],
):
self.id = id
self.id = sid
self.agent = agent
self.max_iterations = max_iterations
self.workdir = workdir
self.command_manager = CommandManager(
self.id, workdir, container_image)
self.command_manager = CommandManager(self.id, workdir, container_image)
self.callbacks = callbacks

def update_state_for_step(self, i):
Expand All @@ -96,9 +97,9 @@ def update_state_after_step(self):

def add_history(self, action: Action, observation: Observation):
if not isinstance(action, Action):
raise ValueError('action must be an instance of Action')
raise ValueError("action must be an instance of Action")
if not isinstance(observation, Observation):
raise ValueError('observation must be an instance of Observation')
raise ValueError("observation must be an instance of Observation")
self.state.history.append((action, observation))
self.state.updated_info.append((action, observation))

Expand All @@ -110,62 +111,64 @@ async def start_loop(self, task: str):
try:
finished = await self.step(i)
except Exception as e:
logger.error('Error in loop', exc_info=True)
logger.error("Error in loop", exc_info=True)
raise e
if finished:
break
if not finished:
logger.info('Exited before finishing the task.')
logger.info("Exited before finishing the task.")

async def step(self, i: int):
print('\n\n==============', flush=True)
print('STEP', i, flush=True)
print_with_color(self.state.plan.main_goal, 'PLAN')
print("\n\n==============", flush=True)
print("STEP", i, flush=True)
print_with_color(self.state.plan.main_goal, "PLAN")

log_obs = self.command_manager.get_background_obs()
for obs in log_obs:
self.add_history(NullAction(), obs)
await self._run_callbacks(obs)
print_with_color(obs, 'BACKGROUND LOG')
print_with_color(obs, "BACKGROUND LOG")

self.update_state_for_step(i)
action: Action = NullAction()
observation: Observation = NullObservation('')
observation: Observation = NullObservation("")
try:
action = self.agent.step(self.state)
if action is None:
raise ValueError('Agent must return an action')
print_with_color(action, 'ACTION')
raise ValueError("Agent must return an action")
print_with_color(action, "ACTION")
except Exception as e:
observation = AgentErrorObservation(str(e))
print_with_color(observation, 'ERROR')
print_with_color(observation, "ERROR")
traceback.print_exc()
# TODO Change to more robust error handling
if 'The api_key client option must be set' in observation.content or 'Incorrect API key provided:' in observation.content:
if (
"The api_key client option must be set" in observation.content
or "Incorrect API key provided:" in observation.content
):
raise
self.update_state_after_step()

await self._run_callbacks(action)

finished = isinstance(action, AgentFinishAction)
if finished:
print_with_color(action, 'INFO')
print_with_color(action, "INFO")
return True

if isinstance(action, AddTaskAction):
try:
self.state.plan.add_subtask(
action.parent, action.goal, action.subtasks)
self.state.plan.add_subtask(action.parent, action.goal, action.subtasks)
except Exception as e:
observation = AgentErrorObservation(str(e))
print_with_color(observation, 'ERROR')
print_with_color(observation, "ERROR")
traceback.print_exc()
elif isinstance(action, ModifyTaskAction):
try:
self.state.plan.set_subtask_state(action.id, action.state)
except Exception as e:
observation = AgentErrorObservation(str(e))
print_with_color(observation, 'ERROR')
print_with_color(observation, "ERROR")
traceback.print_exc()

if action.executable:
Expand All @@ -176,11 +179,11 @@ async def step(self, i: int):
observation = action.run(self)
except Exception as e:
observation = AgentErrorObservation(str(e))
print_with_color(observation, 'ERROR')
print_with_color(observation, "ERROR")
traceback.print_exc()

if not isinstance(observation, NullObservation):
print_with_color(observation, 'OBSERVATION')
print_with_color(observation, "OBSERVATION")

self.add_history(action, observation)
await self._run_callbacks(observation)
Expand All @@ -192,9 +195,8 @@ async def _run_callbacks(self, event):
idx = self.callbacks.index(callback)
try:
callback(event)
except Exception:
logger.exception('Callback error: %s', idx)
pass
except Exception as e:
logger.exception(f"Callback error: {e}, idx: {idx}")
await asyncio.sleep(
0.001
) # Give back control for a tick, so we can await in callbacks
22 changes: 13 additions & 9 deletions opendevin/controller/command_manager.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
from typing import List

from opendevin import config
from opendevin.observation import CmdOutputObservation
from opendevin.sandbox import DockerExecBox, DockerSSHBox, Sandbox
from opendevin import config
from opendevin.schema import ConfigType


class CommandManager:
id: str
directory: str
shell: Sandbox

def __init__(
self,
id: str,
dir: str,
container_image: str | None = None,
self,
sid: str,
directory: str,
container_image: str | None = None,
):
self.directory = dir
if config.get('SANDBOX_TYPE').lower() == 'exec':
self.directory = directory
if config.get(ConfigType.SANDBOX_TYPE).lower() == 'exec':
self.shell = DockerExecBox(
id=(id or 'default'), workspace_dir=dir, container_image=container_image
sid=(sid or 'default'), workspace_dir=directory, container_image=container_image
)
else:
self.shell = DockerSSHBox(
id=(id or 'default'), workspace_dir=dir, container_image=container_image
sid=(sid or 'default'), workspace_dir=directory, container_image=container_image
)

def run_command(self, command: str, background=False) -> CmdOutputObservation:
Expand Down
Loading

0 comments on commit 494a1b6

Please sign in to comment.