Traffic Control: Add new config MAX_CHARS (#1015)

* Add new config MAX_CHARS

* Fix mypy linting issues
This commit is contained in:
Boxuan Li
2024-04-12 12:01:52 -07:00
committed by GitHub
parent 5d5106c510
commit e0c7492609
11 changed files with 184 additions and 132 deletions

View File

@@ -24,7 +24,7 @@ Apart from the standard bash commands, you can also use the following special co
{COMMAND_DOCS}
"""
if COMMAND_DOCS is not None
else ""
else ''
)
SYSTEM_MESSAGE = f"""You are a helpful assistant. You will be provided access (as root) to a bash shell to complete user-provided tasks.
You will be able to execute commands in the bash shell, interact with the file system, install packages, and receive the output of your commands.
@@ -46,27 +46,29 @@ print(math.pi)" > math.py
{COMMAND_SEGMENT}
When you are done, execute the following to close the shell and end the conversation:
<execute>exit</execute>
<execute>exit</execute>
"""
INVALID_INPUT_MESSAGE = (
"I don't understand your input. \n"
"If you want to execute command, please use <execute> YOUR_COMMAND_HERE </execute>.\n"
"If you already completed the task, please exit the shell by generating: <execute> exit </execute>."
'If you want to execute command, please use <execute> YOUR_COMMAND_HERE </execute>.\n'
'If you already completed the task, please exit the shell by generating: <execute> exit </execute>.'
)
def parse_response(response) -> str:
action = response.choices[0].message.content
if "<execute>" in action and "</execute>" not in action:
action += "</execute>"
if '<execute>' in action and '</execute>' not in action:
action += '</execute>'
return action
class CodeActAgent(Agent):
"""
The Code Act Agent is a minimalist agent.
The Code Act Agent is a minimalist agent.
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
"""
def __init__(
self,
llm: LLM,
@@ -82,7 +84,7 @@ class CodeActAgent(Agent):
def step(self, state: State) -> Action:
"""
Performs one step using the Code Act Agent.
Performs one step using the Code Act Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
Parameters:
@@ -97,42 +99,47 @@ class CodeActAgent(Agent):
"""
if len(self.messages) == 0:
assert state.plan.main_goal, "Expecting instruction to be set"
assert state.plan.main_goal, 'Expecting instruction to be set'
self.messages = [
{"role": "system", "content": SYSTEM_MESSAGE},
{"role": "user", "content": state.plan.main_goal},
{'role': 'system', 'content': SYSTEM_MESSAGE},
{'role': 'user', 'content': state.plan.main_goal},
]
updated_info = state.updated_info
if updated_info:
for prev_action, obs in updated_info:
assert isinstance(
prev_action, (CmdRunAction, AgentEchoAction)
), "Expecting CmdRunAction or AgentEchoAction for Action"
), 'Expecting CmdRunAction or AgentEchoAction for Action'
if isinstance(
obs, AgentMessageObservation
): # warning message from itself
self.messages.append({"role": "user", "content": obs.content})
self.messages.append(
{'role': 'user', 'content': obs.content})
elif isinstance(obs, CmdOutputObservation):
content = "OBSERVATION:\n" + obs.content
content = 'OBSERVATION:\n' + obs.content
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
content += f"\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]"
self.messages.append({"role": "user", "content": content})
self.messages.append({'role': 'user', 'content': content})
else:
raise NotImplementedError(
f"Unknown observation type: {obs.__class__}"
)
response = self.llm.completion(
messages=self.messages,
stop=["</execute>"],
stop=['</execute>'],
temperature=0.0
)
action_str: str = parse_response(response)
self.messages.append({"role": "assistant", "content": action_str})
state.num_of_chars += sum(len(message['content'])
for message in self.messages) + len(action_str)
self.messages.append({'role': 'assistant', 'content': action_str})
command = re.search(r"<execute>(.*)</execute>", action_str, re.DOTALL)
command = re.search(r'<execute>(.*)</execute>', action_str, re.DOTALL)
if command is not None:
# a command was found
command_group = command.group(1)
if command_group.strip() == "exit":
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command=command_group)
# # execute the code
@@ -149,4 +156,4 @@ class CodeActAgent(Agent):
) # warning message to itself
def search_memory(self, query: str) -> List[str]:
raise NotImplementedError("Implement this abstract method")
raise NotImplementedError('Implement this abstract method')

View File

@@ -32,46 +32,46 @@ MAX_MONOLOGUE_LENGTH = 20000
MAX_OUTPUT_LENGTH = 5000
INITIAL_THOUGHTS = [
"I exist!",
"Hmm...looks like I can type in a command line prompt",
"Looks like I have a web browser too!",
'I exist!',
'Hmm...looks like I can type in a command line prompt',
'Looks like I have a web browser too!',
"Here's what I want to do: $TASK",
"How am I going to get there though?",
"It seems like I have some kind of short term memory.",
"Each of my thoughts seems to be stored in a JSON array.",
"It seems whatever I say next will be added as an object to the list.",
"But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.",
"Fortunately I have long term memory!",
"I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!",
'How am I going to get there though?',
'It seems like I have some kind of short term memory.',
'Each of my thoughts seems to be stored in a JSON array.',
'It seems whatever I say next will be added as an object to the list.',
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
'Fortunately I have long term memory!',
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
"Let's try it out!",
"RECALL what it is I want to do",
'RECALL what it is I want to do',
"Here's what I want to do: $TASK",
"How am I going to get there though?",
'How am I going to get there though?',
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
'RUN echo "hello world"',
"hello world",
"Cool! I bet I can write files too using the write action.",
'hello world',
'Cool! I bet I can write files too using the write action.',
"WRITE echo \"console.log('hello world')\" > test.js",
"",
'',
"I just created test.js. I'll try and run it now.",
"RUN node test.js",
"hello world",
"It works!",
'RUN node test.js',
'hello world',
'It works!',
"I'm going to try reading it now using the read action.",
"READ test.js",
'READ test.js',
"console.log('hello world')",
"Nice! I can read files too!",
"And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument",
'Nice! I can read files too!',
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
"Let's try that...",
"BROWSE google.com",
'BROWSE google.com',
'<form><input type="text"></input><button type="submit"></button></form>',
"I can browse the web too!",
"And once I have completed my task, I can use the finish action to stop working.",
'I can browse the web too!',
'And once I have completed my task, I can use the finish action to stop working.',
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
"Very cool. Now to accomplish my task.",
'Very cool. Now to accomplish my task.',
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
"In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.",
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
"It seems like there might be an existing project here. I should probably start by running `ls` to see what's here.",
]
@@ -106,15 +106,15 @@ class MonologueAgent(Agent):
- event (dict): The event that will be added to monologue and memory
"""
if "extras" in event and "screenshot" in event["extras"]:
del event["extras"]["screenshot"]
if 'extras' in event and 'screenshot' in event['extras']:
del event['extras']['screenshot']
if (
"args" in event
and "output" in event["args"]
and len(event["args"]["output"]) > MAX_OUTPUT_LENGTH
'args' in event
and 'output' in event['args']
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
):
event["args"]["output"] = (
event["args"]["output"][:MAX_OUTPUT_LENGTH] + "..."
event['args']['output'] = (
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
)
self.monologue.add_event(event)
@@ -137,51 +137,52 @@ class MonologueAgent(Agent):
if self._initialized:
return
if task is None or task == "":
raise ValueError("Instruction must be provided")
if task is None or task == '':
raise ValueError('Instruction must be provided')
self.monologue = Monologue()
self.memory = LongTermMemory()
output_type = ""
output_type = ''
for thought in INITIAL_THOUGHTS:
thought = thought.replace("$TASK", task)
if output_type != "":
observation: Observation = NullObservation(content="")
thought = thought.replace('$TASK', task)
if output_type != '':
observation: Observation = NullObservation(content='')
if output_type == ObservationType.RUN:
observation = CmdOutputObservation(
content=thought, command_id=0, command=""
content=thought, command_id=0, command=''
)
elif output_type == ObservationType.READ:
observation = FileReadObservation(content=thought, path="")
observation = FileReadObservation(content=thought, path='')
elif output_type == ObservationType.RECALL:
observation = AgentRecallObservation(content=thought, memories=[])
observation = AgentRecallObservation(
content=thought, memories=[])
elif output_type == ObservationType.BROWSE:
observation = BrowserOutputObservation(
content=thought, url="", screenshot=""
content=thought, url='', screenshot=''
)
self._add_event(observation.to_dict())
output_type = ""
output_type = ''
else:
action: Action = NullAction()
if thought.startswith("RUN"):
command = thought.split("RUN ")[1]
if thought.startswith('RUN'):
command = thought.split('RUN ')[1]
action = CmdRunAction(command)
output_type = ActionType.RUN
elif thought.startswith("WRITE"):
parts = thought.split("WRITE ")[1].split(" > ")
elif thought.startswith('WRITE'):
parts = thought.split('WRITE ')[1].split(' > ')
path = parts[1]
content = parts[0]
action = FileWriteAction(path=path, content=content)
elif thought.startswith("READ"):
path = thought.split("READ ")[1]
elif thought.startswith('READ'):
path = thought.split('READ ')[1]
action = FileReadAction(path=path)
output_type = ActionType.READ
elif thought.startswith("RECALL"):
query = thought.split("RECALL ")[1]
elif thought.startswith('RECALL'):
query = thought.split('RECALL ')[1]
action = AgentRecallAction(query=query)
output_type = ActionType.RECALL
elif thought.startswith("BROWSE"):
url = thought.split("BROWSE ")[1]
elif thought.startswith('BROWSE'):
url = thought.split('BROWSE ')[1]
action = BrowseURLAction(url=url)
output_type = ActionType.BROWSE
else:
@@ -211,9 +212,10 @@ class MonologueAgent(Agent):
self.monologue.get_thoughts(),
state.background_commands_obs,
)
messages = [{"content": prompt, "role": "user"}]
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
action_resp = resp["choices"][0]["message"]["content"]
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = prompts.parse_action_response(action_resp)
self.latest_action = action
return action

View File

@@ -7,6 +7,7 @@ from opendevin.llm.llm import LLM
from opendevin.state import State
from opendevin.action import Action
class PlannerAgent(Agent):
"""
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
@@ -24,7 +25,7 @@ class PlannerAgent(Agent):
def step(self, state: State) -> Action:
"""
Checks to see if current step is completed, returns AgentFinishAction if True.
Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
Parameters:
@@ -38,12 +39,12 @@ class PlannerAgent(Agent):
if state.plan.task.state in ['completed', 'verified', 'abandoned']:
return AgentFinishAction()
prompt = get_prompt(state.plan, state.history)
messages = [{"content": prompt, "role": "user"}]
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = parse_response(action_resp)
return action
def search_memory(self, query: str) -> List[str]:
return []

View File

@@ -10,6 +10,7 @@ enum ArgConfigType {
LLM_COOLDOWN_TIME = "LLM_COOLDOWN_TIME",
DIRECTORY_REWRITE = "DIRECTORY_REWRITE",
MAX_ITERATIONS = "MAX_ITERATIONS",
MAX_CHARS = "MAX_CHARS",
AGENT = "AGENT",
LANGUAGE = "LANGUAGE",

View File

@@ -22,6 +22,10 @@ DEFAULT_CONFIG: dict = {
ConfigType.LLM_COOLDOWN_TIME: 1,
ConfigType.DIRECTORY_REWRITE: '',
ConfigType.MAX_ITERATIONS: 100,
# GPT-4 pricing is $10 per 1M input tokens. Since tokenization happens on LLM side,
# we cannot easily count number of tokens, but we can count characters.
# Assuming 5 characters per token, 5 million is a reasonable default limit.
ConfigType.MAX_CHARS: 5_000_000,
ConfigType.AGENT: 'MonologueAgent',
ConfigType.SANDBOX_TYPE: 'ssh',
ConfigType.DISABLE_COLOR: 'false',
@@ -47,7 +51,7 @@ def get(key: str, required: bool = False):
"""
value = os.environ.get(key)
if not value:
value = config.get(key)
value = config.get(key)
if not value and required:
raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.")
return value

View File

@@ -15,45 +15,47 @@ from opendevin.action import (
)
from opendevin.agent import Agent
from opendevin.logger import opendevin_logger as logger
from opendevin.exceptions import MaxCharsExceedError
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('DISABLE_COLOR').lower() == 'true'
)
MAX_ITERATIONS = config.get("MAX_ITERATIONS")
MAX_ITERATIONS = config.get('MAX_ITERATIONS')
MAX_CHARS = config.get('MAX_CHARS')
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:
@@ -76,16 +78,19 @@ class AgentController:
self,
agent: Agent,
workdir: str,
sid: str = "",
sid: str = '',
max_iterations: int = MAX_ITERATIONS,
max_chars: int = MAX_CHARS,
container_image: str | None = None,
callbacks: List[Callable] = [],
):
self.id = sid
self.agent = agent
self.max_iterations = max_iterations
self.max_chars = max_chars
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):
@@ -97,9 +102,9 @@ class AgentController:
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))
@@ -111,40 +116,44 @@ class AgentController:
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')
if self.state.num_of_chars > self.max_chars:
raise MaxCharsExceedError(
self.state.num_of_chars, self.max_chars)
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
'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()
@@ -153,22 +162,23 @@ class AgentController:
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:
@@ -179,11 +189,11 @@ class AgentController:
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)

9
opendevin/exceptions.py Normal file
View File

@@ -0,0 +1,9 @@
class MaxCharsExceedError(Exception):
def __init__(self, num_of_chars=None, max_chars_limit=None):
if num_of_chars is not None and max_chars_limit is not None:
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
message = f"Number of characters {num_of_chars} exceeds MAX_CHARS limit: {max_chars_limit}"
else:
message = 'Number of characters exceeds MAX_CHARS limit'
super().__init__(message)

View File

@@ -5,6 +5,7 @@ from typing import Type
import agenthub # noqa F401 (we import this to get the agents registered)
from opendevin import config
from opendevin.schema import ConfigType
from opendevin.agent import Agent
from opendevin.controller import AgentController
from opendevin.llm.llm import LLM
@@ -51,17 +52,24 @@ def parse_arguments():
parser.add_argument(
'-m',
'--model-name',
default=config.get('LLM_MODEL'),
default=config.get(ConfigType.LLM_MODEL),
type=str,
help='The (litellm) model name to use',
)
parser.add_argument(
'-i',
'--max-iterations',
default=100,
default=config.get(ConfigType.MAX_ITERATIONS),
type=int,
help='The maximum number of iterations to run the agent',
)
parser.add_argument(
'-n',
'--max-chars',
default=config.get(ConfigType.MAX_CHARS),
type=int,
help='The maximum number of characters to send to and receive from LLM per task',
)
return parser.parse_args()
@@ -81,6 +89,8 @@ async def main():
raise ValueError(
'No task provided. Please specify a task through -t, -f.')
# FIXME: autopep8 and mypy are fighting each other on this line
# autopep8: off
print(
f'Running agent {args.agent_cls} (model: {args.model_name}, directory: {args.directory}) with task: "{task}"'
)
@@ -88,7 +98,7 @@ async def main():
AgentCls: Type[Agent] = Agent.get_cls(args.agent_cls)
agent = AgentCls(llm=llm)
controller = AgentController(
agent=agent, workdir=args.directory, max_iterations=args.max_iterations
agent=agent, workdir=args.directory, max_iterations=args.max_iterations, max_chars=args.max_chars
)
await controller.start_loop(task)

View File

@@ -15,6 +15,7 @@ class ConfigType(str, Enum):
LLM_COOLDOWN_TIME = 'LLM_COOLDOWN_TIME'
DIRECTORY_REWRITE = 'DIRECTORY_REWRITE'
MAX_ITERATIONS = 'MAX_ITERATIONS'
MAX_CHARS = 'MAX_CHARS'
AGENT = 'AGENT'
SANDBOX_TYPE = 'SANDBOX_TYPE'
DISABLE_COLOR = 'DISABLE_COLOR'

View File

@@ -113,6 +113,7 @@ class AgentUnit:
container_image = config.get(ConfigType.SANDBOX_CONTAINER_IMAGE)
max_iterations = self.get_arg_or_default(
args, ConfigType.MAX_ITERATIONS)
max_chars = self.get_arg_or_default(args, ConfigType.MAX_CHARS)
if not os.path.exists(directory):
logger.info(
@@ -127,6 +128,7 @@ class AgentUnit:
agent=Agent.get_cls(agent_cls)(llm),
workdir=directory,
max_iterations=int(max_iterations),
max_chars=int(max_chars),
container_image=container_image,
callbacks=[self.on_agent_event],
)

View File

@@ -11,10 +11,15 @@ from opendevin.observation import (
CmdOutputObservation,
)
@dataclass
class State:
plan: Plan
iteration: int = 0
background_commands_obs: List[CmdOutputObservation] = field(default_factory=list)
# number of characters we have sent to and received from LLM so far for current task
num_of_chars: int = 0
background_commands_obs: List[CmdOutputObservation] = field(
default_factory=list)
history: List[Tuple[Action, Observation]] = field(default_factory=list)
updated_info: List[Tuple[Action, Observation]] = field(default_factory=list)
updated_info: List[Tuple[Action, Observation]
] = field(default_factory=list)