Просмотр исходного кода

fix(backend) changes to improve Command-R+ behavior, plus file i/o error improvements, attempt 2 (#1417)

* Some improvements to prompts, some better exception handling for various file IO errors, added timeout and max return token configurations for the LLM api.

* More monologue prompt improvements

* Dynamically set username provided in prompt.

* Remove absolute paths from llm prompts, fetch working directory from sandbox when resolving paths in fileio operations, add customizable timeout for bash commands, mention said timeout in llm prompt.

* Switched ssh_box to disabling tty echo and removed the logic attempting to delete it from the response afterwards, fixed get_working_directory for ssh_box.

* Update prompts in integration tests to match monologue agent changes.

* Minor tweaks to make merge easier.

* Another minor prompt tweak, better invalid json handling.

* Fix lint error

* More catch-up to fix lint errors introduced by merge.

* Force WORKSPACE_MOUNT_PATH_IN_SANDBOX to match WORKSPACE_MOUNT_PATH in local sandbox mode, combine exception handlers in prompts.py.

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Christian Balcom 1 год назад
Родитель
Сommit
24b71927c3

+ 1 - 1
agenthub/monologue_agent/agent.py

@@ -82,7 +82,7 @@ INITIAL_THOUGHTS = [
     "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.',
     "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 `pwd` and `ls` to orient myself.',
 ]
 
 

+ 14 - 6
agenthub/monologue_agent/utils/prompts.py

@@ -28,8 +28,8 @@ This is your internal monologue, in JSON format:
 
 
 Your most recent thought is at the bottom of that monologue. Continue your train of thought.
-What is your next thought or action? Your response must be in JSON format.
-It must be an object, and it must contain two fields:
+What is your next single thought or action? Your response must be in JSON format.
+It must be a single object, and it must contain two fields:
 * `action`, which is one of the actions below
 * `args`, which is a map of key-value pairs, specifying the arguments for that action
 
@@ -63,11 +63,15 @@ You should never act twice in a row without thinking. But if your last several
 actions are all "think" actions, you should consider taking a different action.
 
 Notes:
-* your environment is Debian Linux. You can install software with `apt`
-* your working directory will not change, even if you run `cd`. All commands will be run in the `%(WORKSPACE_MOUNT_PATH_IN_SANDBOX)s` directory.
+* you are logged in as %(user)s, but sudo will always work without a password.
+* all non-background commands will be forcibly stopped if they remain running for over %(timeout)s seconds.
+* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
 * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
+* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
+* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
+* whenever an action fails, always `think` about why it may have happened before acting again.
 
-What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.
 
 %(hint)s
 """
@@ -146,11 +150,15 @@ def get_request_action_prompt(
             )
         bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
 
+    user = 'opendevin' if config.get(ConfigType.RUN_AS_DEVIN) else 'root'
+
     return ACTION_PROMPT % {
         'task': task,
         'monologue': json.dumps(thoughts, indent=2),
         'background_commands': bg_commands_message,
         'hint': hint,
+        'user': user,
+        'timeout': config.get(ConfigType.SANDBOX_TIMEOUT),
         'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX),
     }
 
@@ -181,7 +189,7 @@ def parse_action_response(response: str) -> Action:
             raise LLMOutputError(
                 'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
             )
-    except ValueError:
+    except (ValueError, TypeError):
         raise LLMOutputError(
             'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
         )

+ 12 - 8
opendevin/action/fileop.py

@@ -17,20 +17,24 @@ from opendevin.schema.config import ConfigType
 
 from .base import ExecutableAction
 
-SANDBOX_PATH_PREFIX = '/workspace/'
 
+def resolve_path(file_path, working_directory):
+    path_in_sandbox = Path(file_path)
+
+    # Apply working directory
+    if not path_in_sandbox.is_absolute():
+        path_in_sandbox = Path(working_directory) / path_in_sandbox
 
-def resolve_path(file_path):
     # Sanitize the path with respect to the root of the full sandbox
-    # (deny any .. path traversal to parent directories of this)
-    abs_path_in_sandbox = (Path(SANDBOX_PATH_PREFIX) / Path(file_path)).resolve()
+    # (deny any .. path traversal to parent directories of the sandbox)
+    abs_path_in_sandbox = path_in_sandbox.resolve()
 
     # If the path is outside the workspace, deny it
-    if not abs_path_in_sandbox.is_relative_to(SANDBOX_PATH_PREFIX):
+    if not abs_path_in_sandbox.is_relative_to(config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)):
         raise PermissionError(f'File access not permitted: {file_path}')
 
     # Get path relative to the root of the workspace inside the sandbox
-    path_in_workspace = abs_path_in_sandbox.relative_to(Path(SANDBOX_PATH_PREFIX))
+    path_in_workspace = abs_path_in_sandbox.relative_to(Path(config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX)))
 
     # Get path relative to host
     path_in_host_workspace = Path(config.get(ConfigType.WORKSPACE_BASE)) / path_in_workspace
@@ -71,7 +75,7 @@ class FileReadAction(ExecutableAction):
             code_view = ''.join(read_lines)
         else:
             try:
-                whole_path = resolve_path(self.path)
+                whole_path = resolve_path(self.path, controller.action_manager.sandbox.get_working_directory())
                 self.start = max(self.start, 0)
                 try:
                     with open(whole_path, 'r', encoding='utf-8') as file:
@@ -123,7 +127,7 @@ class FileWriteAction(ExecutableAction):
                 return AgentErrorObservation(f'File not found: {self.path}')
         else:
             try:
-                whole_path = resolve_path(self.path)
+                whole_path = resolve_path(self.path, controller.action_manager.sandbox.get_working_directory())
                 mode = 'w' if not os.path.exists(whole_path) else 'r+'
                 try:
                     with open(whole_path, mode, encoding='utf-8') as file:

+ 7 - 1
opendevin/config.py

@@ -38,6 +38,8 @@ DEFAULT_CONFIG: dict = {
     ConfigType.MAX_ITERATIONS: 100,
     ConfigType.AGENT_MEMORY_MAX_THREADS: 2,
     ConfigType.AGENT_MEMORY_ENABLED: False,
+    ConfigType.LLM_TIMEOUT: None,
+    ConfigType.LLM_MAX_RETURN_TOKENS: None,
     # 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.
@@ -48,7 +50,8 @@ DEFAULT_CONFIG: dict = {
     ConfigType.USE_HOST_NETWORK: 'false',
     ConfigType.SSH_HOSTNAME: 'localhost',
     ConfigType.DISABLE_COLOR: 'false',
-    ConfigType.GITHUB_TOKEN: None,
+    ConfigType.SANDBOX_TIMEOUT: 120,
+    ConfigType.GITHUB_TOKEN: None
 }
 
 config_str = ''
@@ -76,6 +79,9 @@ for k, v in config.items():
     if k in [ConfigType.LLM_NUM_RETRIES, ConfigType.LLM_RETRY_MIN_WAIT, ConfigType.LLM_RETRY_MAX_WAIT]:
         config[k] = int_value(config[k], v, config_key=k)
 
+# In local there is no sandbox, the workspace will have the same pwd as the host
+if config[ConfigType.SANDBOX_TYPE] == 'local':
+    config[ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX] = config[ConfigType.WORKSPACE_MOUNT_PATH]
 
 def get_parser():
     parser = argparse.ArgumentParser(

+ 9 - 3
opendevin/controller/action_manager.py

@@ -26,15 +26,21 @@ class ActionManager:
         if sandbox_type == 'exec':
             self.sandbox = DockerExecBox(
                 sid=(sid or 'default'),
+                timeout=config.get(ConfigType.SANDBOX_TIMEOUT)
             )
         elif sandbox_type == 'local':
-            self.sandbox = LocalBox()
+            self.sandbox = LocalBox(
+                timeout=config.get(ConfigType.SANDBOX_TIMEOUT)
+            )
         elif sandbox_type == 'ssh':
             self.sandbox = DockerSSHBox(
-                sid=(sid or 'default')
+                sid=(sid or 'default'),
+                timeout=config.get(ConfigType.SANDBOX_TIMEOUT)
             )
         elif sandbox_type == 'e2b':
-            self.sandbox = E2BBox()
+            self.sandbox = E2BBox(
+                timeout=config.get(ConfigType.SANDBOX_TIMEOUT)
+            )
         else:
             raise ValueError(f'Invalid sandbox type: {sandbox_type}')
 

+ 10 - 3
opendevin/llm/llm.py

@@ -4,10 +4,9 @@ from litellm.exceptions import APIConnectionError, RateLimitError, ServiceUnavai
 from functools import partial
 
 from opendevin import config
-from opendevin.schema.config import ConfigType
 from opendevin.logger import llm_prompt_logger, llm_response_logger
 from opendevin.logger import opendevin_logger as logger
-
+from opendevin.schema import ConfigType
 
 DEFAULT_API_KEY = config.get(ConfigType.LLM_API_KEY)
 DEFAULT_BASE_URL = config.get(ConfigType.LLM_BASE_URL)
@@ -16,6 +15,8 @@ DEFAULT_API_VERSION = config.get(ConfigType.LLM_API_VERSION)
 LLM_NUM_RETRIES = config.get(ConfigType.LLM_NUM_RETRIES)
 LLM_RETRY_MIN_WAIT = config.get(ConfigType.LLM_RETRY_MIN_WAIT)
 LLM_RETRY_MAX_WAIT = config.get(ConfigType.LLM_RETRY_MAX_WAIT)
+LLM_TIMEOUT = config.get(ConfigType.LLM_TIMEOUT)
+LLM_MAX_RETURN_TOKENS = config.get(ConfigType.LLM_MAX_RETURN_TOKENS)
 
 
 class LLM:
@@ -31,6 +32,8 @@ class LLM:
                  num_retries=LLM_NUM_RETRIES,
                  retry_min_wait=LLM_RETRY_MIN_WAIT,
                  retry_max_wait=LLM_RETRY_MAX_WAIT,
+                 llm_timeout=LLM_TIMEOUT,
+                 llm_max_return_tokens=LLM_MAX_RETURN_TOKENS
                  ):
         """
         Args:
@@ -41,6 +44,8 @@ class LLM:
             num_retries (int, optional): The number of retries for API calls. Defaults to LLM_NUM_RETRIES.
             retry_min_wait (int, optional): The minimum time to wait between retries in seconds. Defaults to LLM_RETRY_MIN_TIME.
             retry_max_wait (int, optional): The maximum time to wait between retries in seconds. Defaults to LLM_RETRY_MAX_TIME.
+            llm_timeout (int, optional): The maximum time to wait for a response in seconds. Defaults to LLM_TIMEOUT.
+            llm_max_return_tokens (int, optional): The maximum number of tokens to return. Defaults to LLM_MAX_RETURN_TOKENS.
 
         Attributes:
             model_name (str): The name of the language model.
@@ -54,9 +59,11 @@ class LLM:
         self.api_key = api_key
         self.base_url = base_url
         self.api_version = api_version
+        self.llm_timeout = llm_timeout
+        self.llm_max_return_tokens = llm_max_return_tokens
 
         self._completion = partial(
-            litellm_completion, model=self.model_name, api_key=self.api_key, base_url=self.base_url, api_version=self.api_version)
+            litellm_completion, model=self.model_name, api_key=self.api_key, base_url=self.base_url, api_version=self.api_version, max_tokens=self.llm_max_return_tokens, timeout=self.llm_timeout)
 
         completion_unwrapped = self._completion
 

+ 3 - 0
opendevin/sandbox/docker/exec_box.py

@@ -268,6 +268,9 @@ class DockerExecBox(Sandbox):
             except docker.errors.NotFound:
                 pass
 
+    def get_working_directory(self):
+        return SANDBOX_WORKSPACE_DIR
+
 
 if __name__ == '__main__':
     try:

+ 3 - 0
opendevin/sandbox/docker/local_box.py

@@ -99,6 +99,9 @@ class LocalBox(Sandbox):
     def cleanup(self):
         self.close()
 
+    def get_working_directory(self):
+        return config.get(ConfigType.WORKSPACE_BASE)
+
 
 if __name__ == '__main__':
 

+ 9 - 39
opendevin/sandbox/docker/ssh_box.py

@@ -169,7 +169,7 @@ class DockerSSHBox(Sandbox):
 
     def start_ssh_session(self):
         # start ssh session at the background
-        self.ssh = pxssh.pxssh()
+        self.ssh = pxssh.pxssh(echo=False)
         hostname = SSH_HOSTNAME
         if RUN_AS_DEVIN:
             username = 'opendevin'
@@ -211,49 +211,14 @@ class DockerSSHBox(Sandbox):
             # send a SIGINT to the process
             self.ssh.sendintr()
             self.ssh.prompt()
-            command_output = self.ssh.before.decode(
-                'utf-8').lstrip(cmd).strip()
+            command_output = self.ssh.before.decode('utf-8').strip()
             return -1, f'Command: "{cmd}" timed out. Sending SIGINT to the process: {command_output}'
         command_output = self.ssh.before.decode('utf-8').strip()
 
-        # NOTE: there's some weird behavior with the prompt (it may come AFTER the command output)
-        # so we need to check if the command is in the output
-        n_tries = 5
-        while not command_output.startswith(cmd) and n_tries > 0:
-            self.ssh.prompt()
-            command_output = self.ssh.before.decode('utf-8').strip()
-            time.sleep(0.5)
-            n_tries -= 1
-        if n_tries == 0 and not command_output.startswith(cmd):
-            raise Exception(
-                f'Something went wrong with the SSH sanbox, cannot get output for command [{cmd}] after 5 retries'
-            )
-        logger.debug(f'Command output GOT SO FAR: {command_output}')
-        # once out, make sure that we have *every* output, we while loop until we get an empty output
-        while True:
-            logger.debug('WAITING FOR .prompt()')
-            self.ssh.sendline('\n')
-            timeout_not_reached = self.ssh.prompt(timeout=1)
-            if not timeout_not_reached:
-                logger.debug('TIMEOUT REACHED')
-                break
-            logger.debug('WAITING FOR .before')
-            output = self.ssh.before.decode('utf-8').strip()
-            logger.debug(f'WAITING FOR END OF command output ({bool(output)}): {output}')
-            if output == '':
-                break
-            command_output += output
-        command_output = command_output.lstrip(cmd).strip()
-
         # get the exit code
         self.ssh.sendline('echo $?')
-        self.ssh.prompt()
-        exit_code = self.ssh.before.decode('utf-8')
-        while not exit_code.startswith('echo $?'):
-            self.ssh.prompt()
-            exit_code = self.ssh.before.decode('utf-8')
-            logger.debug(f'WAITING FOR exit code: {exit_code}')
-        exit_code = int(exit_code.lstrip('echo $?').strip())
+        self.ssh.prompt(timeout=10)
+        exit_code = int(self.ssh.before.decode('utf-8').strip())
         return exit_code, command_output
 
     def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
@@ -337,6 +302,11 @@ class DockerSSHBox(Sandbox):
         except docker.errors.NotFound:
             pass
 
+    def get_working_directory(self):
+        self.ssh.sendline('pwd')
+        self.ssh.prompt(timeout=10)
+        return self.ssh.before.decode('utf-8').strip()
+
     def is_container_running(self):
         try:
             container = self.docker_client.containers.get(self.container_name)

+ 3 - 0
opendevin/sandbox/e2b/sandbox.py

@@ -124,3 +124,6 @@ class E2BBox(Sandbox):
 
     def close(self):
         self.sandbox.close()
+
+    def get_working_directory(self):
+        return self.sandbox.cwd

+ 4 - 0
opendevin/sandbox/sandbox.py

@@ -32,3 +32,7 @@ class Sandbox(ABC, PluginMixin):
     @abstractmethod
     def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
         pass
+
+    @abstractmethod
+    def get_working_directory(self):
+        pass

+ 3 - 0
opendevin/schema/config.py

@@ -2,6 +2,8 @@ from enum import Enum
 
 
 class ConfigType(str, Enum):
+    LLM_MAX_RETURN_TOKENS = 'LLM_MAX_RETURN_TOKENS'
+    LLM_TIMEOUT = 'LLM_TIMEOUT'
     LLM_API_KEY = 'LLM_API_KEY'
     LLM_BASE_URL = 'LLM_BASE_URL'
     WORKSPACE_BASE = 'WORKSPACE_BASE'
@@ -26,6 +28,7 @@ class ConfigType(str, Enum):
     E2B_API_KEY = 'E2B_API_KEY'
     SANDBOX_TYPE = 'SANDBOX_TYPE'
     SANDBOX_USER_ID = 'SANDBOX_USER_ID'
+    SANDBOX_TIMEOUT = 'SANDBOX_TIMEOUT'
     USE_HOST_NETWORK = 'USE_HOST_NETWORK'
     SSH_HOSTNAME = 'SSH_HOSTNAME'
     DISABLE_COLOR = 'DISABLE_COLOR'

+ 10 - 6
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log

@@ -310,15 +310,15 @@ This is your internal monologue, in JSON format:
   {
     "action": "think",
     "args": {
-      "thought": "It seems like there might be an existing project here. I should probably start by running `ls` to see what's here."
+      "thought": "It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself."
     }
   }
 ]
 
 
 Your most recent thought is at the bottom of that monologue. Continue your train of thought.
-What is your next thought or action? Your response must be in JSON format.
-It must be an object, and it must contain two fields:
+What is your next single thought or action? Your response must be in JSON format.
+It must be a single object, and it must contain two fields:
 * `action`, which is one of the actions below
 * `args`, which is a map of key-value pairs, specifying the arguments for that action
 
@@ -352,10 +352,14 @@ You should never act twice in a row without thinking. But if your last several
 actions are all "think" actions, you should consider taking a different action.
 
 Notes:
-* your environment is Debian Linux. You can install software with `apt`
-* your working directory will not change, even if you run `cd`. All commands will be run in the `/workspace` directory.
+* you are logged in as opendevin, but sudo will always work without a password.
+* all non-background commands will be forcibly stopped if they remain running for over 120 seconds.
+* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
 * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
+* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
+* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
+* whenever an action fails, always `think` about why it may have happened before acting again.
 
-What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.
 
 You've been thinking a lot lately. Maybe it's time to take action?

+ 10 - 6
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log

@@ -310,7 +310,7 @@ This is your internal monologue, in JSON format:
   {
     "action": "think",
     "args": {
-      "thought": "It seems like there might be an existing project here. I should probably start by running `ls` to see what's here."
+      "thought": "It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself."
     }
   },
   {
@@ -333,8 +333,8 @@ This is your internal monologue, in JSON format:
 
 
 Your most recent thought is at the bottom of that monologue. Continue your train of thought.
-What is your next thought or action? Your response must be in JSON format.
-It must be an object, and it must contain two fields:
+What is your next single thought or action? Your response must be in JSON format.
+It must be a single object, and it must contain two fields:
 * `action`, which is one of the actions below
 * `args`, which is a map of key-value pairs, specifying the arguments for that action
 
@@ -368,8 +368,12 @@ You should never act twice in a row without thinking. But if your last several
 actions are all "think" actions, you should consider taking a different action.
 
 Notes:
-* your environment is Debian Linux. You can install software with `apt`
-* your working directory will not change, even if you run `cd`. All commands will be run in the `/workspace` directory.
+* you are logged in as opendevin, but sudo will always work without a password.
+* all non-background commands will be forcibly stopped if they remain running for over 120 seconds.
+* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
 * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
+* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
+* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
+* whenever an action fails, always `think` about why it may have happened before acting again.
 
-What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.

+ 10 - 6
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_003.log

@@ -310,7 +310,7 @@ This is your internal monologue, in JSON format:
   {
     "action": "think",
     "args": {
-      "thought": "It seems like there might be an existing project here. I should probably start by running `ls` to see what's here."
+      "thought": "It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself."
     }
   },
   {
@@ -344,8 +344,8 @@ This is your internal monologue, in JSON format:
 
 
 Your most recent thought is at the bottom of that monologue. Continue your train of thought.
-What is your next thought or action? Your response must be in JSON format.
-It must be an object, and it must contain two fields:
+What is your next single thought or action? Your response must be in JSON format.
+It must be a single object, and it must contain two fields:
 * `action`, which is one of the actions below
 * `args`, which is a map of key-value pairs, specifying the arguments for that action
 
@@ -379,8 +379,12 @@ You should never act twice in a row without thinking. But if your last several
 actions are all "think" actions, you should consider taking a different action.
 
 Notes:
-* your environment is Debian Linux. You can install software with `apt`
-* your working directory will not change, even if you run `cd`. All commands will be run in the `/workspace` directory.
+* you are logged in as opendevin, but sudo will always work without a password.
+* all non-background commands will be forcibly stopped if they remain running for over 120 seconds.
+* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
 * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
+* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
+* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
+* whenever an action fails, always `think` about why it may have happened before acting again.
 
-What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.

+ 10 - 6
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_004.log

@@ -310,7 +310,7 @@ This is your internal monologue, in JSON format:
   {
     "action": "think",
     "args": {
-      "thought": "It seems like there might be an existing project here. I should probably start by running `ls` to see what's here."
+      "thought": "It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself."
     }
   },
   {
@@ -361,8 +361,8 @@ This is your internal monologue, in JSON format:
 
 
 Your most recent thought is at the bottom of that monologue. Continue your train of thought.
-What is your next thought or action? Your response must be in JSON format.
-It must be an object, and it must contain two fields:
+What is your next single thought or action? Your response must be in JSON format.
+It must be a single object, and it must contain two fields:
 * `action`, which is one of the actions below
 * `args`, which is a map of key-value pairs, specifying the arguments for that action
 
@@ -396,8 +396,12 @@ You should never act twice in a row without thinking. But if your last several
 actions are all "think" actions, you should consider taking a different action.
 
 Notes:
-* your environment is Debian Linux. You can install software with `apt`
-* your working directory will not change, even if you run `cd`. All commands will be run in the `/workspace` directory.
+* you are logged in as opendevin, but sudo will always work without a password.
+* all non-background commands will be forcibly stopped if they remain running for over 120 seconds.
+* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
 * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
+* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
+* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
+* whenever an action fails, always `think` about why it may have happened before acting again.
 
-What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.

+ 10 - 6
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_005.log

@@ -310,7 +310,7 @@ This is your internal monologue, in JSON format:
   {
     "action": "think",
     "args": {
-      "thought": "It seems like there might be an existing project here. I should probably start by running `ls` to see what's here."
+      "thought": "It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself."
     }
   },
   {
@@ -377,8 +377,8 @@ This is your internal monologue, in JSON format:
 
 
 Your most recent thought is at the bottom of that monologue. Continue your train of thought.
-What is your next thought or action? Your response must be in JSON format.
-It must be an object, and it must contain two fields:
+What is your next single thought or action? Your response must be in JSON format.
+It must be a single object, and it must contain two fields:
 * `action`, which is one of the actions below
 * `args`, which is a map of key-value pairs, specifying the arguments for that action
 
@@ -412,8 +412,12 @@ You should never act twice in a row without thinking. But if your last several
 actions are all "think" actions, you should consider taking a different action.
 
 Notes:
-* your environment is Debian Linux. You can install software with `apt`
-* your working directory will not change, even if you run `cd`. All commands will be run in the `/workspace` directory.
+* you are logged in as opendevin, but sudo will always work without a password.
+* all non-background commands will be forcibly stopped if they remain running for over 120 seconds.
+* your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
 * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
+* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
+* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
+* whenever an action fails, always `think` about why it may have happened before acting again.
 
-What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.

+ 15 - 13
tests/test_fileops.py

@@ -1,24 +1,26 @@
+from opendevin import config
+from opendevin.schema import ConfigType
+from opendevin.action import fileop
 from pathlib import Path
-
 import pytest
 
-from opendevin import config
-from opendevin.schema.config import ConfigType
-from opendevin.action import fileop
 
 
 def test_resolve_path():
-    assert fileop.resolve_path('test.txt') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
-    assert fileop.resolve_path('subdir/test.txt') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'subdir' / 'test.txt'
-    assert fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / 'test.txt') == \
-        Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
-    assert fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt') == \
+    assert fileop.resolve_path('test.txt', '/workspace') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
+    assert fileop.resolve_path('subdir/test.txt', '/workspace') == \
         Path(config.get(ConfigType.WORKSPACE_BASE)) / 'subdir' / 'test.txt'
-    assert fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt') == \
+    assert fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / 'test.txt', '/workspace') == \
         Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
+    assert fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt',
+                               '/workspace') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'subdir' / 'test.txt'
+    assert fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt',
+                               '/workspace') == Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test.txt'
     with pytest.raises(PermissionError):
-        fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / '..' / 'test.txt')
+        fileop.resolve_path(Path(fileop.SANDBOX_PATH_PREFIX) / '..' / 'test.txt', '/workspace')
     with pytest.raises(PermissionError):
-        fileop.resolve_path(Path('..') / 'test.txt')
+        fileop.resolve_path(Path('..') / 'test.txt', '/workspace')
     with pytest.raises(PermissionError):
-        fileop.resolve_path(Path('/') / 'test.txt')
+        fileop.resolve_path(Path('/') / 'test.txt', '/workspace')
+    assert fileop.resolve_path('test.txt', '/workspace/test') == \
+        Path(config.get(ConfigType.WORKSPACE_BASE)) / 'test' / 'test.txt'