Compare commits

..

18 Commits

Author SHA1 Message Date
Alex Bäuerle a2b49525ae install markdown 2024-05-01 11:06:55 -07:00
Alex Bäuerle 478ebedd41 Merge remote-tracking branch 'origin' into ab-docs-remove 2024-05-01 10:35:11 -07:00
Alex Bäuerle 016a56a3d9 ci(docs): remove and rebuild docs to make sure we're getting rid of files that are not there anymore
This is not ideal in that we have to regenerate the whole docs everytime but considering 1. it's pretty fast and 2. it seems to be the only way to ensure consistency it's probably worth doing it this way.
2024-05-01 10:32:09 -07:00
Tom Mery 545327cc1e upload file to workspace with button (#1394)
* upload file to workspace with button

* resolve filepath

* regenerate lock file

* fix lock file

* fix torch version

* fix lock

* fix poerty.lock

* fix lint

---------

Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2024-05-01 15:40:35 +00:00
Boxuan Li c7dd443fa2 CoderAgent: Render summary prompt conditionally (#1461)
* CoderAgent: Render repo summary conditionally

* Add unittests

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-05-01 15:40:20 +00:00
Robert Brennan c50319138e Revert "feat(makefile): add capability to skip Docker image pull (#1463)" (#1489)
This reverts commit 442ab7371c.
2024-05-01 11:00:06 -04:00
Xingyao Wang d80f025e21 Implement Jupyter Frontend (#1363)
* initialize plugin definition

* initialize plugin definition

* simplify mixin

* further improve plugin mixin

* add cache dir for pip

* support clean up cache

* add script for setup jupyter and execution server

* integrate JupyterRequirement to ssh_box

* source bashrc at the end of plugin load

* add execute_cli that accept code via stdin

* make JUPYTER_EXEC_SERVER_PORT configurable via env var

* increase background cmd sleep time

* Update opendevin/sandbox/plugins/mixin.py

Co-authored-by: Robert Brennan <accounts@rbren.io>

* add mixin to base class

* make jupyter requirement a dataclass

* source plugins only when >0 requirements

* add `sandbox_plugins` for each agent & have controller take care of it

* update build.sh to make logs available in /opendevin/logs

* switch to use config for lib and cache dir

* Add SANDBOX_WORKSPACE_DIR into config

* Add SANDBOX_WORKSPACE_DIR into config

* fix occurence of /workspace

* fix permission issue with /workspace

* use python to implement execute_cli to avoid stdin escape issue

* add IPythonRunCellAction and get it working

* wait until jupyter is avaialble

* support plugin via copying instead of mounting

* add agent talk action

* support follow-up user language feedback

* add __str__ for action to be printed better

* only print PLAN at the beginning

* wip: update codeact agent

* get rid the initial messate

* update codeact agent to handle null action;
add thought to bash

* dispatch thought for RUN action as well

* fix weird behavior of pxssh where the output would not flush correctly

* make ssh box can handle exit_code properly as well

* add initial version of swe-agent plugin;

* rename swe cursors

* split setup script into two and create two requirements

* print SWE-agent command documentation

* update swe-agent to default to no custom docs

* add initial version of swe-agent plugin;

* rename swe cursors

* split setup script into two and create two requirements

* print SWE-agent command documentation

* update swe-agent to default to no custom docs

* update dockerfile with dependency from swe-agent

* make env setup a separate script for .bashrc source

* add wip prompt

* fix mount_dir for ssh_box

* update prompt

* fix mount_dir for ssh_box

* default to use host network

* default to use host network

* move prompt to a separate file

* fix swe-tool plugins;
add missing _split_string

* remove hostname from sshbox

* update the prompt with edit functionality

* fix swe-tool plugins;
add missing _split_string

* add awaiting into status bar

* fix the bug of additional send event

* remove some print action

* move logic to config.py

* remove debugging comments

* make host network as default

* make WORKSPACE_MOUNT_PATH as abspath

* implement execute_cli via file cp

* Revert "implement execute_cli via file cp"

This reverts commit 06f0155bc1.

* add codeact dependencies to default container

* add IPythonRunCellObservation

* add back cache dir and default to /tmp

* make USE_HOST_NETWORK a bool

* revert use host network to false

* add temporarily fix for IPython RUN action

* preliminary implementation of CodeActAgent's jupyter

* update node module

* update prompt

* revert USE_HOST_NETWORK to true since it is not affecting anything

* attempt to fix lint

* remove newline

* update prompt

* Refactor browser style. (#1358)

* delete useless assets and css class.
* add waiting for page loaded (networkidle with 3s timeout)

* Add integration test framework with mock llm (#1301)

* Add integration test framework with mock llm

* Fix MonologueAgent and PlannerAgent tests

* Remove adhoc logging

* Use existing logs

* Fix SWEAgent and PlannerAgent

* Check-in test log files

* conftest: look up under test name folder only

* Add docstring to conftest

* Finish dev doc

* Avoid non-determinism

* Remove dependency on llm embedding model

* Init embedding model only for MonologueAgent

* Add adhoc fix for sandbox discrepancy

* Test ssh and exec sandboxes

* CI: fix missing sandbox type

* conftest: Remove hack

* Reword comment for TODO

* Revert "refactor(frontend): Terminal (#1315)" (#1360)

This reverts commit 27246aca7e.

* revert USE_HOST_NETWORK to true since it is not affecting anything

* attempt to fix lint

* handle IsADirectory errors (#1365)

* update to 0.4.0 (#1362)

Co-authored-by: Jim Su <jimsu@protonmail.com>

* feat(frontend): multiple design changes (#1370)

* fix/improve terminal hook (#1371)

* Revert "update node module"

This reverts commit 459b1031e7.

* support SyntaxHighlighter and markdown for jupyter visualization

* fix jupyter execution server

* make jupyter active

* improve the display of markdown and raw text

* get base64 image display for react

* add `thought` to most action class

* fix unit tests for current action abstraction

* support user exit

* update test cases with the latest action format (added 'thought')

* fix integration test for CodeActAGent by mocking stdin

* only mock stdin for tests with user_responses.log

* remove -exec integration test for CodeActAgent since it is not supported

* remove specific stop word

* fix comments

* improve clarity of prompt

* attempt to fix lint

* attempt to fix lint yet agiain

* fix py lint

* fix integration tests

* sandbox might failed in chown due to mounting, but it won't be fatal

* update debug instruction for sshbox

* fix typo

* get RUN_AS_DEVIN and network=host working with app sandbox

* get RUN_AS_DEVIN and network=host working with app sandbox

* attempt to fix the workspace base permission

* sandbox might failed in chown due to mounting, but it won't be fatal

* update sshbox instruction

* remove default user id since it will be passed in the instruction

* revert permission fix since it should be resolved by correct SANDBOX_USER_ID

* the permission issue can be fixed by simply provide correct env var

* remove log

* set sandbox user id to getuid by default

* move logging to initializer

* make the uid consistent across host, app container, and sandbox

* remove hostname as it causes sudo issue

* fix permission of entrypoint script

* make the uvicron app run as host user uid for jupyter plugin

* add warning message

* fix frontend lint

* update dev md for instruction of running unit tests

* add back unit tests

* revert back to the original sandbox implementation to fix testcases

* revert use host network

* get docker socket gid and usermod instead of chmod 777

* allow unit test workflow to find docker.sock

* make sandbox test working via patch

* fix arg parser that's broken for some reason

* try to fix app build disk space issue

* fix integration test

* Revert "fix arg parser that's broken for some reason"

This reverts commit 6cc8961133.

* update Development.md

* cleanup intergration tests & add exception for CodeAct+execbox

* fix config

* implement user_message action

* fix doc

* fix event dict error

* fix frontend lint

* revert accidentally changes to integration tests

* revert accidentally changes to integration tests

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Leo <ifuryst@gmail.com>
Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
Co-authored-by: Jim Su <jimsu@protonmail.com>
Co-authored-by: Alex Bäuerle <alex@a13x.io>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Robert Brennan <contact@rbren.io>
2024-05-01 22:37:01 +08:00
Arno.Edwards 442ab7371c feat(makefile): add capability to skip Docker image pull (#1463)
* feat(makefile): add capability to skip Docker image pull

* ci(github-actions): add conditional Docker installation based on ENV variable
2024-05-01 09:01:11 -04:00
Xingyao Wang 1c7cdbefdd feat(CodeActAgent): Support Agent-User Interaction during Task Execution and the Full Integration of CodeActAgent (#1290)
* initialize plugin definition

* initialize plugin definition

* simplify mixin

* further improve plugin mixin

* add cache dir for pip

* support clean up cache

* add script for setup jupyter and execution server

* integrate JupyterRequirement to ssh_box

* source bashrc at the end of plugin load

* add execute_cli that accept code via stdin

* make JUPYTER_EXEC_SERVER_PORT configurable via env var

* increase background cmd sleep time

* Update opendevin/sandbox/plugins/mixin.py

Co-authored-by: Robert Brennan <accounts@rbren.io>

* add mixin to base class

* make jupyter requirement a dataclass

* source plugins only when >0 requirements

* add `sandbox_plugins` for each agent & have controller take care of it

* update build.sh to make logs available in /opendevin/logs

* switch to use config for lib and cache dir

* Add SANDBOX_WORKSPACE_DIR into config

* Add SANDBOX_WORKSPACE_DIR into config

* fix occurence of /workspace

* fix permission issue with /workspace

* use python to implement execute_cli to avoid stdin escape issue

* add IPythonRunCellAction and get it working

* wait until jupyter is avaialble

* support plugin via copying instead of mounting

* add agent talk action

* support follow-up user language feedback

* add __str__ for action to be printed better

* only print PLAN at the beginning

* wip: update codeact agent

* get rid the initial messate

* update codeact agent to handle null action;
add thought to bash

* dispatch thought for RUN action as well

* fix weird behavior of pxssh where the output would not flush correctly

* make ssh box can handle exit_code properly as well

* add initial version of swe-agent plugin;

* rename swe cursors

* split setup script into two and create two requirements

* print SWE-agent command documentation

* update swe-agent to default to no custom docs

* add initial version of swe-agent plugin;

* rename swe cursors

* split setup script into two and create two requirements

* print SWE-agent command documentation

* update swe-agent to default to no custom docs

* update dockerfile with dependency from swe-agent

* make env setup a separate script for .bashrc source

* add wip prompt

* fix mount_dir for ssh_box

* update prompt

* fix mount_dir for ssh_box

* default to use host network

* default to use host network

* move prompt to a separate file

* fix swe-tool plugins;
add missing _split_string

* remove hostname from sshbox

* update the prompt with edit functionality

* fix swe-tool plugins;
add missing _split_string

* add awaiting into status bar

* fix the bug of additional send event

* remove some print action

* move logic to config.py

* remove debugging comments

* make host network as default

* make WORKSPACE_MOUNT_PATH as abspath

* implement execute_cli via file cp

* Revert "implement execute_cli via file cp"

This reverts commit 06f0155bc1.

* add codeact dependencies to default container

* add IPythonRunCellObservation

* add back cache dir and default to /tmp

* make USE_HOST_NETWORK a bool

* revert use host network to false

* add temporarily fix for IPython RUN action

* update prompt

* revert USE_HOST_NETWORK to true since it is not affecting anything

* attempt to fix lint

* remove newline

* fix jupyter execution server

* add `thought` to most action class

* fix unit tests for current action abstraction

* support user exit

* update test cases with the latest action format (added 'thought')

* fix integration test for CodeActAGent by mocking stdin

* only mock stdin for tests with user_responses.log

* remove -exec integration test for CodeActAgent since it is not supported

* remove specific stop word

* fix comments

* improve clarity of prompt

* fix py lint

* fix integration tests

* sandbox might failed in chown due to mounting, but it won't be fatal

* update debug instruction for sshbox

* fix typo

* get RUN_AS_DEVIN and network=host working with app sandbox

* get RUN_AS_DEVIN and network=host working with app sandbox

* attempt to fix the workspace base permission

* sandbox might failed in chown due to mounting, but it won't be fatal

* update sshbox instruction

* remove default user id since it will be passed in the instruction

* revert permission fix since it should be resolved by correct SANDBOX_USER_ID

* the permission issue can be fixed by simply provide correct env var

* remove log

* set sandbox user id to getuid by default

* move logging to initializer

* make the uid consistent across host, app container, and sandbox

* remove hostname as it causes sudo issue

* fix permission of entrypoint script

* make the uvicron app run as host user uid for jupyter plugin

* add warning message

* update dev md for instruction of running unit tests

* add back unit tests

* revert back to the original sandbox implementation to fix testcases

* revert use host network

* get docker socket gid and usermod instead of chmod 777

* allow unit test workflow to find docker.sock

* make sandbox test working via patch

* fix arg parser that's broken for some reason

* try to fix app build disk space issue

* fix integration test

* Revert "fix arg parser that's broken for some reason"

This reverts commit 6cc8961133.

* update Development.md

* cleanup intergration tests & add exception for CodeAct+execbox

* fix config

* implement user_message action

* fix doc

* fix event dict error

* fix frontend lint

* revert accidentally changes to integration tests

* revert accidentally changes to integration tests

---------

Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Robert Brennan <contact@rbren.io>
2024-05-01 08:40:00 -04:00
Engel Nyst ea214d1c07 Re-initialize monologue agent after reset (#1478)
* re-initialize monologue agent after reset

* Add to docstring

* Remove now duplicated instances
2024-05-01 08:38:11 -04:00
amodev 62e4fb47b2 Fix API key exposure in toast notifications, resolves #1477 (#1480) 2024-05-01 06:01:48 +00:00
sp.wack 5f2d6f58f2 update tests after setting modals upgrade (#1467) 2024-04-30 10:18:32 -07:00
Robert Brennan 0cda5f64af Add integration test with dummy agent (#1316)
* first pass at dummy

* add assertion to dummy

* add dummy workflow

* beef up tests

* try and fix huggingface issue

* remove newlines

* rename test

* move to pytest

* Revert " move to pytest"

This reverts commit de8121c400.

* fix lint

* delint

* Update .github/workflows/dummy-agent-test.yml

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>

---------

Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
2024-04-30 16:52:00 +00:00
ChengyangDu eb1c3d8790 fix: configurable sand box user id (#1459) 2024-04-30 16:51:40 +00:00
Xingyao Wang fe43aeb9b6 Revert ssh box implemetation, fix multi-line command issues and add unit tests (#1460)
* update dev md for instruction of running unit tests

* add back unit tests

* revert back to the original sandbox implementation to fix testcases

* allow unit test workflow to find docker.sock

* make sandbox test working via patch

* fix arg parser that's broken for some reason

* fix integration test

* Revert "fix arg parser that's broken for some reason"

This reverts commit 6cc8961133.

* update Development.md
2024-04-30 12:46:35 -04:00
Xingyao Wang ccbbabac1c Get RUN_AS_DEVIN working with app sandbox (#1426)
* get RUN_AS_DEVIN and network=host working with app sandbox

* attempt to fix the workspace base permission

* sandbox might failed in chown due to mounting, but it won't be fatal

* update sshbox instruction

* remove default user id since it will be passed in the instruction

* revert permission fix since it should be resolved by correct SANDBOX_USER_ID

* the permission issue can be fixed by simply provide correct env var

* remove log

* set sandbox user id to getuid by default

* move logging to initializer

* make the uid consistent across host, app container, and sandbox

* remove hostname as it causes sudo issue

* fix permission of entrypoint script

* make the uvicron app run as host user uid for jupyter plugin

* revert use host network

* get docker socket gid and usermod instead of chmod 777

* try to fix app build disk space issue
2024-04-30 12:39:52 -04:00
Alex Bäuerle eb7703a44e feat(frontend): add empty states to all tabs according to Figma (#1456)
* feat(frontend): add empty states to all tabs according to Figma

* import order

---------

Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-04-30 12:34:23 -04:00
Robert Brennan 31c1a2d748 Update bug_template.yml (#1458) 2024-04-30 03:36:39 +02:00
124 changed files with 3586 additions and 564 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ body:
label: Is there an existing issue for the same bug?
description: Please check if an issue already exists for the bug you encountered.
options:
- label: I have checked the troubleshooting document at https://github.com/OpenDevin/OpenDevin/blob/main/docs/guides/Troubleshooting.md
- label: I have checked the troubleshooting document at https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting
required: true
- label: I have checked the existing issues.
required: true
+21
View File
@@ -0,0 +1,21 @@
name: Run e2e test with dummy agent
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install --without evaluation
wget https://huggingface.co/BAAI/bge-small-en-v1.5/raw/main/1_Pooling/config.json -P /tmp/llama_index/models--BAAI--bge-small-en-v1.5/snapshots/5c38ec7c405ec4b44b94cc5a9bb96e735b38267a/1_Pooling/
- name: Run tests
run: |
poetry run python opendevin/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent
+15 -2
View File
@@ -42,8 +42,21 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Build and push ${{ matrix.image }}
if: github.event.pull_request.head.repo.full_name == github.repository
+6
View File
@@ -34,11 +34,16 @@ jobs:
brew install colima docker
colima start
# For testcontainers to find the Colima socket
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
@@ -70,6 +75,7 @@ jobs:
- name: Run Tests
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
+12
View File
@@ -82,3 +82,15 @@ If you encounter any issues with the Language Model (LM) or you're simply curiou
```bash
make help
```
### 8. Testing
#### Unit tests
```bash
poetry run pytest ./tests/unit/test_sandbox.py
```
#### Integration tests
Please refer to [this README](./tests/integration/README.md) for details.
+6
View File
@@ -235,6 +235,12 @@ clean:
@rm -rf opendevin/.cache
@echo "$(GREEN)Caches cleaned up successfully.$(RESET)"
python-docs:
@echo "$(YELLOW)Generating documentation...$(RESET)"
@rm -rf docs/modules/python
@pydoc-markdown
@echo "$(GREEN)Documentation generated successfully.$(RESET)"
# Help
help:
@echo "$(BLUE)Usage: make [target]$(RESET)"
+2
View File
@@ -26,6 +26,7 @@ The `state` contains:
Here is a list of available Actions, which can be returned by `agent.step()`:
- [`CmdRunAction`](../opendevin/action/bash.py) - Runs a command inside a sandboxed terminal
- [`CmdKillAction`](../opendevin/action/bash.py) - Kills a background command
- [`IPythonRunCellAction`](../opendevin/action/bash.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../opendevin/sandbox/plugins) as a requirement.
- [`FileReadAction`](../opendevin/action/fileop.py) - Reads the content of a file
- [`FileWriteAction`](../opendevin/action/fileop.py) - Writes new content to a file
- [`BrowseURLAction`](../opendevin/action/browse.py) - Gets the content of a URL
@@ -33,6 +34,7 @@ Here is a list of available Actions, which can be returned by `agent.step()`:
- [`AddTaskAction`](../opendevin/action/tasks.py) - Adds a subtask to the plan
- [`ModifyTaskAction`](../opendevin/action/tasks.py) - Changes the state of a subtask
- [`AgentThinkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history (as well as the chat log)
- [`AgentTalkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history and talk to the user.
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user to enter a new task
You can use `action.to_dict()` and `action_from_dict` to serialize and deserialize actions.
+5 -2
View File
@@ -8,17 +8,20 @@ from .micro.registry import all_microagents
load_dotenv()
# Import agents after environment variables are loaded
from . import ( # noqa: E402
SWE_agent,
codeact_agent,
delegator_agent,
dummy_agent,
monologue_agent,
planner_agent,
)
__all__ = ['monologue_agent', 'codeact_agent',
'planner_agent', 'SWE_agent', 'delegator_agent']
'planner_agent', 'SWE_agent',
'delegator_agent',
'dummy_agent']
for agent in all_microagents.values():
name = agent['name']
+78 -56
View File
@@ -1,54 +1,37 @@
import re
from typing import List, Mapping
from agenthub.codeact_agent.prompt import EXAMPLES, SYSTEM_MESSAGE
from opendevin.action import (
Action,
AgentEchoAction,
AgentFinishAction,
AgentTalkAction,
CmdRunAction,
IPythonRunCellAction,
NullAction,
)
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentMessageObservation,
CmdOutputObservation,
IPythonRunCellObservation,
UserMessageObservation,
)
from opendevin.sandbox.plugins import (
JupyterRequirement,
PluginRequirement,
SWEAgentCommandsRequirement,
)
from opendevin.sandbox.plugins import JupyterRequirement, PluginRequirement
from opendevin.state import State
SYSTEM_MESSAGE = """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.
DO NOT provide code in ```triple backticks```. Instead, you should execute bash command on behalf of the user by wrapping them with <execute> and </execute>.
For example:
You can list the files in the current directory by executing the following command:
<execute>ls</execute>
You can also install packages using pip:
<execute> pip install numpy </execute>
You can also write a block of code to a file:
<execute>
echo "import math
print(math.pi)" > math.py
</execute>
When you are done, execute the following to close the shell and end the conversation:
<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>.'
)
def parse_response(response) -> str:
action = response.choices[0].message.content
if '<execute>' in action and '</execute>' not in action:
action += '</execute>'
for lang in ['bash', 'ipython']:
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
action += f'</execute_{lang}>'
return action
@@ -58,7 +41,20 @@ class CodeActAgent(Agent):
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
"""
sandbox_plugins: List[PluginRequirement] = [JupyterRequirement()]
sandbox_plugins: List[PluginRequirement] = [JupyterRequirement(), SWEAgentCommandsRequirement()]
SUPPORTED_ACTIONS = (
CmdRunAction,
IPythonRunCellAction,
AgentEchoAction,
AgentTalkAction,
NullAction
)
SUPPORTED_OBSERVATIONS = (
AgentMessageObservation,
UserMessageObservation,
CmdOutputObservation,
IPythonRunCellObservation
)
def __init__(
self,
@@ -93,56 +89,82 @@ class CodeActAgent(Agent):
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': 'user',
'content': (
f'Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\n'
f"NOW, LET'S START!\n\n{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'
if isinstance(
obs, AgentMessageObservation
): # warning message from itself
prev_action, self.SUPPORTED_ACTIONS
), f'{prev_action.__class__} is not supported (supported: {self.SUPPORTED_ACTIONS})'
# prev_action is already added to self.messages when returned
# handle observations
assert isinstance(
obs, self.SUPPORTED_OBSERVATIONS
), f'{obs.__class__} is not supported (supported: {self.SUPPORTED_OBSERVATIONS})'
if isinstance(obs, (AgentMessageObservation, UserMessageObservation)):
self.messages.append(
{'role': 'user', 'content': obs.content})
# User wants to exit
if obs.content.strip() == '/exit':
return AgentFinishAction()
elif isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + obs.content
content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
self.messages.append({'role': 'user', 'content': content})
elif isinstance(obs, IPythonRunCellObservation):
content = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
splited = content.split('\n')
for i, line in enumerate(splited):
if '![image](data:image/png;base64,' in line:
splited[i] = '![image](data:image/png;base64, ...) already displayed to user'
content = '\n'.join(splited)
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_ipython>',
'</execute_bash>',
],
temperature=0.0
)
action_str: str = parse_response(response)
state.num_of_chars += sum(len(message['content'])
for message in self.messages) + len(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)
if command is not None:
if bash_command := re.search(r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL):
# remove the command from the action string to get thought
thought = action_str.replace(bash_command.group(0), '').strip()
# a command was found
command_group = command.group(1)
command_group = bash_command.group(1).strip()
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command=command_group)
# # execute the code
# # TODO: does exit_code get loaded into Message?
# exit_code, observation = self.env.execute(command_group)
# self._history.append(Message(Role.ASSISTANT, observation))
return CmdRunAction(command=command_group, thought=thought)
elif python_code := re.search(r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL):
# a code block was found
code_group = python_code.group(1).strip()
thought = action_str.replace(python_code.group(0), '').strip()
return IPythonRunCellAction(code=code_group, thought=thought)
else:
# we could provide a error message for the model to continue similar to
# https://github.com/xingyaoww/mint-bench/blob/main/mint/envs/general_env.py#L18-L23
# observation = INVALID_INPUT_MESSAGE
# self._history.append(Message(Role.ASSISTANT, observation))
return AgentEchoAction(
content=INVALID_INPUT_MESSAGE
) # warning message to itself
# We assume the LLM is GOOD enough that when it returns pure natural language
# it want to talk to the user
return AgentTalkAction(content=action_str)
def search_memory(self, query: str) -> List[str]:
raise NotImplementedError('Implement this abstract method')
+226
View File
@@ -0,0 +1,226 @@
from opendevin.sandbox.plugins import SWEAgentCommandsRequirement
_SWEAGENT_BASH_DOCS = '\n'.join(
filter(
lambda x: not x.startswith('submit'),
SWEAgentCommandsRequirement.documentation.split('\n')
)
)
# _SWEAGENT_BASH_DOCS content below:
"""
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
goto <line_number> - moves the window to show <line_number>
scroll_down - moves the window down {WINDOW} lines
scroll_up - moves the window down {WINDOW} lines
create <filename> - creates and opens a new file with the given name
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
edit <start_line>:<end_line>
<replacement_text>
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
"""
_COMMAND_DOCS = (
'\nApart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:\n'
f'{_SWEAGENT_BASH_DOCS}'
"Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
)
SYSTEM_MESSAGE = f"""A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
{_COMMAND_DOCS}
Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
The assistant's response should be concise, but do express their thoughts.
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
"""
EXAMPLES = """
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
Observation:
[Code executed successfully with no output]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
Observation:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
Observation:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return str(numbers)
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
ASSISTANT:
I should edit the file to display the numbers in a table format. Let me do that for you:
<execute_bash>
edit 8:8 <<EOF
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
EOF
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
ASSISTANT:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
"""
INVALID_INPUT_MESSAGE = (
"I don't understand your input. \n"
'If you want to execute a bash command, please use <execute_bash> YOUR_COMMAND_HERE </execute_bash>.\n'
'If you want to execute a block of Python code, please use <execute_ipython> YOUR_COMMAND_HERE </execute_ipython>.\n'
)
+5
View File
@@ -0,0 +1,5 @@
from opendevin.agent import Agent
from .agent import DummyAgent
Agent.register('DummyAgent', DummyAgent)
+107 -12
View File
@@ -1,23 +1,118 @@
"""Module for a Dummy agent."""
import time
from typing import List, TypedDict
from typing import List
from opendevin.action import Action
from opendevin.action.base import NullAction
from opendevin.action import (
Action,
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
ModifyTaskAction,
)
from opendevin.agent import Agent
from opendevin.controller.agent_controller import AgentController
from opendevin.observation.base import NullObservation, Observation
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentRecallObservation,
CmdOutputObservation,
FileReadObservation,
FileWriteObservation,
NullObservation,
Observation,
)
from opendevin.state import State
"""
FIXME: There are a few problems this surfaced
* FileWrites seem to add an unintended newline at the end of the file
* command_id is sometimes a number, sometimes a string
* Why isn't the output of the background command split between two steps?
* Browser not working
"""
ActionObs = TypedDict('ActionObs', {'action': Action, 'observations': List[Observation]})
BACKGROUND_CMD = 'echo "This is in the background" && sleep .1 && echo "This too"'
class DummyAgent(Agent):
"""A dummy agent that does nothing but can be used in testing."""
"""
The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
without making any LLM calls.
"""
async def run(self, controller: AgentController) -> Observation:
return NullObservation('')
def __init__(self, llm: LLM):
super().__init__(llm)
self.steps: List[ActionObs] = [{
'action': AddTaskAction(parent='0', goal='check the current directory'),
'observations': [NullObservation('')],
}, {
'action': AddTaskAction(parent='0.0', goal='run ls'),
'observations': [NullObservation('')],
}, {
'action': ModifyTaskAction(id='0.0', state='in_progress'),
'observations': [NullObservation('')],
}, {
'action': AgentThinkAction(thought='Time to get started!'),
'observations': [NullObservation('')],
}, {
'action': CmdRunAction(command='echo "foo"'),
'observations': [CmdOutputObservation('foo', command_id=-1, command='echo "foo"')],
}, {
'action': FileWriteAction(content='echo "Hello, World!"', path='hello.sh'),
'observations': [FileWriteObservation('', path='hello.sh')],
}, {
'action': FileReadAction(path='hello.sh'),
'observations': [FileReadObservation('echo "Hello, World!"\n', path='hello.sh')],
}, {
'action': CmdRunAction(command='bash hello.sh'),
'observations': [CmdOutputObservation('Hello, World!', command_id=-1, command='bash hello.sh')],
}, {
'action': CmdRunAction(command=BACKGROUND_CMD, background=True),
'observations': [
CmdOutputObservation('Background command started. To stop it, send a `kill` action with id 42', command_id='42', command=BACKGROUND_CMD), # type: ignore[arg-type]
CmdOutputObservation('This is in the background\nThis too\n', command_id='42', command=BACKGROUND_CMD), # type: ignore[arg-type]
]
}, {
'action': AgentRecallAction(query='who am I?'),
'observations': [
AgentRecallObservation('', memories=['I am a computer.']),
# CmdOutputObservation('This too\n', command_id='42', command=BACKGROUND_CMD),
],
}, {
'action': BrowseURLAction(url='https://google.com'),
'observations': [
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
],
}, {
'action': AgentFinishAction(),
'observations': [],
}]
def step(self, state: State) -> Action:
return NullAction('')
time.sleep(0.1)
if state.iteration > 0:
prev_step = self.steps[state.iteration - 1]
if 'observations' in prev_step:
expected_observations = prev_step['observations']
hist_start = len(state.history) - len(expected_observations)
for i in range(len(expected_observations)):
hist_obs = state.history[hist_start + i][1].to_dict()
expected_obs = expected_observations[i].to_dict()
if 'command_id' in hist_obs['extras'] and hist_obs['extras']['command_id'] != -1:
del hist_obs['extras']['command_id']
hist_obs['content'] = ''
if 'command_id' in expected_obs['extras'] and expected_obs['extras']['command_id'] != -1:
del expected_obs['extras']['command_id']
expected_obs['content'] = ''
if hist_obs != expected_obs:
print('\nactual', hist_obs)
print('\nexpect', expected_obs)
assert hist_obs == expected_obs, f'Expected observation {expected_obs}, got {hist_obs}'
return self.steps[state.iteration]['action']
def search_memory(self, query: str) -> List[str]:
return []
return ['I am a computer.']
+14
View File
@@ -0,0 +1,14 @@
## Introduction
This package contains definitions of micro-agents. A micro-agent is defined
in the following structure:
```
[AgentName]
├── agent.yaml
└── prompt.md
```
Note that `prompt.md` could use jinja2 template syntax. During runtime, `prompt.md`
is loaded and rendered, and used together with `agent.yaml` to initialize a
micro-agent.
+2
View File
@@ -4,9 +4,11 @@ need to modify to complete this task:
{{ state.plan.main_goal }}
{% if state.inputs.summary %}
Here's a summary of the codebase, as it relates to this task:
{{ state.inputs.summary }}
{% endif %}
## Available Actions
{{ instructions.actions.run }}
+8 -6
View File
@@ -136,6 +136,7 @@ class MonologueAgent(Agent):
Utilizes the INITIAL_THOUGHTS list to give the agent a context for it's capabilities
and how to navigate the WORKSPACE_MOUNT_PATH_IN_SANDBOX in `config` (e.g., /workspace by default).
Short circuited to return when already initialized.
Will execute again when called after reset.
Parameters:
- task (str): The initial goal statement provided by the user
@@ -156,6 +157,10 @@ class MonologueAgent(Agent):
else:
self.memory = None
self._add_initial_thoughts(task)
self._initialized = True
def _add_initial_thoughts(self, task):
previous_action = ''
for thought in INITIAL_THOUGHTS:
thought = thought.replace('$TASK', task)
@@ -207,7 +212,6 @@ class MonologueAgent(Agent):
else:
action = AgentThinkAction(thought=thought)
self._add_event(action.to_memory())
self._initialized = True
def step(self, state: State) -> Action:
"""
@@ -256,8 +260,6 @@ class MonologueAgent(Agent):
def reset(self) -> None:
super().reset()
self.monologue = Monologue()
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
self.memory = LongTermMemory()
else:
self.memory = None
# Reset the initial monologue and memory
self._initialized = False
+27 -3
View File
@@ -32,7 +32,8 @@ FROM python:3.12-slim as runtime
WORKDIR /app
ENV RUN_AS_DEVIN=false
ENV RUN_AS_DEVIN=true
ENV SANDBOX_USER_ID=1000
ENV USE_HOST_NETWORK=false
ENV SSH_HOSTNAME=host.docker.internal
ENV WORKSPACE_BASE=/opt/workspace_base
@@ -40,13 +41,23 @@ ENV OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION
RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \
&& apt-get install -y curl ssh
&& apt-get install -y curl ssh sudo
RUN useradd -m -u $SANDBOX_USER_ID -s /bin/bash opendevin && \
usermod -aG sudo opendevin && \
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN chown -R opendevin:opendevin /app
USER opendevin
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH" \
PYTHONPATH='/app'
COPY --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# change ownership of the virtual environment to the sandbox user
USER root
RUN chown -R opendevin:opendevin ${VIRTUAL_ENV}
USER opendevin
COPY ./opendevin ./opendevin
COPY ./agenthub ./agenthub
@@ -55,4 +66,17 @@ RUN playwright install --with-deps chromium
COPY --from=frontend-builder /app/dist ./frontend/dist
CMD ["uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"]
USER root
RUN chown -R opendevin:opendevin /app
# make group permissions the same as user permissions
RUN chmod -R g=u /app
USER opendevin
# change ownership of the app directory to the sandbox user
COPY ./containers/app/entrypoint.sh /app/entrypoint.sh
# run the script as root
USER root
RUN chown opendevin:opendevin /app/entrypoint.sh
RUN chmod 777 /app/entrypoint.sh
CMD ["/app/entrypoint.sh"]
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# check user is root
if [ "$(id -u)" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
if [ -z "$SANDBOX_USER_ID" ]; then
echo "SANDBOX_USER_ID is not set"
exit 1
fi
# change uid of opendevin user to match the host user
# but the group id is not changed, so the user can still access everything under /app
usermod -u $SANDBOX_USER_ID opendevin
# get the user group of /var/run/docker.sock and set opendevin to that group
DOCKER_SOCKET_GID=$(stat -c '%g' /var/run/docker.sock)
echo "Docker socket group id: $DOCKER_SOCKET_GID"
usermod -aG $DOCKER_SOCKET_GID opendevin
# switch to the user and start the server
su opendevin -c "cd /app && uvicorn opendevin.server.listen:app --host 0.0.0.0 --port 3000"
+4
View File
@@ -27,3 +27,7 @@ RUN mkdir -p -m0755 /var/run/sshd
# symlink python3 to python
RUN ln -s /usr/bin/python3 /usr/bin/python
# install basic dependencies for CodeActAgent
RUN pip3 install --upgrade pip
RUN pip3 install jupyterlab notebook jupyter_kernel_gateway flake8
+7 -3
View File
@@ -42,7 +42,11 @@ repos:
always_run: true
pass_filenames: false
- repo: https://github.com/NiklasRosenstein/pydoc-markdown
rev: develop
- repo: local
hooks:
- id: pydoc-markdown
- id: pydoc
name: pydoc
entry: ./dev_config/python/pydoc.sh
language: script
types: [python]
pass_filenames: false
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
pip install pydoc-markdown && rm -rf ./docs/modules/python && pydoc-markdown
@@ -3,13 +3,12 @@ sidebar_label: agent
title: agenthub.dummy_agent.agent
---
Module for a Dummy agent.
## DummyAgent Objects
```python
class DummyAgent(Agent)
```
A dummy agent that does nothing but can be used in testing.
The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
without making any LLM calls.
@@ -12,3 +12,12 @@ class CmdOutputObservation(Observation)
This data class represents the output of a command.
## IPythonRunCellObservation Objects
```python
@dataclass
class IPythonRunCellObservation(Observation)
```
This data class represents the output of a IPythonRunCellAction.
+13 -1
View File
@@ -13,9 +13,13 @@ class ActionTypeSchema(BaseModel)
Initializes the agent. Only sent by client.
#### USER\_MESSAGE
Sends a message from the user. Only sent by the client.
#### START
Starts a new development task. Only sent by the client.
Starts a new development task OR send chat from the user. Only sent by the client.
#### READ
@@ -29,6 +33,10 @@ Writes the content to a file.
Runs a command.
#### RUN\_IPYTHON
Runs a IPython cell.
#### KILL
Kills a background command.
@@ -45,6 +53,10 @@ Searches long-term memory
Allows the agent to make a plan, set a goal, or record thoughts
#### TALK
Allows the agent to respond to the user.
#### DELEGATE
Delegates a task to another agent.
@@ -21,6 +21,10 @@ The HTML content of a URL
The output of a command
#### RUN\_IPYTHON
Runs a IPython cell.
#### RECALL
The result of a search
@@ -17,6 +17,10 @@ Initial state of the task.
The task is running.
#### AWAITING\_USER\_INPUT
The task is awaiting user input.
#### PAUSED
The task is paused.
+19
View File
@@ -26,6 +26,7 @@
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.0",
"react-icons": "^5.0.1",
@@ -44,6 +45,7 @@
"@types/node": "^18.0.0 ",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.0.0",
@@ -4878,6 +4880,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-highlight": {
"version": "0.12.8",
"resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.8.tgz",
"integrity": "sha512-V7O7zwXUw8WSPd//YUO8sz489J/EeobJljASGhP0rClrvq+1Y1qWEpToGu+Pp7YuChxhAXSgkLkrOYpZX5A62g==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-syntax-highlighter": {
"version": "15.5.11",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.11.tgz",
@@ -12799,6 +12810,14 @@
"react": "^18.2.0"
}
},
"node_modules/react-highlight": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/react-highlight/-/react-highlight-0.15.0.tgz",
"integrity": "sha512-5uV/b/N4Z421GSVVe05fz+OfTsJtFzx/fJBdafZyw4LS70XjIZwgEx3Lrkfc01W/RzZ2Dtfb0DApoaJFAIKBtA==",
"dependencies": {
"highlight.js": "^10.5.0"
}
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+2
View File
@@ -25,6 +25,7 @@
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.0",
"react-icons": "^5.0.1",
@@ -64,6 +65,7 @@
"@types/node": "^18.0.0 ",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.0.0",
@@ -15,6 +15,10 @@ const AgentStatusMap: { [k: string]: { message: string; indicator: string } } =
message: "Agent is running task...",
indicator: "bg-green-500",
},
[AgentTaskState.AWAITING_USER_INPUT]: {
message: "Agent is awaiting user input...",
indicator: "bg-orange-500",
},
[AgentTaskState.PAUSED]: {
message: "Agent has paused.",
indicator: "bg-yellow-500",
+12 -21
View File
@@ -1,25 +1,13 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { IoIosGlobe } from "react-icons/io";
import { useSelector } from "react-redux";
import { HiOutlineMagnifyingGlass } from "react-icons/hi2";
import { HiCursorClick } from "react-icons/hi";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import logo from "../assets/logo.png";
function BlankPage(): JSX.Element {
return (
<div className="h-full bg-slate-200 flex flex-col items-center justify-center">
<img src={logo} alt="Blank Page" className="w-28 h-28" />
<div className="h-8 flex items-center bg-slate-900 px-2 rounded-3xl ml-3 space-x-2">
<HiOutlineMagnifyingGlass size={20} />
<span>OpenDevin: Code Less, Make More.</span>
<HiCursorClick size={20} />
</div>
</div>
);
}
function Browser(): JSX.Element {
const { t } = useTranslation();
const { url, screenshotSrc } = useSelector(
(state: RootState) => state.browser,
);
@@ -30,15 +18,18 @@ function Browser(): JSX.Element {
: `data:image/png;base64,${screenshotSrc || ""}`;
return (
<div className="h-full w-full flex flex-col justify-evenly p-2 space-y-2">
<div className="w-full py-2 px-5 rounded-3xl bg-neutral-700 text-gray-200 truncate">
<div className="h-full w-full flex flex-col text-neutral-400">
<div className="w-full p-2 truncate border-b border-neutral-600">
{url}
</div>
<div className="overflow-y-auto h-4/5 scrollbar-hide rounded-xl">
<div className="overflow-y-auto grow scrollbar-hide rounded-xl">
{screenshotSrc ? (
<img src={imgSrc} className="rounded-xl" alt="Browser Screenshot" />
) : (
<BlankPage />
<div className="flex flex-col items-center h-full justify-center">
<IoIosGlobe size={100} />
{t(I18nKey.BROWSER$EMPTY_MESSAGE)}
</div>
)}
</div>
</div>
+8 -1
View File
@@ -3,6 +3,7 @@ import { IoMdChatbubbles } from "react-icons/io";
import Markdown from "react-markdown";
import { useSelector } from "react-redux";
import { useTypingEffect } from "#/hooks/useTypingEffect";
import AgentTaskState from "../types/AgentTaskState";
import {
addAssistantMessageToChat,
sendChatMessage,
@@ -117,6 +118,12 @@ function MessageList(): JSX.Element {
function ChatInterface(): JSX.Element {
const { initialized } = useSelector((state: RootState) => state.task);
const { curTaskState } = useSelector((state: RootState) => state.agent);
const onUserMessage = (msg: string) => {
const isNewTask = curTaskState === AgentTaskState.INIT;
sendChatMessage(msg, isNewTask);
};
return (
<div className="flex flex-col h-full p-0 bg-neutral-800">
@@ -125,7 +132,7 @@ function ChatInterface(): JSX.Element {
Chat
</div>
<MessageList />
<ChatInput disabled={!initialized} onSendMessage={sendChatMessage} />
<ChatInput disabled={!initialized} onSendMessage={onUserMessage} />
</div>
);
}
+19 -8
View File
@@ -2,13 +2,17 @@ import Editor, { Monaco } from "@monaco-editor/react";
import { Tab, Tabs } from "@nextui-org/react";
import type { editor } from "monaco-editor";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { VscCode } from "react-icons/vsc";
import { useDispatch, useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { selectFile } from "#/services/fileService";
import { setCode } from "#/state/codeSlice";
import { RootState } from "#/store";
import FileExplorer from "./file-explorer/FileExplorer";
function CodeEditor(): JSX.Element {
const { t } = useTranslation();
const [selectedFileName, setSelectedFileName] = useState("");
const dispatch = useDispatch();
@@ -64,14 +68,21 @@ function CodeEditor(): JSX.Element {
title={selectedFileName}
/>
</Tabs>
<div className="flex grow">
<Editor
height="100%"
path={selectedFileName.toLocaleLowerCase()}
defaultValue=""
value={code}
onMount={handleEditorDidMount}
/>
<div className="flex grow items-center justify-center">
{selectedFileName === "" ? (
<div className="flex flex-col items-center text-neutral-400">
<VscCode size={100} />
{t(I18nKey.CODE_EDITOR$EMPTY_MESSAGE)}
</div>
) : (
<Editor
height="100%"
path={selectedFileName.toLocaleLowerCase()}
defaultValue=""
value={code}
onMount={handleEditorDidMount}
/>
)}
</div>
</div>
</div>
+77
View File
@@ -0,0 +1,77 @@
import React from "react";
import { useSelector } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import Markdown from "react-markdown";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { RootState } from "#/store";
import { Cell } from "#/state/jupyterSlice";
interface IJupyterCell {
cell: Cell;
}
function JupyterCell({ cell }: IJupyterCell): JSX.Element {
const code = cell.content;
if (cell.type === "input") {
return (
<div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs">
<div className="mb-1 text-gray-400">EXECUTE</div>
<pre
className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5"
style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }}
>
<SyntaxHighlighter language="python" style={atomOneDark}>
{code}
</SyntaxHighlighter>
</pre>
</div>
);
}
return (
<div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs">
<div className="mb-1 text-gray-400">STDOUT/STDERR</div>
<pre
className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5 max-h-[60vh] bg-gray-800"
style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }}
>
{/* split code by newline and render each line as a plaintext, except it starts with `![image]` so we render it as markdown */}
{code.split("\n").map((line, index) => {
if (line.startsWith("![image](data:image/png;base64,")) {
// add new line before and after the image
return (
<div key={index}>
<Markdown urlTransform={(value: string) => value}>
{line}
</Markdown>
<br />
</div>
);
}
return (
<div key={index}>
<SyntaxHighlighter language="plaintext" style={atomOneDark}>
{line}
</SyntaxHighlighter>
<br />
</div>
);
})}
</pre>
</div>
);
}
function Jupyter(): JSX.Element {
const { cells } = useSelector((state: RootState) => state.jupyter);
return (
<div className="flex-1 overflow-y-auto flex flex-col">
{cells.map((cell, index) => (
<JupyterCell key={index} cell={cell} />
))}
</div>
);
}
export default Jupyter;
+8 -2
View File
@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
FaCheckCircle,
FaQuestionCircle,
@@ -7,7 +8,9 @@ import {
FaRegClock,
FaRegTimesCircle,
} from "react-icons/fa";
import { VscListOrdered } from "react-icons/vsc";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { Plan, Task, TaskState } from "#/services/planService";
import { RootState } from "#/store";
@@ -55,10 +58,13 @@ interface PlanProps {
}
function PlanContainer({ plan }: PlanProps): JSX.Element {
const { t } = useTranslation();
if (plan.mainGoal === undefined) {
return (
<div className="p-2">
Nothing is currently planned. Start a task for this to change.
<div className="w-full h-full flex flex-col text-neutral-400 items-center justify-center">
<VscListOrdered size={100} />
{t(I18nKey.PLANNER$EMPTY_MESSAGE)}
</div>
);
}
+16 -1
View File
@@ -12,6 +12,7 @@ import { AllTabs, TabOption, TabType } from "#/types/TabOption";
import Browser from "./Browser";
import CodeEditor from "./CodeEditor";
import Planner from "./Planner";
import Jupyter from "./Jupyter";
function Workspace() {
const { t } = useTranslation();
@@ -20,12 +21,13 @@ function Workspace() {
const screenshotSrc = useSelector(
(state: RootState) => state.browser.screenshotSrc,
);
const jupyterCells = useSelector((state: RootState) => state.jupyter.cells);
const [activeTab, setActiveTab] = useState<TabType>(TabOption.CODE);
const [changes, setChanges] = useState<Record<TabType, boolean>>({
[TabOption.PLANNER]: false,
[TabOption.CODE]: false,
[TabOption.BROWSER]: false,
[TabOption.JUPYTER]: false,
});
const tabData = useMemo(
@@ -45,6 +47,11 @@ function Workspace() {
icon: <IoIosGlobe size={18} />,
component: <Browser key="browser" />,
},
[TabOption.JUPYTER]: {
name: t(I18nKey.WORKSPACE$JUPYTER_TAB_LABEL),
icon: <VscCode size={18} />,
component: <Jupyter key="jupyter" />,
},
}),
[t],
);
@@ -73,6 +80,14 @@ function Workspace() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenshotSrc]);
useEffect(() => {
if (activeTab !== TabOption.JUPYTER && jupyterCells.length > 0) {
// FIXME: This is a temporary solution to show the jupyter tab when the first cell is added
// Only need to show the tab only when a cell is added
setChanges((prev) => ({ ...prev, [TabOption.JUPYTER]: true }));
}
}, [jupyterCells]);
return (
<div className="flex flex-col min-h-0 grow">
<div
@@ -3,15 +3,17 @@ import {
IoIosArrowBack,
IoIosArrowForward,
IoIosRefresh,
IoIosCloudUpload,
} from "react-icons/io";
import { twMerge } from "tailwind-merge";
import { WorkspaceFile, getWorkspace } from "#/services/fileService";
import { WorkspaceFile, getWorkspace, uploadFile } from "#/services/fileService";
import IconButton from "../IconButton";
import ExplorerTree from "./ExplorerTree";
import { removeEmptyNodes } from "./utils";
interface ExplorerActionsProps {
onRefresh: () => void;
onUpload: () => void;
toggleHidden: () => void;
isHidden: boolean;
}
@@ -19,6 +21,7 @@ interface ExplorerActionsProps {
function ExplorerActions({
toggleHidden,
onRefresh,
onUpload,
isHidden,
}: ExplorerActionsProps) {
return (
@@ -29,17 +32,30 @@ function ExplorerActions({
)}
>
{!isHidden && (
<IconButton
icon={
<IoIosRefresh
size={16}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
}
testId="refresh"
ariaLabel="Refresh workspace"
onClick={onRefresh}
/>
<>
<IconButton
icon={
<IoIosRefresh
size={16}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
}
testId="refresh"
ariaLabel="Refresh workspace"
onClick={onRefresh}
/>
<IconButton
icon={
<IoIosCloudUpload
size={16}
className="text-neutral-400 hover:text-neutral-100 transition"
/>
}
testId="upload"
ariaLabel="Upload File"
onClick={onUpload}
/>
</>
)}
<IconButton
@@ -56,8 +72,8 @@ function ExplorerActions({
/>
)
}
testId="close"
ariaLabel="Close workspace"
testId="toggle"
ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
onClick={toggleHidden}
/>
</div>
@@ -71,12 +87,33 @@ interface FileExplorerProps {
function FileExplorer({ onFileClick }: FileExplorerProps) {
const [workspace, setWorkspace] = React.useState<WorkspaceFile>();
const [isHidden, setIsHidden] = React.useState(false);
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
const getWorkspaceData = async () => {
const wsFile = await getWorkspace();
setWorkspace(removeEmptyNodes(wsFile));
};
const selectFileInput = () => {
fileInputRef.current?.click(); // Trigger the file browser
};
const uploadFileData = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files ? event.target.files[0] : null;
if (!file) {
console.log("No file selected.");
return;
}
console.log("File selected:", file);
try {
const response = await uploadFile(file);
console.log(response);
await getWorkspaceData(); // Refresh the workspace to show the new file
} catch (error) {
console.error("Error uploading file:", error);
}
};
React.useEffect(() => {
(async () => {
await getWorkspaceData();
@@ -105,8 +142,15 @@ function FileExplorer({ onFileClick }: FileExplorerProps) {
isHidden={isHidden}
toggleHidden={() => setIsHidden((prev) => !prev)}
onRefresh={getWorkspaceData}
onUpload={selectFileInput}
/>
</div>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={uploadFileData}
/>
</div>
);
}
@@ -3,7 +3,7 @@ import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LoadPreviousSessionModal from "./LoadPreviousSessionModal";
import { clearMsgs, fetchMsgs } from "../../../services/session";
import { sendChatMessageFromEvent } from "../../../services/chatService";
import { addChatMessageFromEvent } from "../../../services/chatService";
import { handleAssistantMessage } from "../../../services/actions";
import toast from "../../../utils/toast";
@@ -37,7 +37,7 @@ vi.mock("../../../services/session", async (importOriginal) => ({
vi.mock("../../../services/chatService", async (importOriginal) => ({
...(await importOriginal<typeof import("../../../services/chatService")>()),
sendChatMessageFromEvent: vi.fn(),
addChatMessageFromEvent: vi.fn(),
}));
vi.mock("../../../services/actions", async (importOriginal) => ({
@@ -94,7 +94,7 @@ describe("LoadPreviousSession", () => {
await waitFor(() => {
expect(fetchMsgs).toHaveBeenCalledTimes(1);
expect(sendChatMessageFromEvent).toHaveBeenCalledTimes(1);
expect(addChatMessageFromEvent).toHaveBeenCalledTimes(1);
expect(handleAssistantMessage).toHaveBeenCalledTimes(1);
});
// modal should close right after fetching messages
@@ -117,7 +117,7 @@ describe("LoadPreviousSession", () => {
await waitFor(async () => {
await expect(() => fetchMsgs()).rejects.toThrow();
expect(handleAssistantMessage).not.toHaveBeenCalled();
expect(sendChatMessageFromEvent).not.toHaveBeenCalled();
expect(addChatMessageFromEvent).not.toHaveBeenCalled();
// error toast should be shown
expect(toast.stickyError).toHaveBeenCalledWith(
"ws",
@@ -2,7 +2,7 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { handleAssistantMessage } from "#/services/actions";
import { sendChatMessageFromEvent } from "#/services/chatService";
import { addChatMessageFromEvent } from "#/services/chatService";
import { clearMsgs, fetchMsgs } from "#/services/session";
import toast from "#/utils/toast";
import BaseModal from "../base-modal/BaseModal";
@@ -28,7 +28,7 @@ function LoadPreviousSessionModal({
messages.forEach((message) => {
if (message.role === "user") {
sendChatMessageFromEvent(message.payload);
addChatMessageFromEvent(message.payload);
}
if (message.role === "assistant") {
@@ -1,9 +1,9 @@
import { Settings } from "#/services/settings";
import AgentTaskState from "#/types/AgentTaskState";
import { act, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { renderWithProviders } from "test-utils";
import AgentTaskState from "#/types/AgentTaskState";
import { Settings } from "#/services/settings";
import SettingsForm from "./SettingsForm";
const onModelChangeMock = vi.fn();
@@ -106,7 +106,6 @@ describe("SettingsForm", () => {
});
expect(onModelChangeMock).toHaveBeenCalledWith("model3");
expect(onAPIKeyChangeMock).toHaveBeenCalledWith("");
});
it("should call the onAgentChange handler when the agent changes", () => {
@@ -38,7 +38,8 @@ function SettingsForm({
useEffect(() => {
if (
curTaskState === AgentTaskState.RUNNING ||
curTaskState === AgentTaskState.PAUSED
curTaskState === AgentTaskState.PAUSED ||
curTaskState === AgentTaskState.AWAITING_USER_INPUT
) {
setDisabled(true);
} else {
@@ -1,13 +1,13 @@
import { fetchAgents, fetchModels } from "#/api";
import { initializeAgent } from "#/services/agent";
import { Settings, getSettings, saveSettings } from "#/services/settings";
import toast from "#/utils/toast";
import { act, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import i18next from "i18next";
import React from "react";
import { renderWithProviders } from "test-utils";
import { Mock } from "vitest";
import toast from "#/utils/toast";
import { Settings, getSettings, saveSettings } from "#/services/settings";
import { initializeAgent } from "#/services/agent";
import { fetchAgents, fetchModels } from "#/api";
import SettingsModal from "./SettingsModal";
const toastSpy = vi.spyOn(toast, "settingsChanged");
@@ -129,6 +129,7 @@ describe("SettingsModal", () => {
expect(saveSettings).toHaveBeenCalledWith({
...initialSettings,
LLM_MODEL: "model3",
LLM_API_KEY: "", // reset after model change
});
});
@@ -160,6 +161,7 @@ describe("SettingsModal", () => {
expect(initializeAgent).toHaveBeenCalledWith({
...initialSettings,
LLM_MODEL: "model3",
LLM_API_KEY: "", // reset after model change
});
});
@@ -1,3 +1,7 @@
import { Spinner } from "@nextui-org/react";
import i18next from "i18next";
import React from "react";
import { useTranslation } from "react-i18next";
import { fetchAgents, fetchModels } from "#/api";
import { AvailableLanguages } from "#/i18n";
import { I18nKey } from "#/i18n/declaration";
@@ -9,10 +13,6 @@ import {
saveSettings,
} from "#/services/settings";
import toast from "#/utils/toast";
import { Spinner } from "@nextui-org/react";
import i18next from "i18next";
import React from "react";
import { useTranslation } from "react-i18next";
import BaseModal from "../base-modal/BaseModal";
import SettingsForm from "./SettingsForm";
@@ -76,8 +76,14 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
i18next.changeLanguage(settings.LANGUAGE);
initializeAgent(settings); // reinitialize the agent with the new settings
const sensitiveKeys = ['LLM_API_KEY'];
Object.entries(updatedSettings).forEach(([key, value]) => {
toast.settingsChanged(`${key} set to "${value}"`);
if (!sensitiveKeys.includes(key)) {
toast.settingsChanged(`${key} set to "${value}"`);
} else {
toast.settingsChanged(`${key} has been updated securely.`);
}
});
localStorage.setItem(
@@ -86,6 +92,13 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
);
};
const isDisabled =
Object.entries(settings)
// filter api key
.filter(([key]) => key !== "LLM_API_KEY")
.some(([, value]) => !value) ||
JSON.stringify(settings) === JSON.stringify(currentSettings);
return (
<BaseModal
isOpen={isOpen}
@@ -96,9 +109,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
{
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
action: handleSaveSettings,
isDisabled:
Object.values(settings).some((value) => !value) ||
JSON.stringify(settings) === JSON.stringify(currentSettings),
isDisabled,
closeAfterAction: true,
className: "bg-primary rounded-lg",
},
+25
View File
@@ -39,6 +39,19 @@
"pt": "Planejador",
"es": "Planificador"
},
"WORKSPACE$JUPYTER_TAB_LABEL": {
"en": "Jupyter IPython",
"zh-CN": "Jupyter IPython",
"de": "Jupyter IPython",
"ko-KR": "Jupyter IPython",
"no": "Jupyter IPython",
"zh-TW": "Jupyter IPython",
"ar": "Jupyter IPython",
"fr": "Jupyter IPython",
"it": "Jupyter IPython",
"pt": "Jupyter IPython",
"es": "Jupyter IPython"
},
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
"en": "Code Editor",
"zh-CN": "代码编辑器",
@@ -328,5 +341,17 @@
"SETTINGS$API_KEY_PLACEHOLDER": {
"en": "Enter your API key.",
"de": "Model API key."
},
"CODE_EDITOR$EMPTY_MESSAGE": {
"en": "No file selected.",
"de": "Keine Datei ausgewählt."
},
"BROWSER$EMPTY_MESSAGE": {
"en": "No page loaded.",
"de": "Keine Seite geladen."
},
"PLANNER$EMPTY_MESSAGE": {
"en": "No plan created.",
"de": "Kein Plan erstellt."
}
}
+13
View File
@@ -3,6 +3,7 @@ import { setScreenshotSrc, setUrl } from "#/state/browserSlice";
import { appendAssistantMessage } from "#/state/chatSlice";
import { setCode, updatePath } from "#/state/codeSlice";
import { appendInput } from "#/state/commandSlice";
import { appendJupyterInput } from "#/state/jupyterSlice";
import { setPlan } from "#/state/planSlice";
import { setInitialized } from "#/state/taskSlice";
import store from "#/store";
@@ -29,12 +30,24 @@ const messageActions = {
[ActionType.THINK]: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.args.thought));
},
[ActionType.TALK]: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.args.content));
},
[ActionType.FINISH]: (message: ActionMessage) => {
store.dispatch(appendAssistantMessage(message.message));
},
[ActionType.RUN]: (message: ActionMessage) => {
if (message.args.thought) {
store.dispatch(appendAssistantMessage(message.args.thought));
}
store.dispatch(appendInput(message.args.command));
},
[ActionType.RUN_IPYTHON]: (message: ActionMessage) => {
if (message.args.thought) {
store.dispatch(appendAssistantMessage(message.args.thought));
}
store.dispatch(appendJupyterInput(message.args.code));
},
[ActionType.ADD_TASK]: () => {
getPlan().then((fetchedPlan) => store.dispatch(setPlan(fetchedPlan)));
},
+8 -3
View File
@@ -11,14 +11,19 @@ import { SocketMessage } from "#/types/ResponseType";
import { ActionMessage } from "#/types/Message";
import Socket from "./socket";
export function sendChatMessage(message: string): void {
export function sendChatMessage(message: string, isTask: boolean = true): void {
store.dispatch(appendUserMessage(message));
const event = { action: ActionType.START, args: { task: message } };
let event;
if (isTask) {
event = { action: ActionType.START, args: { task: message } };
} else {
event = { action: ActionType.USER_MESSAGE, args: { message } };
}
const eventString = JSON.stringify(event);
Socket.send(eventString);
}
export function sendChatMessageFromEvent(event: string | SocketMessage): void {
export function addChatMessageFromEvent(event: string | SocketMessage): void {
try {
let data: ActionMessage;
if (typeof event === "string") {
+18
View File
@@ -12,6 +12,24 @@ export async function selectFile(file: string): Promise<string> {
return data.code as string;
}
export async function uploadFile(file: File): Promise<string> {
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload-file", {
method: "POST",
body: formData,
});
const data = await res.json();
if (res.status !== 200) {
throw new Error(data.error || "Failed to upload file.");
}
return `File uploaded: ${data.filename}, Location: ${data.location}`;
}
export async function getWorkspace(): Promise<WorkspaceFile> {
const res = await fetch("/api/refresh-files");
const data = await res.json();
+5
View File
@@ -3,6 +3,7 @@ import { setUrl, setScreenshotSrc } from "#/state/browserSlice";
import store from "#/store";
import { ObservationMessage } from "#/types/Message";
import { appendOutput } from "#/state/commandSlice";
import { appendJupyterOutput } from "#/state/jupyterSlice";
import ObservationType from "#/types/ObservationType";
export function handleObservationMessage(message: ObservationMessage) {
@@ -10,6 +11,10 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.RUN:
store.dispatch(appendOutput(message.content));
break;
case ObservationType.RUN_IPYTHON:
// FIXME: render this as markdown
store.dispatch(appendJupyterOutput(message.content));
break;
case ObservationType.BROWSE:
if (message.extras?.screenshot) {
store.dispatch(setScreenshotSrc(message.extras.screenshot));
+12 -2
View File
@@ -1,6 +1,7 @@
import { describe, expect, it, vi, Mock } from "vitest";
import {
DEFAULT_SETTINGS,
Settings,
getSettings,
getSettingsDifference,
saveSettings,
@@ -18,7 +19,8 @@ describe("getSettings", () => {
(localStorage.getItem as Mock)
.mockReturnValueOnce("llm_value")
.mockReturnValueOnce("agent_value")
.mockReturnValueOnce("language_value");
.mockReturnValueOnce("language_value")
.mockReturnValueOnce("api_key");
const settings = getSettings();
@@ -26,11 +28,13 @@ describe("getSettings", () => {
LLM_MODEL: "llm_value",
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "api_key",
});
});
it("should handle return defaults if localStorage key does not exist", () => {
(localStorage.getItem as Mock)
.mockReturnValueOnce(null)
.mockReturnValueOnce(null)
.mockReturnValueOnce(null)
.mockReturnValueOnce(null);
@@ -41,16 +45,18 @@ describe("getSettings", () => {
LLM_MODEL: DEFAULT_SETTINGS.LLM_MODEL,
AGENT: DEFAULT_SETTINGS.AGENT,
LANGUAGE: DEFAULT_SETTINGS.LANGUAGE,
LLM_API_KEY: "",
});
});
});
describe("saveSettings", () => {
it("should save the settings", () => {
const settings = {
const settings: Settings = {
LLM_MODEL: "llm_value",
AGENT: "agent_value",
LANGUAGE: "language_value",
LLM_API_KEY: "some_key",
};
saveSettings(settings);
@@ -61,6 +67,10 @@ describe("saveSettings", () => {
"LANGUAGE",
"language_value",
);
expect(localStorage.setItem).toHaveBeenCalledWith(
"LLM_API_KEY",
"some_key",
);
});
it("should save partial settings", () => {
+13 -9
View File
@@ -17,15 +17,19 @@ const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[];
/**
* Get the settings from local storage or use the default settings if not found
*/
export const getSettings = (): Settings => ({
LLM_MODEL: localStorage.getItem("LLM_MODEL") || DEFAULT_SETTINGS.LLM_MODEL,
AGENT: localStorage.getItem("AGENT") || DEFAULT_SETTINGS.AGENT,
LANGUAGE: localStorage.getItem("LANGUAGE") || DEFAULT_SETTINGS.LANGUAGE,
LLM_API_KEY:
localStorage.getItem(
`API_KEY_${localStorage.getItem("LLM_MODEL") || DEFAULT_SETTINGS.LLM_MODEL}`,
) || DEFAULT_SETTINGS.LLM_API_KEY,
});
export const getSettings = (): Settings => {
const model = localStorage.getItem("LLM_MODEL");
const agent = localStorage.getItem("AGENT");
const language = localStorage.getItem("LANGUAGE");
const apiKey = localStorage.getItem(`API_KEY_${model}`);
return {
LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL,
AGENT: agent || DEFAULT_SETTINGS.AGENT,
LANGUAGE: language || DEFAULT_SETTINGS.LANGUAGE,
LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY,
};
};
/**
* Save the settings to local storage. Only valid settings are saved.
+27
View File
@@ -0,0 +1,27 @@
import { createSlice } from "@reduxjs/toolkit";
export type Cell = {
content: string;
type: "input" | "output";
};
const initialCells: Cell[] = [];
export const cellSlice = createSlice({
name: "cell",
initialState: {
cells: initialCells,
},
reducers: {
appendJupyterInput: (state, action) => {
state.cells.push({ content: action.payload, type: "input" });
},
appendJupyterOutput: (state, action) => {
state.cells.push({ content: action.payload, type: "output" });
},
},
});
export const { appendJupyterInput, appendJupyterOutput } = cellSlice.actions;
export default cellSlice.reducer;
+2
View File
@@ -7,6 +7,7 @@ import commandReducer from "./state/commandSlice";
import errorsReducer from "./state/errorsSlice";
import planReducer from "./state/planSlice";
import taskReducer from "./state/taskSlice";
import jupyterReducer from "./state/jupyterSlice";
export const rootReducer = combineReducers({
browser: browserReducer,
@@ -17,6 +18,7 @@ export const rootReducer = combineReducers({
errors: errorsReducer,
plan: planReducer,
agent: agentReducer,
jupyter: jupyterReducer,
});
const store = configureStore({
+10 -1
View File
@@ -2,7 +2,10 @@ enum ActionType {
// Initializes the agent. Only sent by client.
INIT = "initialize",
// Starts a new development task. Only sent by the client.
// Sends a message from the user
USER_MESSAGE = "user_message",
// Starts a new development task
START = "start",
// Reads the contents of a file.
@@ -14,6 +17,9 @@ enum ActionType {
// Runs a command.
RUN = "run",
// Runs a IPython command.
RUN_IPYTHON = "run_ipython",
// Kills a background command.
KILL = "kill",
@@ -26,6 +32,9 @@ enum ActionType {
// Allows the agent to make a plan, set a goal, or record thoughts.
THINK = "think",
// Allows the agent to respond to the user. Only sent by the agent.
TALK = "talk",
// If you're absolutely certain that you've completed your task and have tested your work,
// use the finish action to stop working.
FINISH = "finish",
+1
View File
@@ -1,6 +1,7 @@
enum AgentTaskState {
INIT = "init",
RUNNING = "running",
AWAITING_USER_INPUT = "awaiting_user_input",
PAUSED = "paused",
STOPPED = "stopped",
FINISHED = "finished",
+3
View File
@@ -8,6 +8,9 @@ enum ObservationType {
// The output of a command
RUN = "run",
// The output of an IPython command
RUN_IPYTHON = "run_ipython",
// The result of a search
RECALL = "recall",
+12 -2
View File
@@ -2,10 +2,20 @@ enum TabOption {
PLANNER = "planner",
CODE = "code",
BROWSER = "browser",
JUPYTER = "jupyter",
}
type TabType = TabOption.PLANNER | TabOption.CODE | TabOption.BROWSER;
type TabType =
| TabOption.PLANNER
| TabOption.CODE
| TabOption.BROWSER
| TabOption.JUPYTER;
const AllTabs = [TabOption.CODE, TabOption.BROWSER, TabOption.PLANNER];
const AllTabs = [
TabOption.CODE,
TabOption.BROWSER,
TabOption.PLANNER,
TabOption.JUPYTER,
];
export { AllTabs, TabOption, type TabType };
+6 -1
View File
@@ -5,10 +5,11 @@ from .agent import (
AgentFinishAction,
AgentRecallAction,
AgentSummarizeAction,
AgentTalkAction,
AgentThinkAction,
)
from .base import Action, NullAction
from .bash import CmdKillAction, CmdRunAction
from .bash import CmdKillAction, CmdRunAction, IPythonRunCellAction
from .browse import BrowseURLAction
from .fileop import FileReadAction, FileWriteAction
from .github import GitHubPushAction
@@ -17,11 +18,13 @@ from .tasks import AddTaskAction, ModifyTaskAction
actions = (
CmdKillAction,
CmdRunAction,
IPythonRunCellAction,
BrowseURLAction,
FileReadAction,
FileWriteAction,
AgentRecallAction,
AgentThinkAction,
AgentTalkAction,
AgentFinishAction,
AgentDelegateAction,
AddTaskAction,
@@ -61,10 +64,12 @@ __all__ = [
'FileWriteAction',
'AgentRecallAction',
'AgentThinkAction',
'AgentTalkAction',
'AgentFinishAction',
'AgentDelegateAction',
'AgentEchoAction',
'AgentSummarizeAction',
'AddTaskAction',
'ModifyTaskAction',
'IPythonRunCellAction'
]
+20 -1
View File
@@ -18,11 +18,12 @@ if TYPE_CHECKING:
@dataclass
class AgentRecallAction(ExecutableAction):
query: str
thought: str = ''
action: str = ActionType.RECALL
async def run(self, controller: 'AgentController') -> AgentRecallObservation:
return AgentRecallObservation(
content='Recalling memories...',
content='',
memories=controller.agent.search_memory(self.query),
)
@@ -44,6 +45,22 @@ class AgentThinkAction(NotExecutableAction):
return self.thought
@dataclass
class AgentTalkAction(NotExecutableAction):
content: str
action: str = ActionType.TALK
async def run(self, controller: 'AgentController') -> 'Observation':
raise NotImplementedError
@property
def message(self) -> str:
return self.content
def __str__(self) -> str:
return self.content
@dataclass
class AgentEchoAction(ExecutableAction):
content: str
@@ -70,6 +87,7 @@ class AgentSummarizeAction(NotExecutableAction):
@dataclass
class AgentFinishAction(NotExecutableAction):
outputs: Dict = field(default_factory=dict)
thought: str = ''
action: str = ActionType.FINISH
async def run(self, controller: 'AgentController') -> 'Observation':
@@ -84,6 +102,7 @@ class AgentFinishAction(NotExecutableAction):
class AgentDelegateAction(ExecutableAction):
agent: str
inputs: dict
thought: str = ''
action: str = ActionType.DELEGATE
async def run(self, controller: 'AgentController') -> 'Observation':
+60 -1
View File
@@ -1,7 +1,10 @@
import os
import pathlib
from dataclasses import dataclass
from typing import TYPE_CHECKING
from opendevin.schema import ActionType
from opendevin import config
from opendevin.schema import ActionType, ConfigType
from .base import ExecutableAction
@@ -9,11 +12,14 @@ if TYPE_CHECKING:
from opendevin.controller import AgentController
from opendevin.observation import CmdOutputObservation, Observation
from opendevin.observation import IPythonRunCellObservation
@dataclass
class CmdRunAction(ExecutableAction):
command: str
background: bool = False
thought: str = ''
action: str = ActionType.RUN
async def run(self, controller: 'AgentController') -> 'Observation':
@@ -23,10 +29,18 @@ class CmdRunAction(ExecutableAction):
def message(self) -> str:
return f'Running command: {self.command}'
def __str__(self) -> str:
ret = '**CmdRunAction**\n'
if self.thought:
ret += f'THOUGHT:{self.thought}\n'
ret += f'COMMAND:\n{self.command}'
return ret
@dataclass
class CmdKillAction(ExecutableAction):
id: int
thought: str = ''
action: str = ActionType.KILL
async def run(self, controller: 'AgentController') -> 'CmdOutputObservation':
@@ -35,3 +49,48 @@ class CmdKillAction(ExecutableAction):
@property
def message(self) -> str:
return f'Killing command: {self.id}'
def __str__(self) -> str:
return f'**CmdKillAction**\n{self.id}'
@dataclass
class IPythonRunCellAction(ExecutableAction):
code: str
thought: str = ''
action: str = ActionType.RUN_IPYTHON
async def run(self, controller: 'AgentController') -> 'IPythonRunCellObservation':
# echo "import math" | execute_cli
# write code to a temporary file and pass it to `execute_cli` via stdin
tmp_filepath = os.path.join(
config.get(ConfigType.WORKSPACE_BASE),
'.tmp', '.ipython_execution_tmp.py'
)
pathlib.Path(os.path.dirname(tmp_filepath)).mkdir(parents=True, exist_ok=True)
with open(tmp_filepath, 'w') as tmp_file:
tmp_file.write(self.code)
tmp_filepath_inside_sandbox = os.path.join(
config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX),
'.tmp', '.ipython_execution_tmp.py'
)
obs = controller.action_manager.run_command(
f'execute_cli < {tmp_filepath_inside_sandbox}',
background=False
)
return IPythonRunCellObservation(
content=obs.content,
code=self.code
)
def __str__(self) -> str:
ret = '**IPythonRunCellAction**\n'
if self.thought:
ret += f'THOUGHT:{self.thought}\n'
ret += f'CODE:\n{self.code}'
return ret
@property
def message(self) -> str:
return f'Running Python code interactively: {self.code}'
+1
View File
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
@dataclass
class BrowseURLAction(ExecutableAction):
url: str
thought: str = ''
action: str = ActionType.BROWSE
async def run(self, controller: 'AgentController') -> BrowserOutputObservation: # type: ignore
+2 -2
View File
@@ -50,7 +50,7 @@ class FileReadAction(ExecutableAction):
path: str
start: int = 0
end: int = -1
thoughts: str = ''
thought: str = ''
action: str = ActionType.READ
def _read_lines(self, all_lines: list[str]):
@@ -100,7 +100,7 @@ class FileWriteAction(ExecutableAction):
content: str
start: int = 0
end: int = -1
thoughts: str = ''
thought: str = ''
action: str = ActionType.WRITE
def _insert_lines(self, to_insert: list[str], original: list[str]):
+3
View File
@@ -15,6 +15,7 @@ class AddTaskAction(ExecutableAction):
parent: str
goal: str
subtasks: list = field(default_factory=list)
thought: str = ''
action: str = ActionType.ADD_TASK
async def run(self, controller: 'AgentController') -> NullObservation: # type: ignore
@@ -31,6 +32,7 @@ class AddTaskAction(ExecutableAction):
class ModifyTaskAction(ExecutableAction):
id: str
state: str
thought: str = ''
action: str = ActionType.MODIFY_TASK
async def run(self, controller: 'AgentController') -> NullObservation: # type: ignore
@@ -47,6 +49,7 @@ class ModifyTaskAction(ExecutableAction):
class TaskStateChangedAction(NotExecutableAction):
"""Fake action, just to notify the client that a task state has changed."""
task_state: str
thought: str = ''
action: str = ActionType.CHANGE_TASK_STATE
@property
+3 -2
View File
@@ -52,8 +52,10 @@ DEFAULT_CONFIG: dict = {
ConfigType.USE_HOST_NETWORK: 'false',
ConfigType.SSH_HOSTNAME: 'localhost',
ConfigType.DISABLE_COLOR: 'false',
ConfigType.SANDBOX_USER_ID: os.getuid() if hasattr(os, 'getuid') else None,
ConfigType.SANDBOX_TIMEOUT: 120,
ConfigType.GITHUB_TOKEN: None
ConfigType.GITHUB_TOKEN: None,
ConfigType.SANDBOX_USER_ID: None
}
config_str = ''
@@ -169,7 +171,6 @@ def finalize_config():
if config.get(ConfigType.WORKSPACE_MOUNT_PATH) is None:
config[ConfigType.WORKSPACE_MOUNT_PATH] = config.get(ConfigType.WORKSPACE_BASE)
finalize_config()
+49 -1
View File
@@ -1,11 +1,13 @@
import asyncio
from typing import Callable, List, Type
from agenthub.codeact_agent.codeact_agent import CodeActAgent
from opendevin import config
from opendevin.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
AgentTalkAction,
NullAction,
)
from opendevin.action.tasks import TaskStateChangedAction
@@ -23,8 +25,10 @@ from opendevin.observation import (
AgentErrorObservation,
NullObservation,
Observation,
UserMessageObservation,
)
from opendevin.plan import Plan
from opendevin.sandbox import DockerSSHBox
from opendevin.schema import TaskState
from opendevin.schema.config import ConfigType
from opendevin.state import State
@@ -64,6 +68,11 @@ class AgentController:
# Initialize agent-required plugins for sandbox (if any)
self.action_manager.init_sandbox_plugins(agent.sandbox_plugins)
if isinstance(agent, CodeActAgent) and not isinstance(self.action_manager.sandbox, DockerSSHBox):
logger.warning('CodeActAgent requires DockerSSHBox as sandbox! Using other sandbox that are not stateful (LocalBox, DockerExecBox) will not work properly.')
self._await_user_message_queue: asyncio.Queue = asyncio.Queue()
def update_state_for_step(self, i):
if self.state is None:
return
@@ -174,6 +183,36 @@ class AgentController:
async def notify_task_state_changed(self):
await self._run_callbacks(TaskStateChangedAction(self._task_state))
async def add_user_message(self, message: UserMessageObservation):
if self.state is None:
return
if self._task_state == TaskState.AWAITING_USER_INPUT:
self._await_user_message_queue.put_nowait(message)
# set the task state to running
self._task_state = TaskState.RUNNING
await self.notify_task_state_changed()
elif self._task_state == TaskState.RUNNING:
self.add_history(NullAction(), message)
else:
raise ValueError(f'Task (state: {self._task_state}) is not in a state to add user message')
async def wait_for_user_input(self) -> UserMessageObservation:
self._task_state = TaskState.AWAITING_USER_INPUT
await self.notify_task_state_changed()
# wait for the next user message
if len(self.callbacks) == 0:
logger.info('Use STDIN to request user message as no callbacks are registered', extra={'msg_type': 'INFO'})
message = input('Request user input [type /exit to stop interaction] >> ')
user_message_observation = UserMessageObservation(message)
else:
user_message_observation = await self._await_user_message_queue.get()
self._await_user_message_queue.task_done()
return user_message_observation
async def start_delegate(self, action: AgentDelegateAction):
AgentCls: Type[Agent] = Agent.get_cls(action.agent)
agent = AgentCls(llm=self.agent.llm)
@@ -201,7 +240,8 @@ class AgentController:
return False
logger.info(f'STEP {i}', extra={'msg_type': 'STEP'})
logger.info(self.state.plan.main_goal, extra={'msg_type': 'PLAN'})
if i == 0:
logger.info(self.state.plan.main_goal, extra={'msg_type': 'PLAN'})
if self.state.num_of_chars > self.max_chars:
raise MaxCharsExceedError(self.state.num_of_chars, self.max_chars)
@@ -226,6 +266,14 @@ class AgentController:
await self._run_callbacks(action)
# whether to await for user messages
if isinstance(action, AgentTalkAction):
# await for the next user messages
user_message_observation = await self.wait_for_user_input()
logger.info(user_message_observation, extra={'msg_type': 'OBSERVATION'})
self.add_history(action, user_message_observation)
return False
finished = isinstance(action, AgentFinishAction)
if finished:
self.state.outputs = action.outputs # type: ignore[attr-defined]
+2 -1
View File
@@ -5,7 +5,7 @@ from .error import AgentErrorObservation
from .files import FileReadObservation, FileWriteObservation
from .message import AgentMessageObservation, UserMessageObservation
from .recall import AgentRecallObservation
from .run import CmdOutputObservation
from .run import CmdOutputObservation, IPythonRunCellObservation
observations = (
CmdOutputObservation,
@@ -40,6 +40,7 @@ __all__ = [
'Observation',
'NullObservation',
'CmdOutputObservation',
'IPythonRunCellObservation',
'BrowserOutputObservation',
'FileReadObservation',
'FileWriteObservation',
+18
View File
@@ -23,3 +23,21 @@ class CmdOutputObservation(Observation):
@property
def message(self) -> str:
return f'Command `{self.command}` executed with exit code {self.exit_code}.'
@dataclass
class IPythonRunCellObservation(Observation):
"""
This data class represents the output of a IPythonRunCellAction.
"""
code: str
observation: str = ObservationType.RUN_IPYTHON
@property
def error(self) -> bool:
return False # IPython cells do not return exit codes
@property
def message(self) -> str:
return 'Coded executed in IPython cell.'
+4 -1
View File
@@ -122,7 +122,10 @@ class DockerExecBox(Sandbox):
self.container.exec_run(
f'kill -9 {pid}', workdir=SANDBOX_WORKSPACE_DIR)
return -1, f'Command: "{cmd}" timed out'
return exit_code, logs.decode('utf-8').strip()
logs_out = logs.decode('utf-8')
if logs_out.endswith('\n'):
logs_out = logs_out[:-1]
return exit_code, logs_out
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
# mkdir -p sandbox_dest if it doesn't exist
+35 -11
View File
@@ -41,7 +41,6 @@ if SANDBOX_USER_ID := config.get(ConfigType.SANDBOX_USER_ID):
elif hasattr(os, 'getuid'):
USER_ID = os.getuid()
class DockerSSHBox(Sandbox):
instance_id: str
container_image: str
@@ -62,6 +61,7 @@ class DockerSSHBox(Sandbox):
timeout: int = 120,
sid: str | None = None,
):
logger.info(f'SSHBox is running as {"opendevin" if RUN_AS_DEVIN else "root"} user with USER_ID={USER_ID} in the sandbox')
# Initialize docker client. Throws an exception if Docker is not reachable.
try:
self.docker_client = docker.from_env()
@@ -150,8 +150,10 @@ class DockerSSHBox(Sandbox):
workdir=SANDBOX_WORKSPACE_DIR,
)
if exit_code != 0:
raise Exception(
f'Failed to chown workspace directory for opendevin in sandbox: {logs}')
# This is not a fatal error, just a warning
logger.warning(
f'Failed to chown workspace directory for opendevin in sandbox: {logs}. But this should be fine if the {SANDBOX_WORKSPACE_DIR=} is mounted by the app docker container.'
)
else:
exit_code, logs = self.container.exec_run(
# change password for root
@@ -169,7 +171,7 @@ class DockerSSHBox(Sandbox):
def start_ssh_session(self):
# start ssh session at the background
self.ssh = pxssh.pxssh(echo=False)
self.ssh = pxssh.pxssh()
hostname = SSH_HOSTNAME
if RUN_AS_DEVIN:
username = 'opendevin'
@@ -213,14 +215,36 @@ class DockerSSHBox(Sandbox):
# send a SIGINT to the process
self.ssh.sendintr()
self.ssh.prompt()
command_output = self.ssh.before.decode('utf-8').strip()
command_output = self.ssh.before.decode(
'utf-8').lstrip(cmd).strip()
return -1, f'Command: "{cmd}" timed out. Sending SIGINT to the process: {command_output}'
command_output = self.ssh.before.decode('utf-8').strip()
# 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(timeout=10)
exit_code = int(self.ssh.before.decode('utf-8').strip())
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())
return exit_code, command_output
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
@@ -305,9 +329,10 @@ class DockerSSHBox(Sandbox):
pass
def get_working_directory(self):
self.ssh.sendline('pwd')
self.ssh.prompt(timeout=10)
return self.ssh.before.decode('utf-8').strip()
exit_code, result = self.execute('pwd')
if exit_code != 0:
raise Exception('Failed to get working directory')
return result.strip()
def is_container_running(self):
try:
@@ -351,7 +376,6 @@ class DockerSSHBox(Sandbox):
**network_kwargs,
working_dir=SANDBOX_WORKSPACE_DIR,
name=self.container_name,
hostname='opendevin_sandbox',
detach=True,
volumes={
mount_dir: {
@@ -187,8 +187,7 @@ class JupyterKernel:
outputs.append(msg['content']['data']['text/plain'])
if 'image/png' in msg['content']['data']:
# use markdone to display image (in case of large image)
# outputs.append(f"\n<img src=\'data:image/png;base64,{msg['content']['data']['image/png']}\'/>\n")
outputs.append(f"![image](data:image/png;base64,{msg['content']['data']['image / png']})")
outputs.append(f"\n![image](data:image/png;base64,{msg['content']['data']['image/png']})\n")
elif msg_type == 'execute_reply':
execution_done = True
+13 -1
View File
@@ -10,8 +10,12 @@ class ActionTypeSchema(BaseModel):
"""Initializes the agent. Only sent by client.
"""
USER_MESSAGE: str = Field(default='user_message')
"""Sends a message from the user. Only sent by the client.
"""
START: str = Field(default='start')
"""Starts a new development task. Only sent by the client.
"""Starts a new development task OR send chat from the user. Only sent by the client.
"""
READ: str = Field(default='read')
@@ -26,6 +30,10 @@ class ActionTypeSchema(BaseModel):
"""Runs a command.
"""
RUN_IPYTHON: str = Field(default='run_ipython')
"""Runs a IPython cell.
"""
KILL: str = Field(default='kill')
"""Kills a background command.
"""
@@ -42,6 +50,10 @@ class ActionTypeSchema(BaseModel):
"""Allows the agent to make a plan, set a goal, or record thoughts
"""
TALK: str = Field(default='talk')
"""Allows the agent to respond to the user.
"""
DELEGATE: str = Field(default='delegate')
"""Delegates a task to another agent.
"""
+4
View File
@@ -20,6 +20,10 @@ class ObservationTypeSchema(BaseModel):
"""The output of a command
"""
RUN_IPYTHON: str = Field(default='run_ipython')
"""Runs a IPython cell.
"""
RECALL: str = Field(default='recall')
"""The result of a search
"""
+4
View File
@@ -10,6 +10,10 @@ class TaskState(str, Enum):
"""The task is running.
"""
AWAITING_USER_INPUT = 'awaiting_user_input'
"""The task is awaiting user input.
"""
PAUSED = 'paused'
"""The task is paused.
"""
+12 -6
View File
@@ -94,6 +94,8 @@ class AgentUnit:
await self.create_controller(data)
case ActionType.START:
await self.start_task(data)
case ActionType.USER_MESSAGE:
await self.send_user_message(data)
case ActionType.CHANGE_TASK_STATE:
task_state_action = data.get('args', {}).get('task_state_action', None)
if task_state_action is None:
@@ -177,23 +179,27 @@ class AgentUnit:
Args:
start_event: The start event data.
"""
if 'task' not in start_event['args']:
await self.send_error('No task specified')
return
await self.send_message('Starting new task...')
task = start_event['args']['task']
if self.controller is None:
await self.send_error('No agent started. Please wait a second...')
return
try:
if self.agent_task:
self.agent_task.cancel()
assert not self.agent_task, 'Agent task already running'
self.agent_task = asyncio.create_task(
self.controller.start(task), name='agent start task loop'
)
except Exception as e:
await self.send_error(f'Error during task loop: {e}')
async def send_user_message(self, data: dict):
if not self.agent_task or not self.controller:
await self.send_error('No agent started.')
return
await self.controller.add_user_message(
UserMessageObservation(data['args']['message'])
)
async def set_task_state(self, new_state_action: TaskStateAction):
"""Sets the state of the agent task."""
if self.controller is None:
+20 -1
View File
@@ -1,9 +1,10 @@
import json
import shutil
import uuid
from pathlib import Path
import litellm
from fastapi import Depends, FastAPI, Response, WebSocket, status
from fastapi import Depends, FastAPI, Response, UploadFile, WebSocket, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -137,6 +138,24 @@ def select_file(file: str):
return {'code': content}
@app.post('/api/upload-file')
async def upload_file(file: UploadFile):
try:
workspace_base = config.get(ConfigType.WORKSPACE_BASE)
file_path = Path(workspace_base, file.filename)
# The following will check if the file is within the workspace base and throw an exception if not
file_path.resolve().relative_to(Path(workspace_base).resolve())
with open(file_path, 'wb') as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
logger.error(f'Error saving file {file.filename}: {e}', exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': f'Error saving file: {e}'}
)
return {'filename': file.filename, 'location': str(file_path)}
@app.get('/api/plan')
def get_plan(
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
-6
View File
@@ -1,6 +0,0 @@
{
"name": "OpenDevin",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
Generated
+15 -1
View File
@@ -4446,6 +4446,20 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-multipart"
version = "0.0.9"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
]
[package.extras]
dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
[[package]]
name = "pytz"
version = "2024.1"
@@ -6329,4 +6343,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "62bc3f49304639795bc824fb0ceacbfaeb40c88ea9e5a90d13d4a3bf5219dbba"
content-hash = "4e50c6a8427ab71919c0c5bcdfbb83afe71aa50966452b23bca62e0b69da4c30"
+1
View File
@@ -26,6 +26,7 @@ playwright = "*"
e2b = "^0.14.13"
pexpect = "*"
jinja2 = "^3.1.3"
python-multipart = "*"
[tool.poetry.group.llama-index.dependencies]
llama-index = "*"
+3
View File
@@ -89,12 +89,15 @@ mkdir workspace
poetry run python ./opendevin/main.py -i 10 -t "Write a shell script 'hello.sh' that prints 'hello'." -c "MonologueAgent" -d "./workspace"
```
**NOTE**: If your agent decide to support user-agent interaction via natural language (e.g., you will prompted to enter user resposes when running the above `main.py` command), you should create a file named `tests/integration/mock/<AgentName>/<TestName>/user_responses.log` containing all the responses in order you provided to the agent, delimited by newline ('\n'). This will be used to mock the STDIN during testing.
After running the above commands, you should be able to locate the real prompts
and responses logged. The log folder follows `logs/llm/%y-%m-%d_%H-%M.log` format.
Now, move all files under that folder to `tests/integration/mock/<AgentName>/<TestName>` folder. For example, moving all files from `logs/llm/24-04-23_21-55/` folder to
`tests/integration/mock/MonologueAgent/test_write_simple_script` folder.
That's it, you are good to go! When you launch an integration test, mock
responses are loaded and used to replace a real LLM, so that we get
deterministic and consistent behavior, and most importantly, without spending real
+30
View File
@@ -1,3 +1,4 @@
import io
import os
import re
from functools import partial
@@ -53,6 +54,28 @@ def get_mock_response(test_name, messages):
return resp_file.read()
def mock_user_response(*args, test_name, **kwargs):
"""The agent will ask for user input using `input()` when calling `asyncio.run(main(task))`.
This function mocks the user input by providing the response from the mock response file.
It will read the `user_responses.log` file in the test directory and set as
STDIN input for the agent to read.
"""
user_response_file = os.path.join(
script_dir,
'mock',
os.environ.get('AGENT'),
test_name,
'user_responses.log'
)
if not os.path.exists(user_response_file):
return ''
with open(user_response_file, 'r') as f:
ret = f.read().rstrip()
ret += '\n'
return ret
def mock_completion(*args, test_name, **kwargs):
messages = kwargs['messages']
message_str = ''
@@ -67,4 +90,11 @@ def mock_completion(*args, test_name, **kwargs):
@pytest.fixture(autouse=True)
def patch_completion(monkeypatch, request):
test_name = request.node.name
# Mock LLM completion
monkeypatch.setattr('opendevin.llm.llm.litellm_completion', partial(mock_completion, test_name=test_name))
# Mock user input (only for tests that have user_responses.log)
user_responses_str = mock_user_response(test_name=test_name)
if user_responses_str:
user_responses = io.StringIO(user_responses_str)
monkeypatch.setattr('sys.stdin', user_responses)
@@ -0,0 +1,215 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
Apart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
goto <line_number> - moves the window to show <line_number>
scroll_down - moves the window down {WINDOW} lines
scroll_up - moves the window down {WINDOW} lines
create <filename> - creates and opens a new file with the given name
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
edit <start_line>:<end_line>
<replacement_text>
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
The assistant's response should be concise, but do express their thoughts.
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
Observation:
[Code executed successfully with no output]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
Observation:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
Observation:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return str(numbers)
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
ASSISTANT:
I should edit the file to display the numbers in a table format. Let me do that for you:
<execute_bash>
edit 8:8 <<EOF
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
EOF
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
ASSISTANT:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
NOW, LET'S START!
Write a shell script 'hello.sh' that prints 'hello'.
@@ -0,0 +1,229 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
Apart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
goto <line_number> - moves the window to show <line_number>
scroll_down - moves the window down {WINDOW} lines
scroll_up - moves the window down {WINDOW} lines
create <filename> - creates and opens a new file with the given name
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
edit <start_line>:<end_line>
<replacement_text>
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
The assistant's response should be concise, but do express their thoughts.
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
Observation:
[Code executed successfully with no output]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
Observation:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
Observation:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return str(numbers)
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
ASSISTANT:
I should edit the file to display the numbers in a table format. Let me do that for you:
<execute_bash>
edit 8:8 <<EOF
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
EOF
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
ASSISTANT:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
NOW, LET'S START!
Write a shell script 'hello.sh' that prints 'hello'.
----------
Let's start by creating the shell script `hello.sh` that prints "hello". I'll write the script and save it to a file.
<execute_bash>
echo "echo 'hello'" > hello.sh
</execute_bash>
----------
OBSERVATION:
[Command -1 finished with exit code 0]]
@@ -0,0 +1,237 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
Apart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
goto <line_number> - moves the window to show <line_number>
scroll_down - moves the window down {WINDOW} lines
scroll_up - moves the window down {WINDOW} lines
create <filename> - creates and opens a new file with the given name
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
edit <start_line>:<end_line>
<replacement_text>
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
The assistant's response should be concise, but do express their thoughts.
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
Observation:
[Code executed successfully with no output]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
Observation:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
Observation:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return str(numbers)
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
ASSISTANT:
I should edit the file to display the numbers in a table format. Let me do that for you:
<execute_bash>
edit 8:8 <<EOF
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
EOF
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
ASSISTANT:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
NOW, LET'S START!
Write a shell script 'hello.sh' that prints 'hello'.
----------
Let's start by creating the shell script `hello.sh` that prints "hello". I'll write the script and save it to a file.
<execute_bash>
echo "echo 'hello'" > hello.sh
</execute_bash>
----------
OBSERVATION:
[Command -1 finished with exit code 0]]
----------
The shell script `hello.sh` has been successfully created. Would you like to run the script to see the output, or is there anything else you'd like to do with it?
----------
can you run it to make sure it is working?
@@ -0,0 +1,251 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
Apart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
goto <line_number> - moves the window to show <line_number>
scroll_down - moves the window down {WINDOW} lines
scroll_up - moves the window down {WINDOW} lines
create <filename> - creates and opens a new file with the given name
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
edit <start_line>:<end_line>
<replacement_text>
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
The assistant's response should be concise, but do express their thoughts.
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
Observation:
[Code executed successfully with no output]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
Observation:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
Observation:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return str(numbers)
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
ASSISTANT:
I should edit the file to display the numbers in a table format. Let me do that for you:
<execute_bash>
edit 8:8 <<EOF
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
EOF
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
ASSISTANT:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
NOW, LET'S START!
Write a shell script 'hello.sh' that prints 'hello'.
----------
Let's start by creating the shell script `hello.sh` that prints "hello". I'll write the script and save it to a file.
<execute_bash>
echo "echo 'hello'" > hello.sh
</execute_bash>
----------
OBSERVATION:
[Command -1 finished with exit code 0]]
----------
The shell script `hello.sh` has been successfully created. Would you like to run the script to see the output, or is there anything else you'd like to do with it?
----------
can you run it to make sure it is working?
----------
Sure, let's run the script `hello.sh` to ensure it's working correctly.
<execute_bash>
bash hello.sh
</execute_bash>
----------
OBSERVATION:
hello
[Command -1 finished with exit code 0]]
@@ -0,0 +1,259 @@
----------
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
Apart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:
open <path> [<line_number>] - opens the file at the given path in the editor. If line_number is provided, the window will be move to include that line
goto <line_number> - moves the window to show <line_number>
scroll_down - moves the window down {WINDOW} lines
scroll_up - moves the window down {WINDOW} lines
create <filename> - creates and opens a new file with the given name
search_dir <search_term> [<dir>] - searches for search_term in all files in dir. If dir is not provided, searches in the current directory
search_file <search_term> [<file>] - searches for search_term in file. If file is not provided, searches in the current open file
find_file <file_name> [<dir>] - finds all files with the given name in dir. If dir is not provided, searches in the current directory
edit <start_line>:<end_line>
<replacement_text>
end_of_edit - replaces lines <start_line> through <end_line> (inclusive) with the given text in the open file. The replacement text is terminated by a line with only end_of_edit on it. All of the <replacement text> will be entered, so make sure your indentation is formatted properly. Python files will be checked for syntax errors after the edit. If the system detects a syntax error, the edit will not be executed. Simply try to edit the file again, but make sure to read the error message and modify the edit command you issue accordingly. Issuing the same command a second time will just lead to the same error message again.
Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
The assistant's response should be concise, but do express their thoughts.
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
----------
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
numbers = list(range(1, 11))
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
Observation:
[Code executed successfully with no output]
ASSISTANT:
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
ASSISTANT:
It seems that Flask is not installed. Let me install Flask for you:
<execute_bash>
pip install flask
</execute_bash>
USER:
Observation:
Defaulting to user installation because normal site-packages is not writeable
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
ASSISTANT:
Now that Flask is installed, let me run the Python file again:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 124
ASSISTANT:
Let me check the server log again:
<execute_bash>
cat server.log
</execute_bash>
USER:
Observation:
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
ASSISTANT:
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
USER: Now kill the server, make it display the numbers in a table format.
ASSISTANT:
Sure! Let me stop the server first:
<execute_bash>
kill 124
</execute_bash>
USER:
Observation:
[1]+ Terminated python3 app.py > server.log 2>&1
ASSISTANT:
The server has been stopped. Let me open the Python file and modify it.
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return str(numbers)
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
ASSISTANT:
I should edit the file to display the numbers in a table format. Let me do that for you:
<execute_bash>
edit 8:8 <<EOF
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
EOF
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (11 lines total)]
1:
2:from flask import Flask
3:app = Flask(__name__)
4:
5:@app.route('/')
6:def index():
7: numbers = list(range(1, 11))
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
9:
10:if __name__ == '__main__':
11: app.run(port=5000)
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
ASSISTANT:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
NOW, LET'S START!
Write a shell script 'hello.sh' that prints 'hello'.
----------
Let's start by creating the shell script `hello.sh` that prints "hello". I'll write the script and save it to a file.
<execute_bash>
echo "echo 'hello'" > hello.sh
</execute_bash>
----------
OBSERVATION:
[Command -1 finished with exit code 0]]
----------
The shell script `hello.sh` has been successfully created. Would you like to run the script to see the output, or is there anything else you'd like to do with it?
----------
can you run it to make sure it is working?
----------
Sure, let's run the script `hello.sh` to ensure it's working correctly.
<execute_bash>
bash hello.sh
</execute_bash>
----------
OBSERVATION:
hello
[Command -1 finished with exit code 0]]
----------
The script `hello.sh` is working correctly as it printed "hello". If you have any more tasks or need further modifications, feel free to let me know!
----------
can you run it to make sure it is working?
@@ -0,0 +1,4 @@
Let's start by creating the shell script `hello.sh` that prints "hello". I'll write the script and save it to a file.
<execute_bash>
echo "echo 'hello'" > hello.sh
@@ -0,0 +1 @@
The shell script `hello.sh` has been successfully created. Would you like to run the script to see the output, or is there anything else you'd like to do with it?
@@ -0,0 +1,4 @@
Sure, let's run the script `hello.sh` to ensure it's working correctly.
<execute_bash>
bash hello.sh
@@ -0,0 +1 @@
The script `hello.sh` is working correctly as it printed "hello". If you have any more tasks or need further modifications, feel free to let me know!
@@ -0,0 +1 @@
The script `hello.sh` was just run and it successfully printed "hello". If you need any further assistance or have another task in mind, please let me know!
@@ -0,0 +1,2 @@
can you run it to make sure it is working?
/exit
@@ -92,7 +92,8 @@ This is your internal monologue, in JSON format:
{
"action": "recall",
"args": {
"query": "what it is I want to do"
"query": "what it is I want to do",
"thought": ""
}
},
{
@@ -119,7 +120,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -144,7 +146,7 @@ This is your internal monologue, in JSON format:
"content": "echo \"console.log('hello world')\"",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -163,7 +165,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -193,7 +196,7 @@ This is your internal monologue, in JSON format:
"path": "test.js",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -224,7 +227,8 @@ This is your internal monologue, in JSON format:
{
"action": "browse",
"args": {
"url": "google.com"
"url": "google.com",
"thought": ""
}
},
{
@@ -92,7 +92,8 @@ This is your internal monologue, in JSON format:
{
"action": "recall",
"args": {
"query": "what it is I want to do"
"query": "what it is I want to do",
"thought": ""
}
},
{
@@ -119,7 +120,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -144,7 +146,7 @@ This is your internal monologue, in JSON format:
"content": "echo \"console.log('hello world')\"",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -163,7 +165,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -193,7 +196,7 @@ This is your internal monologue, in JSON format:
"path": "test.js",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -224,7 +227,8 @@ This is your internal monologue, in JSON format:
{
"action": "browse",
"args": {
"url": "google.com"
"url": "google.com",
"thought": ""
}
},
{
@@ -317,7 +321,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -92,7 +92,8 @@ This is your internal monologue, in JSON format:
{
"action": "recall",
"args": {
"query": "what it is I want to do"
"query": "what it is I want to do",
"thought": ""
}
},
{
@@ -119,7 +120,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -144,7 +146,7 @@ This is your internal monologue, in JSON format:
"content": "echo \"console.log('hello world')\"",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -163,7 +165,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -193,7 +196,7 @@ This is your internal monologue, in JSON format:
"path": "test.js",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -224,7 +227,8 @@ This is your internal monologue, in JSON format:
{
"action": "browse",
"args": {
"url": "google.com"
"url": "google.com",
"thought": ""
}
},
{
@@ -317,7 +321,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -332,7 +337,7 @@ This is your internal monologue, in JSON format:
{
"action": "think",
"args": {
"thought": "Before I create the 'hello.sh' file, I need to think about the content. It should contain a shebang line to specify the shell interpreter and the command to print 'hello'."
"thought": "Since there are no existing files listed by the 'ls' command, I should create the 'hello.sh' script now."
}
},
{
@@ -92,7 +92,8 @@ This is your internal monologue, in JSON format:
{
"action": "recall",
"args": {
"query": "what it is I want to do"
"query": "what it is I want to do",
"thought": ""
}
},
{
@@ -119,7 +120,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -144,7 +146,7 @@ This is your internal monologue, in JSON format:
"content": "echo \"console.log('hello world')\"",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -163,7 +165,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -193,7 +196,7 @@ This is your internal monologue, in JSON format:
"path": "test.js",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -224,7 +227,8 @@ This is your internal monologue, in JSON format:
{
"action": "browse",
"args": {
"url": "google.com"
"url": "google.com",
"thought": ""
}
},
{
@@ -317,7 +321,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -332,7 +337,7 @@ This is your internal monologue, in JSON format:
{
"action": "think",
"args": {
"thought": "Before I create the 'hello.sh' file, I need to think about the content. It should contain a shebang line to specify the shell interpreter and the command to print 'hello'."
"thought": "Since there are no existing files listed by the 'ls' command, I should create the 'hello.sh' script now."
}
},
{
@@ -347,7 +352,7 @@ This is your internal monologue, in JSON format:
"content": "#!/bin/bash\necho 'hello'",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -92,7 +92,8 @@ This is your internal monologue, in JSON format:
{
"action": "recall",
"args": {
"query": "what it is I want to do"
"query": "what it is I want to do",
"thought": ""
}
},
{
@@ -119,7 +120,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "echo \"hello world\"",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -144,7 +146,7 @@ This is your internal monologue, in JSON format:
"content": "echo \"console.log('hello world')\"",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -163,7 +165,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "node test.js",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -193,7 +196,7 @@ This is your internal monologue, in JSON format:
"path": "test.js",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -224,7 +227,8 @@ This is your internal monologue, in JSON format:
{
"action": "browse",
"args": {
"url": "google.com"
"url": "google.com",
"thought": ""
}
},
{
@@ -317,7 +321,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "ls",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -332,7 +337,7 @@ This is your internal monologue, in JSON format:
{
"action": "think",
"args": {
"thought": "Before I create the 'hello.sh' file, I need to think about the content. It should contain a shebang line to specify the shell interpreter and the command to print 'hello'."
"thought": "Since there are no existing files listed by the 'ls' command, I should create the 'hello.sh' script now."
}
},
{
@@ -347,7 +352,7 @@ This is your internal monologue, in JSON format:
"content": "#!/bin/bash\necho 'hello'",
"start": 0,
"end": -1,
"thoughts": ""
"thought": ""
}
},
{
@@ -361,7 +366,8 @@ This is your internal monologue, in JSON format:
"action": "run",
"args": {
"command": "bash hello.sh",
"background": false
"background": false,
"thought": ""
}
},
{
@@ -0,0 +1,403 @@
----------
You're a thoughtful robot. Your main task is this:
Write a shell script 'hello.sh' that prints 'hello'.
Don't expand the scope of your task--just complete it as written.
This is your internal monologue, in JSON format:
[
{
"action": "think",
"args": {
"thought": "I exist!"
}
},
{
"action": "think",
"args": {
"thought": "Hmm...looks like I can type in a command line prompt"
}
},
{
"action": "think",
"args": {
"thought": "Looks like I have a web browser too!"
}
},
{
"action": "think",
"args": {
"thought": "Here's what I want to do: Write a shell script 'hello.sh' that prints 'hello'."
}
},
{
"action": "think",
"args": {
"thought": "How am I going to get there though?"
}
},
{
"action": "think",
"args": {
"thought": "It seems like I have some kind of short term memory."
}
},
{
"action": "think",
"args": {
"thought": "Each of my thoughts seems to be stored in a JSON array."
}
},
{
"action": "think",
"args": {
"thought": "It seems whatever I say next will be added as an object to the list."
}
},
{
"action": "think",
"args": {
"thought": "But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process."
}
},
{
"action": "think",
"args": {
"thought": "Fortunately I have long term memory!"
}
},
{
"action": "think",
"args": {
"thought": "I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!"
}
},
{
"action": "think",
"args": {
"thought": "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!"
}
},
{
"action": "think",
"args": {
"thought": "Let's try it out!"
}
},
{
"action": "recall",
"args": {
"query": "what it is I want to do",
"thought": ""
}
},
{
"observation": "recall",
"content": "Here's what I want to do: Write a shell script 'hello.sh' that prints 'hello'.",
"extras": {
"memories": [],
"role": "assistant"
}
},
{
"action": "think",
"args": {
"thought": "How am I going to get there though?"
}
},
{
"action": "think",
"args": {
"thought": "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!"
}
},
{
"action": "run",
"args": {
"command": "echo \"hello world\"",
"background": false,
"thought": ""
}
},
{
"observation": "run",
"content": "hello world",
"extras": {
"command_id": 0,
"command": "",
"exit_code": 0
}
},
{
"action": "think",
"args": {
"thought": "Cool! I bet I can write files too using the write action."
}
},
{
"action": "write",
"args": {
"path": "test.js",
"content": "echo \"console.log('hello world')\"",
"start": 0,
"end": -1,
"thought": ""
}
},
{
"action": "think",
"args": {
"thought": ""
}
},
{
"action": "think",
"args": {
"thought": "I just created test.js. I'll try and run it now."
}
},
{
"action": "run",
"args": {
"command": "node test.js",
"background": false,
"thought": ""
}
},
{
"observation": "run",
"content": "hello world",
"extras": {
"command_id": 0,
"command": "",
"exit_code": 0
}
},
{
"action": "think",
"args": {
"thought": "It works!"
}
},
{
"action": "think",
"args": {
"thought": "I'm going to try reading it now using the read action."
}
},
{
"action": "read",
"args": {
"path": "test.js",
"start": 0,
"end": -1,
"thought": ""
}
},
{
"observation": "read",
"content": "console.log('hello world')",
"extras": {
"path": ""
}
},
{
"action": "think",
"args": {
"thought": "Nice! I can read files too!"
}
},
{
"action": "think",
"args": {
"thought": "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"
}
},
{
"action": "think",
"args": {
"thought": "Let's try that..."
}
},
{
"action": "browse",
"args": {
"url": "google.com",
"thought": ""
}
},
{
"observation": "browse",
"content": "<form><input type=\"text\"></input><button type=\"submit\"></button></form>",
"extras": {
"url": "",
"status_code": 200,
"error": false
}
},
{
"action": "think",
"args": {
"thought": "I can browse the web too!"
}
},
{
"action": "think",
"args": {
"thought": "And once I have completed my task, I can use the finish action to stop working."
}
},
{
"action": "think",
"args": {
"thought": "But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work."
}
},
{
"action": "think",
"args": {
"thought": "Very cool. Now to accomplish my task."
}
},
{
"action": "think",
"args": {
"thought": "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."
}
},
{
"action": "think",
"args": {
"thought": "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."
}
},
{
"action": "think",
"args": {
"thought": "OK so my task is to Write a shell script 'hello.sh' that prints 'hello'.. I haven't made any progress yet. Where should I start?"
}
},
{
"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."
}
},
{
"action": "run",
"args": {
"command": "ls",
"background": false,
"thought": ""
}
},
{
"observation": "run",
"content": "",
"extras": {
"command_id": -1,
"command": "ls",
"exit_code": 0
}
},
{
"action": "think",
"args": {
"thought": "Since there are no existing files listed by the 'ls' command, I should create the 'hello.sh' script now."
}
},
{
"observation": "null",
"content": "",
"extras": {}
},
{
"action": "write",
"args": {
"path": "hello.sh",
"content": "#!/bin/bash\necho 'hello'",
"start": 0,
"end": -1,
"thought": ""
}
},
{
"observation": "write",
"content": "",
"extras": {
"path": "hello.sh"
}
},
{
"action": "run",
"args": {
"command": "bash hello.sh",
"background": false,
"thought": ""
}
},
{
"observation": "run",
"content": "hello",
"extras": {
"command_id": -1,
"command": "bash hello.sh",
"exit_code": 0
}
},
{
"action": "think",
"args": {
"thought": "The script 'hello.sh' has been written and tested successfully. It prints 'hello' as required. I should now finalize my task."
}
},
{
"observation": "null",
"content": "",
"extras": {}
}
]
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:
* `action`, which is one of the actions below
* `args`, which is a map of key-value pairs, specifying the arguments for that action
Here are the possible actions:
* `read` - reads the content of a file. Arguments:
* `path` - the path of the file to read
* `write` - writes the content to a file. Arguments:
* `path` - the path of the file to write
* `content` - the content to write to the file
* `run` - runs a command. Arguments:
* `command` - the command to run
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
* `kill` - kills a background command
* `id` - the ID of the background command to kill
* `browse` - opens a web page. Arguments:
* `url` - the URL to open
* `recall` - recalls a past memory. Arguments:
* `query` - the query to search for
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
* `thought` - the thought to record
* `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
You MUST take time to think in between read, write, run, browse, and recall actions.
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.
* 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 &`)
What is your next thought or action? Again, you must reply with JSON, and only with JSON.
@@ -1,6 +1,6 @@
{
"action": "think",
"args": {
"thought": "Before I create the 'hello.sh' file, I need to think about the content. It should contain a shebang line to specify the shell interpreter and the command to print 'hello'."
"thought": "Since there are no existing files listed by the 'ls' command, I should create the 'hello.sh' script now."
}
}
@@ -1,3 +1,4 @@
```json
{
"action": "run",
"args": {
@@ -5,3 +6,4 @@
"background": false
}
}
```
@@ -1,4 +1,8 @@
```json
{
"action": "finish",
"args": {}
"action": "think",
"args": {
"thought": "The script 'hello.sh' has been written and tested successfully. It prints 'hello' as required. I should now finalize my task."
}
}
```
@@ -0,0 +1,4 @@
{
"action": "finish",
"args": {}
}
@@ -58,7 +58,8 @@ ten actions--more happened before that.
"action": "modify_task",
"args": {
"id": "0",
"state": "in_progress"
"state": "in_progress",
"thought": ""
}
}
]
@@ -58,24 +58,14 @@ ten actions--more happened before that.
"action": "modify_task",
"args": {
"id": "0",
"state": "in_progress"
"state": "in_progress",
"thought": ""
}
},
{
"action": "write",
"action": "think",
"args": {
"path": "hello.sh",
"content": "#!/bin/bash\necho 'hello'",
"start": 0,
"end": -1,
"thoughts": ""
}
},
{
"observation": "write",
"content": "",
"extras": {
"path": "hello.sh"
"thought": "I need to write a shell script called 'hello.sh' that contains the necessary command to print 'hello'. This will likely involve using echo or printf to output the text to the console when the script is run. I should start by creating and writing the content to the file."
}
}
]
@@ -119,4 +109,4 @@ actions are all `think` actions, you should consider taking a different action.
What is your next thought or action? Again, you must reply with JSON, and only with JSON.
You just changed a file. You should think about how it affects your plan.
Look at your last thought in the history above. What does it suggest? Don't think anymore--take action.

Some files were not shown because too many files have changed in this diff Show More