Преглед изворни кода

Traffic Control: Add new config MAX_CHARS (#1015)

* Add new config MAX_CHARS

* Fix mypy linting issues
Boxuan Li пре 1 година
родитељ
комит
e0c7492609

+ 28 - 21
agenthub/codeact_agent/codeact_agent.py

@@ -24,7 +24,7 @@ Apart from the standard bash commands, you can also use the following special co
 {COMMAND_DOCS}
 {COMMAND_DOCS}
 """
 """
     if COMMAND_DOCS is not None
     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.
 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.
 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}
 {COMMAND_SEGMENT}
 
 
 When you are done, execute the following to close the shell and end the conversation:
 When you are done, execute the following to close the shell and end the conversation:
-<execute>exit</execute> 
+<execute>exit</execute>
 """
 """
 
 
 INVALID_INPUT_MESSAGE = (
 INVALID_INPUT_MESSAGE = (
     "I don't understand your input. \n"
     "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:
 def parse_response(response) -> str:
     action = response.choices[0].message.content
     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
     return action
 
 
+
 class CodeActAgent(Agent):
 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.
     The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
     """
     """
-    
+
     def __init__(
     def __init__(
         self,
         self,
         llm: LLM,
         llm: LLM,
@@ -82,7 +84,7 @@ class CodeActAgent(Agent):
 
 
     def step(self, state: State) -> Action:
     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.
         This includes gathering info on previous steps and prompting the model to make a command to execute.
 
 
         Parameters:
         Parameters:
@@ -97,42 +99,47 @@ class CodeActAgent(Agent):
         """
         """
 
 
         if len(self.messages) == 0:
         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 = [
             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
         updated_info = state.updated_info
         if updated_info:
         if updated_info:
             for prev_action, obs in updated_info:
             for prev_action, obs in updated_info:
                 assert isinstance(
                 assert isinstance(
                     prev_action, (CmdRunAction, AgentEchoAction)
                     prev_action, (CmdRunAction, AgentEchoAction)
-                ), "Expecting CmdRunAction or AgentEchoAction for Action"
+                ), 'Expecting CmdRunAction or AgentEchoAction for Action'
                 if isinstance(
                 if isinstance(
                     obs, AgentMessageObservation
                     obs, AgentMessageObservation
                 ):  # warning message from itself
                 ):  # warning message from itself
-                    self.messages.append({"role": "user", "content": obs.content})
+                    self.messages.append(
+                        {'role': 'user', 'content': obs.content})
                 elif isinstance(obs, CmdOutputObservation):
                 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}]]"
                     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:
                 else:
                     raise NotImplementedError(
                     raise NotImplementedError(
                         f"Unknown observation type: {obs.__class__}"
                         f"Unknown observation type: {obs.__class__}"
                     )
                     )
         response = self.llm.completion(
         response = self.llm.completion(
             messages=self.messages,
             messages=self.messages,
-            stop=["</execute>"],
+            stop=['</execute>'],
             temperature=0.0
             temperature=0.0
         )
         )
         action_str: str = parse_response(response)
         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:
         if command is not None:
             # a command was found
             # a command was found
             command_group = command.group(1)
             command_group = command.group(1)
-            if command_group.strip() == "exit":
+            if command_group.strip() == 'exit':
                 return AgentFinishAction()
                 return AgentFinishAction()
             return CmdRunAction(command=command_group)
             return CmdRunAction(command=command_group)
             # # execute the code
             # # execute the code
@@ -149,4 +156,4 @@ class CodeActAgent(Agent):
             )  # warning message to itself
             )  # warning message to itself
 
 
     def search_memory(self, query: str) -> List[str]:
     def search_memory(self, query: str) -> List[str]:
-        raise NotImplementedError("Implement this abstract method")
+        raise NotImplementedError('Implement this abstract method')

+ 58 - 56
agenthub/monologue_agent/agent.py

@@ -32,46 +32,46 @@ MAX_MONOLOGUE_LENGTH = 20000
 MAX_OUTPUT_LENGTH = 5000
 MAX_OUTPUT_LENGTH = 5000
 
 
 INITIAL_THOUGHTS = [
 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",
     "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!",
     "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!",
     "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",
     "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!",
     "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"',
     '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",
     "WRITE echo \"console.log('hello world')\" > test.js",
-    "",
+    '',
     "I just created test.js. I'll try and run it now.",
     "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.",
     "I'm going to try reading it now using the read action.",
-    "READ test.js",
+    'READ test.js',
     "console.log('hello world')",
     "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...",
     "Let's try that...",
-    "BROWSE google.com",
+    'BROWSE google.com',
     '<form><input type="text"></input><button type="submit"></button></form>',
     '<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.",
     "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.",
     "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?",
     "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.",
     "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
         - 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 (
         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)
         self.monologue.add_event(event)
@@ -137,51 +137,52 @@ class MonologueAgent(Agent):
         if self._initialized:
         if self._initialized:
             return
             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.monologue = Monologue()
         self.memory = LongTermMemory()
         self.memory = LongTermMemory()
 
 
-        output_type = ""
+        output_type = ''
         for thought in INITIAL_THOUGHTS:
         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:
                 if output_type == ObservationType.RUN:
                     observation = CmdOutputObservation(
                     observation = CmdOutputObservation(
-                        content=thought, command_id=0, command=""
+                        content=thought, command_id=0, command=''
                     )
                     )
                 elif output_type == ObservationType.READ:
                 elif output_type == ObservationType.READ:
-                    observation = FileReadObservation(content=thought, path="")
+                    observation = FileReadObservation(content=thought, path='')
                 elif output_type == ObservationType.RECALL:
                 elif output_type == ObservationType.RECALL:
-                    observation = AgentRecallObservation(content=thought, memories=[])
+                    observation = AgentRecallObservation(
+                        content=thought, memories=[])
                 elif output_type == ObservationType.BROWSE:
                 elif output_type == ObservationType.BROWSE:
                     observation = BrowserOutputObservation(
                     observation = BrowserOutputObservation(
-                        content=thought, url="", screenshot=""
+                        content=thought, url='', screenshot=''
                     )
                     )
                 self._add_event(observation.to_dict())
                 self._add_event(observation.to_dict())
-                output_type = ""
+                output_type = ''
             else:
             else:
                 action: Action = NullAction()
                 action: Action = NullAction()
-                if thought.startswith("RUN"):
-                    command = thought.split("RUN ")[1]
+                if thought.startswith('RUN'):
+                    command = thought.split('RUN ')[1]
                     action = CmdRunAction(command)
                     action = CmdRunAction(command)
                     output_type = ActionType.RUN
                     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]
                     path = parts[1]
                     content = parts[0]
                     content = parts[0]
                     action = FileWriteAction(path=path, content=content)
                     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)
                     action = FileReadAction(path=path)
                     output_type = ActionType.READ
                     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)
                     action = AgentRecallAction(query=query)
                     output_type = ActionType.RECALL
                     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)
                     action = BrowseURLAction(url=url)
                     output_type = ActionType.BROWSE
                     output_type = ActionType.BROWSE
                 else:
                 else:
@@ -211,9 +212,10 @@ class MonologueAgent(Agent):
             self.monologue.get_thoughts(),
             self.monologue.get_thoughts(),
             state.background_commands_obs,
             state.background_commands_obs,
         )
         )
-        messages = [{"content": prompt, "role": "user"}]
+        messages = [{'content': prompt, 'role': 'user'}]
         resp = self.llm.completion(messages=messages)
         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)
         action = prompts.parse_action_response(action_resp)
         self.latest_action = action
         self.latest_action = action
         return action
         return action

+ 4 - 3
agenthub/planner_agent/agent.py

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

+ 1 - 0
frontend/src/types/ConfigType.tsx

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

+ 5 - 1
opendevin/config.py

@@ -22,6 +22,10 @@ DEFAULT_CONFIG: dict = {
     ConfigType.LLM_COOLDOWN_TIME: 1,
     ConfigType.LLM_COOLDOWN_TIME: 1,
     ConfigType.DIRECTORY_REWRITE: '',
     ConfigType.DIRECTORY_REWRITE: '',
     ConfigType.MAX_ITERATIONS: 100,
     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.AGENT: 'MonologueAgent',
     ConfigType.SANDBOX_TYPE: 'ssh',
     ConfigType.SANDBOX_TYPE: 'ssh',
     ConfigType.DISABLE_COLOR: 'false',
     ConfigType.DISABLE_COLOR: 'false',
@@ -47,7 +51,7 @@ def get(key: str, required: bool = False):
     """
     """
     value = os.environ.get(key)
     value = os.environ.get(key)
     if not value:
     if not value:
-      value = config.get(key)
+        value = config.get(key)
     if not value and required:
     if not value and required:
         raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.")
         raise KeyError(f"Please set '{key}' in `config.toml` or `.env`.")
     return value
     return value

+ 56 - 46
opendevin/controller/agent_controller.py

@@ -15,45 +15,47 @@ from opendevin.action import (
 )
 )
 from opendevin.agent import Agent
 from opendevin.agent import Agent
 from opendevin.logger import opendevin_logger as logger
 from opendevin.logger import opendevin_logger as logger
+from opendevin.exceptions import MaxCharsExceedError
 from opendevin.observation import Observation, AgentErrorObservation, NullObservation
 from opendevin.observation import Observation, AgentErrorObservation, NullObservation
 from opendevin.plan import Plan
 from opendevin.plan import Plan
 from opendevin.state import State
 from opendevin.state import State
 from .command_manager import CommandManager
 from .command_manager import CommandManager
 
 
 ColorType = Literal[
 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 = (
 DISABLE_COLOR_PRINTING = (
     config.get('DISABLE_COLOR').lower() == 'true'
     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] = {
     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:
     if DISABLE_COLOR_PRINTING:
         print(f'\n{print_type.upper()}:\n{str(text)}', flush=True)
         print(f'\n{print_type.upper()}:\n{str(text)}', flush=True)
     else:
     else:
@@ -76,16 +78,19 @@ class AgentController:
         self,
         self,
         agent: Agent,
         agent: Agent,
         workdir: str,
         workdir: str,
-        sid: str = "",
+        sid: str = '',
         max_iterations: int = MAX_ITERATIONS,
         max_iterations: int = MAX_ITERATIONS,
+        max_chars: int = MAX_CHARS,
         container_image: str | None = None,
         container_image: str | None = None,
         callbacks: List[Callable] = [],
         callbacks: List[Callable] = [],
     ):
     ):
         self.id = sid
         self.id = sid
         self.agent = agent
         self.agent = agent
         self.max_iterations = max_iterations
         self.max_iterations = max_iterations
+        self.max_chars = max_chars
         self.workdir = workdir
         self.workdir = workdir
-        self.command_manager = CommandManager(self.id, workdir, container_image)
+        self.command_manager = CommandManager(
+            self.id, workdir, container_image)
         self.callbacks = callbacks
         self.callbacks = callbacks
 
 
     def update_state_for_step(self, i):
     def update_state_for_step(self, i):
@@ -97,9 +102,9 @@ class AgentController:
 
 
     def add_history(self, action: Action, observation: Observation):
     def add_history(self, action: Action, observation: Observation):
         if not isinstance(action, Action):
         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):
         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.history.append((action, observation))
         self.state.updated_info.append((action, observation))
         self.state.updated_info.append((action, observation))
 
 
@@ -111,40 +116,44 @@ class AgentController:
             try:
             try:
                 finished = await self.step(i)
                 finished = await self.step(i)
             except Exception as e:
             except Exception as e:
-                logger.error("Error in loop", exc_info=True)
+                logger.error('Error in loop', exc_info=True)
                 raise e
                 raise e
             if finished:
             if finished:
                 break
                 break
         if not finished:
         if not finished:
-            logger.info("Exited before finishing the task.")
+            logger.info('Exited before finishing the task.')
 
 
     async def step(self, i: int):
     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()
         log_obs = self.command_manager.get_background_obs()
         for obs in log_obs:
         for obs in log_obs:
             self.add_history(NullAction(), obs)
             self.add_history(NullAction(), obs)
             await self._run_callbacks(obs)
             await self._run_callbacks(obs)
-            print_with_color(obs, "BACKGROUND LOG")
+            print_with_color(obs, 'BACKGROUND LOG')
 
 
         self.update_state_for_step(i)
         self.update_state_for_step(i)
         action: Action = NullAction()
         action: Action = NullAction()
-        observation: Observation = NullObservation("")
+        observation: Observation = NullObservation('')
         try:
         try:
             action = self.agent.step(self.state)
             action = self.agent.step(self.state)
             if action is None:
             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:
         except Exception as e:
             observation = AgentErrorObservation(str(e))
             observation = AgentErrorObservation(str(e))
-            print_with_color(observation, "ERROR")
+            print_with_color(observation, 'ERROR')
             traceback.print_exc()
             traceback.print_exc()
             # TODO Change to more robust error handling
             # TODO Change to more robust error handling
             if (
             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
                 raise
         self.update_state_after_step()
         self.update_state_after_step()
@@ -153,22 +162,23 @@ class AgentController:
 
 
         finished = isinstance(action, AgentFinishAction)
         finished = isinstance(action, AgentFinishAction)
         if finished:
         if finished:
-            print_with_color(action, "INFO")
+            print_with_color(action, 'INFO')
             return True
             return True
 
 
         if isinstance(action, AddTaskAction):
         if isinstance(action, AddTaskAction):
             try:
             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:
             except Exception as e:
                 observation = AgentErrorObservation(str(e))
                 observation = AgentErrorObservation(str(e))
-                print_with_color(observation, "ERROR")
+                print_with_color(observation, 'ERROR')
                 traceback.print_exc()
                 traceback.print_exc()
         elif isinstance(action, ModifyTaskAction):
         elif isinstance(action, ModifyTaskAction):
             try:
             try:
                 self.state.plan.set_subtask_state(action.id, action.state)
                 self.state.plan.set_subtask_state(action.id, action.state)
             except Exception as e:
             except Exception as e:
                 observation = AgentErrorObservation(str(e))
                 observation = AgentErrorObservation(str(e))
-                print_with_color(observation, "ERROR")
+                print_with_color(observation, 'ERROR')
                 traceback.print_exc()
                 traceback.print_exc()
 
 
         if action.executable:
         if action.executable:
@@ -179,11 +189,11 @@ class AgentController:
                     observation = action.run(self)
                     observation = action.run(self)
             except Exception as e:
             except Exception as e:
                 observation = AgentErrorObservation(str(e))
                 observation = AgentErrorObservation(str(e))
-                print_with_color(observation, "ERROR")
+                print_with_color(observation, 'ERROR')
                 traceback.print_exc()
                 traceback.print_exc()
 
 
         if not isinstance(observation, NullObservation):
         if not isinstance(observation, NullObservation):
-            print_with_color(observation, "OBSERVATION")
+            print_with_color(observation, 'OBSERVATION')
 
 
         self.add_history(action, observation)
         self.add_history(action, observation)
         await self._run_callbacks(observation)
         await self._run_callbacks(observation)

+ 9 - 0
opendevin/exceptions.py

@@ -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)

+ 13 - 3
opendevin/main.py

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

+ 1 - 0
opendevin/schema/config.py

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

+ 2 - 0
opendevin/server/agent/agent.py

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

+ 7 - 2
opendevin/state.py

@@ -11,10 +11,15 @@ from opendevin.observation import (
     CmdOutputObservation,
     CmdOutputObservation,
 )
 )
 
 
+
 @dataclass
 @dataclass
 class State:
 class State:
     plan: Plan
     plan: Plan
     iteration: int = 0
     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)
     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)