Merge remote-tracking branch 'origin' into ab-docs-remove

This commit is contained in:
Alex Bäuerle 2024-05-01 10:35:11 -07:00
commit 478ebedd41
No known key found for this signature in database
GPG Key ID: EB015650BB01959A
180 changed files with 3893 additions and 811 deletions

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
.github/workflows/dummy-agent-test.yml vendored Normal file
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

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

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:

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.

View File

@ -200,12 +200,22 @@ setup-config-prompts:
@read -p "Enter your LLM Base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
if [[ ! -z "$$llm_base_url" ]]; then echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@echo "Enter your LLM Embedding Model\nChoices are openai, azureopenai, llama2 or leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "LLM_EMBEDDING_MODEL=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
if [ "$$llm_embedding_model" = "llama2" ]; then \
read -p "Enter the local model URL (will overwrite LLM_BASE_URL): " llm_base_url; \
echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
@echo "Enter your LLM Embedding Model"; \
echo "Choices are:"; \
echo " - openai"; \
echo " - azureopenai"; \
echo " - Embeddings available only with OllamaEmbedding:"; \
echo " - llama2"; \
echo " - mxbai-embed-large"; \
echo " - nomic-embed-text"; \
echo " - all-minilm"; \
echo " - stable-code"; \
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "LLM_EMBEDDING_MODEL=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
read -p "Enter the local model URL for the embedding model (will set LLM_EMBEDDING_BASE_URL): " llm_embedding_base_url; \
echo "LLM_EMBEDDING_BASE_URL=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
read -p "Enter the Azure endpoint URL (will overwrite LLM_BASE_URL): " llm_base_url; \
echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \

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.

View File

@ -1,4 +1,5 @@
from opendevin.agent import Agent
from .agent import SWEAgent
Agent.register('SWEAgent', SWEAgent)

View File

@ -1,23 +1,23 @@
from typing import List
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.state import State
from opendevin.action import (
Action,
AgentThinkAction,
FileReadAction,
FileWriteAction,
)
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.observation import Observation
from opendevin.state import State
from .parser import parse_command
from .prompts import (
SYSTEM_MESSAGE,
STEP_PROMPT,
CONTEXT_PROMPT,
MEMORY_FORMAT,
NO_ACTION,
CONTEXT_PROMPT
STEP_PROMPT,
SYSTEM_MESSAGE,
)

View File

@ -1,17 +1,17 @@
import re
from opendevin.action import (
Action,
AgentEchoAction,
AgentFinishAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
BrowseURLAction,
AgentEchoAction,
AgentThinkAction,
)
import re
from .prompts import CUSTOM_DOCS, COMMAND_USAGE
from .prompts import COMMAND_USAGE, CUSTOM_DOCS
# commands: exit, read, write, browse, kill, search_file, search_dir

View File

@ -1,20 +1,27 @@
from .micro.registry import all_microagents
from .micro.agent import MicroAgent
from dotenv import load_dotenv
from opendevin.agent import Agent
from dotenv import load_dotenv
from .micro.agent import MicroAgent
from .micro.registry import all_microagents
load_dotenv()
# Import agents after environment variables are loaded
from . import monologue_agent # noqa: E402
from . import codeact_agent # noqa: E402
from . import planner_agent # noqa: E402
from . import SWE_agent # noqa: E402
from . import delegator_agent # noqa: E402
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']

View File

@ -1,4 +1,5 @@
from opendevin.agent import Agent
from .codeact_agent import CodeActAgent
Agent.register('CodeActAgent', CodeActAgent)

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.state import State
from opendevin.sandbox.plugins import PluginRequirement, JupyterRequirement
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')

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

View File

@ -1,4 +1,5 @@
from opendevin.agent import Agent
from .agent import DelegatorAgent
Agent.register('DelegatorAgent', DelegatorAgent)

View File

@ -1,11 +1,10 @@
from typing import List
from opendevin.action import Action, AgentDelegateAction, AgentFinishAction
from opendevin.agent import Agent
from opendevin.action import AgentFinishAction, AgentDelegateAction
from opendevin.observation import AgentDelegateObservation
from opendevin.llm.llm import LLM
from opendevin.observation import AgentDelegateObservation
from opendevin.state import State
from opendevin.action import Action
class DelegatorAgent(Agent):

View File

@ -0,0 +1,5 @@
from opendevin.agent import Agent
from .agent import DummyAgent
Agent.register('DummyAgent', DummyAgent)

View File

@ -1,21 +1,118 @@
"""Module for a Dummy agent."""
import time
from typing import List, TypedDict
from opendevin.action.base import NullAction
from opendevin.state import State
from opendevin.action import Action
from typing import List
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
agenthub/micro/README.md Normal file
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.

View File

@ -1,13 +1,13 @@
import json
from typing import List, Dict
from typing import Dict, List
from jinja2 import Environment, BaseLoader
from jinja2 import BaseLoader, Environment
from opendevin.action import Action, action_from_dict
from opendevin.agent import Agent
from opendevin.exceptions import LLMOutputError
from opendevin.llm.llm import LLM
from opendevin.state import State
from opendevin.action import Action, action_from_dict
from opendevin.exceptions import LLMOutputError
from .instructions import instructions
from .registry import all_microagents

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 }}

View File

@ -1,5 +1,5 @@
from typing import Dict
import os
from typing import Dict
instructions: Dict = {}

View File

@ -1,4 +1,5 @@
import os
import yaml
all_microagents = {}

View File

@ -1,4 +1,5 @@
from opendevin.agent import Agent
from .agent import MonologueAgent
Agent.register('MonologueAgent', MonologueAgent)

View File

@ -1,35 +1,34 @@
from typing import List
from opendevin.agent import Agent
from opendevin.state import State
from opendevin.llm.llm import LLM
from opendevin.schema import ActionType
from opendevin.exceptions import AgentNoInstructionError
from opendevin.schema.config import ConfigType
from opendevin import config
from opendevin.action import (
Action,
NullAction,
CmdRunAction,
FileWriteAction,
FileReadAction,
AgentRecallAction,
BrowseURLAction,
GitHubPushAction,
AgentThinkAction,
)
from opendevin.observation import (
Observation,
NullObservation,
CmdOutputObservation,
FileReadObservation,
AgentRecallObservation,
BrowserOutputObservation,
)
import agenthub.monologue_agent.utils.prompts as prompts
from agenthub.monologue_agent.utils.monologue import Monologue
from opendevin import config
from opendevin.action import (
Action,
AgentRecallAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
GitHubPushAction,
NullAction,
)
from opendevin.agent import Agent
from opendevin.exceptions import AgentNoInstructionError
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentRecallObservation,
BrowserOutputObservation,
CmdOutputObservation,
FileReadObservation,
NullObservation,
Observation,
)
from opendevin.schema import ActionType
from opendevin.schema.config import ConfigType
from opendevin.state import State
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
from agenthub.monologue_agent.utils.memory import LongTermMemory
@ -137,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
@ -157,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)
@ -208,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:
"""
@ -257,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

View File

@ -1,4 +1,5 @@
import json
from json_repair import repair_json

View File

@ -1,17 +1,22 @@
import llama_index.embeddings.openai.base as llama_openai
import threading
import chromadb
from llama_index.core import Document
import llama_index.embeddings.openai.base as llama_openai
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_random_exponential
from openai._exceptions import APIConnectionError, RateLimitError, InternalServerError
from openai._exceptions import APIConnectionError, InternalServerError, RateLimitError
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
from opendevin import config
from opendevin.logger import opendevin_logger as logger
from opendevin.schema.config import ConfigType
from . import json
num_retries = config.get(ConfigType.LLM_NUM_RETRIES)
@ -51,11 +56,12 @@ embedding_strategy = config.get(ConfigType.LLM_EMBEDDING_MODEL)
# TODO: More embeddings: https://docs.llamaindex.ai/en/stable/examples/embeddings/OpenAI/
# There's probably a more programmatic way to do this.
if embedding_strategy == 'llama2':
supported_ollama_embed_models = ['llama2', 'mxbai-embed-large', 'nomic-embed-text', 'all-minilm', 'stable-code']
if embedding_strategy in supported_ollama_embed_models:
from llama_index.embeddings.ollama import OllamaEmbedding
embed_model = OllamaEmbedding(
model_name='llama2',
base_url=config.get(ConfigType.LLM_BASE_URL, required=True),
model_name=embedding_strategy,
base_url=config.get(ConfigType.LLM_EMBEDDING_BASE_URL, required=True),
ollama_additional_kwargs={'mirostat': 0},
)
elif embedding_strategy == 'openai':

View File

@ -1,8 +1,8 @@
from opendevin.llm.llm import LLM
from opendevin.exceptions import AgentEventTypeError
import agenthub.monologue_agent.utils.json as json
import agenthub.monologue_agent.utils.prompts as prompts
from opendevin.exceptions import AgentEventTypeError
from opendevin.llm.llm import LLM
from opendevin.logger import opendevin_logger as logger

View File

@ -1,21 +1,20 @@
import re
from json import JSONDecodeError
from typing import List
from . import json
from json import JSONDecodeError
import re
from opendevin import config
from opendevin.action import (
action_from_dict,
Action,
action_from_dict,
)
from opendevin.exceptions import LLMOutputError
from opendevin.observation import (
CmdOutputObservation,
)
from opendevin.exceptions import LLMOutputError
from opendevin import config
from opendevin.schema.config import ConfigType
from . import json
ACTION_PROMPT = """
You're a thoughtful robot. Your main task is this:
%(task)s

View File

@ -1,4 +1,5 @@
from opendevin.agent import Agent
from .agent import PlannerAgent
Agent.register('PlannerAgent', PlannerAgent)

View File

@ -1,11 +1,11 @@
from typing import List
from .prompt import get_prompt, parse_response
from opendevin.action import Action, AgentFinishAction
from opendevin.agent import Agent
from opendevin.action import AgentFinishAction
from opendevin.llm.llm import LLM
from opendevin.state import State
from opendevin.action import Action
from .prompt import get_prompt, parse_response
class PlannerAgent(Agent):

View File

@ -1,29 +1,29 @@
import json
from typing import List, Tuple, Dict, Type
from opendevin.plan import Plan
from opendevin.action import Action, action_from_dict
from opendevin.observation import Observation
from opendevin.schema import ActionType
from opendevin.logger import opendevin_logger as logger
from typing import Dict, List, Tuple, Type
from opendevin.action import (
NullAction,
CmdRunAction,
CmdKillAction,
Action,
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentSummarizeAction,
AgentThinkAction,
BrowseURLAction,
CmdKillAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
AgentRecallAction,
AgentThinkAction,
AgentFinishAction,
AgentSummarizeAction,
AddTaskAction,
ModifyTaskAction,
NullAction,
action_from_dict,
)
from opendevin.logger import opendevin_logger as logger
from opendevin.observation import (
NullObservation,
Observation,
)
from opendevin.plan import Plan
from opendevin.schema import ActionType
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
ActionType.RUN: CmdRunAction,

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
containers/app/entrypoint.sh Executable file
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"

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

View File

@ -7,6 +7,7 @@ select = [
"E",
"W",
"F",
"I",
"Q",
]

View File

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

View File

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

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.

View File

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

View File

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

View File

@ -11,12 +11,12 @@
},
"outputs": [],
"source": [
"import requests\n",
"import matplotlib.pyplot as plt\n",
"import pandas as pd\n",
"from tqdm import tqdm\n",
"from datasets import load_dataset\n",
"import requests\n",
"import seaborn as sns\n",
"import matplotlib.pyplot as plt"
"from datasets import load_dataset\n",
"from tqdm import tqdm"
]
},
{

View File

@ -11,9 +11,10 @@ Outputs:
'''
# fetch devin's outputs into a json file for evaluation
import json
import os
import sys
import json
import requests
from tqdm import tqdm

View File

@ -1,4 +1,5 @@
import os
import pytest
from conftest import agents

View File

@ -1,9 +1,10 @@
import os
import pytest
import subprocess
import logging
import shutil
import datetime
import logging
import os
import shutil
import subprocess
import pytest
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CASES_DIR = os.path.join(SCRIPT_DIR, 'cases')

View File

@ -1,4 +1,5 @@
import argparse
import pytest
from opendevin import config

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",

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",

View File

@ -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",

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>

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>
);
}

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>

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;

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>
);
}

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

View File

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

View File

@ -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",

View File

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

View File

@ -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", () => {

View File

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

View File

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

View File

@ -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",
},

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."
}
}

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)));
},

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") {

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();

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

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", () => {

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.

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;

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({

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",

View File

@ -1,6 +1,7 @@
enum AgentTaskState {
INIT = "init",
RUNNING = "running",
AWAITING_USER_INPUT = "awaiting_user_input",
PAUSED = "paused",
STOPPED = "stopped",
FINISHED = "finished",

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",

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 };

View File

@ -1,27 +1,30 @@
from ..exceptions import AgentMalformedActionError
from .agent import (
AgentDelegateAction,
AgentEchoAction,
AgentFinishAction,
AgentRecallAction,
AgentSummarizeAction,
AgentTalkAction,
AgentThinkAction,
)
from .base import Action, NullAction
from .bash import CmdRunAction, CmdKillAction
from .bash import CmdKillAction, CmdRunAction, IPythonRunCellAction
from .browse import BrowseURLAction
from .fileop import FileReadAction, FileWriteAction
from .github import GitHubPushAction
from .agent import (
AgentRecallAction,
AgentThinkAction,
AgentFinishAction,
AgentEchoAction,
AgentSummarizeAction,
AgentDelegateAction,
)
from .tasks import AddTaskAction, ModifyTaskAction
from ..exceptions import AgentMalformedActionError
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'
]

View File

@ -2,12 +2,13 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Dict
from opendevin.observation import (
AgentRecallObservation,
AgentMessageObservation,
AgentRecallObservation,
NullObservation,
Observation,
)
from opendevin.schema import ActionType
from .base import ExecutableAction, NotExecutableAction
if TYPE_CHECKING:
@ -17,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),
)
@ -43,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
@ -69,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':
@ -83,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':

View File

@ -1,5 +1,6 @@
from dataclasses import dataclass, asdict
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING
from opendevin.schema import ActionType
if TYPE_CHECKING:

View File

@ -1,18 +1,25 @@
import os
import pathlib
from dataclasses import dataclass
from typing import TYPE_CHECKING
from opendevin import config
from opendevin.schema import ActionType, ConfigType
from .base import ExecutableAction
from opendevin.schema import ActionType
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':
@ -22,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':
@ -34,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}'

View File

@ -1,10 +1,12 @@
import os
import base64
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING
from playwright.async_api import async_playwright
from opendevin.observation import BrowserOutputObservation
from opendevin.schema import ActionType
from typing import TYPE_CHECKING
from playwright.async_api import async_playwright
from .base import ExecutableAction
@ -15,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

View File

@ -1,18 +1,16 @@
import os
from dataclasses import dataclass
from pathlib import Path
from opendevin import config
from opendevin.observation import (
Observation,
AgentErrorObservation,
FileReadObservation,
FileWriteObservation,
AgentErrorObservation,
Observation,
)
from opendevin.schema import ActionType
from opendevin.sandbox import E2BBox
from opendevin import config
from opendevin.schema import ActionType
from opendevin.schema.config import ConfigType
from .base import ExecutableAction
@ -52,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]):
@ -102,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]):

View File

@ -1,14 +1,15 @@
import random
import string
from dataclasses import dataclass
from opendevin.observation import Observation, AgentErrorObservation
from typing import TYPE_CHECKING
import requests
from opendevin import config
from opendevin.observation import AgentErrorObservation, Observation
from opendevin.observation.message import AgentMessageObservation
from opendevin.observation.run import CmdOutputObservation
from opendevin.schema import ActionType
from opendevin import config
from typing import TYPE_CHECKING
import requests
import random
import string
from opendevin.schema.config import ConfigType
from .base import ExecutableAction

View File

@ -1,10 +1,11 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from opendevin.observation import NullObservation
from opendevin.schema import ActionType
from .base import ExecutableAction, NotExecutableAction
from opendevin.schema import ActionType
from opendevin.observation import NullObservation
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from opendevin.controller import AgentController
@ -14,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
@ -30,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
@ -46,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

View File

@ -1,11 +1,11 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Type, TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, List, Type
if TYPE_CHECKING:
from opendevin.action import Action
from opendevin.state import State
from opendevin.llm.llm import LLM
from opendevin.exceptions import AgentAlreadyRegisteredError, AgentNotRegisteredError
from opendevin.llm.llm import LLM
from opendevin.sandbox.plugins import PluginRequirement

View File

@ -1,12 +1,13 @@
import os
import argparse
import toml
import logging
import os
import pathlib
import platform
import toml
from dotenv import load_dotenv
from opendevin.schema import ConfigType
import logging
logger = logging.getLogger(__name__)
@ -30,6 +31,7 @@ DEFAULT_CONFIG: dict = {
ConfigType.SANDBOX_CONTAINER_IMAGE: DEFAULT_CONTAINER_IMAGE,
ConfigType.RUN_AS_DEVIN: 'true',
ConfigType.LLM_EMBEDDING_MODEL: 'local',
ConfigType.LLM_EMBEDDING_BASE_URL: None,
ConfigType.LLM_EMBEDDING_DEPLOYMENT_NAME: None,
ConfigType.LLM_API_VERSION: None,
ConfigType.LLM_NUM_RETRIES: 5,
@ -50,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 = ''
@ -153,6 +157,9 @@ def finalize_config():
if config.get(ConfigType.WORKSPACE_MOUNT_PATH) is None:
config[ConfigType.WORKSPACE_MOUNT_PATH] = os.path.abspath(config[ConfigType.WORKSPACE_BASE])
if config.get(ConfigType.LLM_EMBEDDING_BASE_URL) is None:
config[ConfigType.LLM_EMBEDDING_BASE_URL] = config.get(ConfigType.LLM_BASE_URL)
USE_HOST_NETWORK = config[ConfigType.USE_HOST_NETWORK].lower() != 'false'
if USE_HOST_NETWORK and platform.system() == 'Darwin':
logger.warning(
@ -164,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()

View File

@ -1,5 +1,5 @@
from .agent_controller import AgentController
from .action_manager import ActionManager
from .agent_controller import AgentController
__all__ = [
'AgentController',

View File

@ -1,17 +1,18 @@
from typing import List
from opendevin import config
from opendevin.observation import CmdOutputObservation, AgentErrorObservation
from opendevin.sandbox import DockerExecBox, DockerSSHBox, Sandbox, LocalBox, E2BBox
from opendevin.schema import ConfigType
from opendevin.action import (
Action,
)
from opendevin.observation import (
Observation,
AgentErrorObservation,
CmdOutputObservation,
NullObservation,
Observation,
)
from opendevin.sandbox import DockerExecBox, DockerSSHBox, E2BBox, LocalBox, Sandbox
from opendevin.sandbox.plugins import PluginRequirement
from opendevin.schema import ConfigType
class ActionManager:

View File

@ -1,30 +1,37 @@
import asyncio
from typing import Callable, List, Type
from agenthub.codeact_agent.codeact_agent import CodeActAgent
from opendevin import config
from opendevin.schema.config import ConfigType
from opendevin.action import (
Action,
AgentFinishAction,
AgentDelegateAction,
AgentFinishAction,
AgentTalkAction,
NullAction,
)
from opendevin.observation import (
Observation,
AgentErrorObservation,
AgentDelegateObservation,
NullObservation,
)
from opendevin.agent import Agent
from opendevin.exceptions import AgentMalformedActionError, AgentNoActionError, MaxCharsExceedError, LLMOutputError
from opendevin.logger import opendevin_logger as logger
from opendevin.plan import Plan
from opendevin.state import State
from opendevin.action.tasks import TaskStateChangedAction
from opendevin.schema import TaskState
from opendevin.agent import Agent
from opendevin.controller.action_manager import ActionManager
from opendevin.exceptions import (
AgentMalformedActionError,
AgentNoActionError,
LLMOutputError,
MaxCharsExceedError,
)
from opendevin.logger import opendevin_logger as logger
from opendevin.observation import (
AgentDelegateObservation,
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
MAX_ITERATIONS = config.get(ConfigType.MAX_ITERATIONS)
MAX_CHARS = config.get(ConfigType.MAX_CHARS)
@ -61,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
@ -171,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)
@ -198,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)
@ -223,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]

View File

@ -3,10 +3,11 @@ import os
import sys
import traceback
from datetime import datetime
from opendevin import config
from typing import Literal, Mapping
from termcolor import colored
from opendevin import config
from opendevin.schema.config import ConfigType
DISABLE_COLOR_PRINTING = (

View File

@ -3,8 +3,8 @@ import sys
from typing import Type
import agenthub # noqa F401 (we import this to get the agents registered)
from opendevin.config import args
from opendevin.agent import Agent
from opendevin.config import args
from opendevin.controller import AgentController
from opendevin.llm.llm import LLM

View File

@ -1,11 +1,11 @@
from .base import Observation, NullObservation
from .run import CmdOutputObservation
from .base import NullObservation, Observation
from .browse import BrowserOutputObservation
from .files import FileReadObservation, FileWriteObservation
from .message import UserMessageObservation, AgentMessageObservation
from .recall import AgentRecallObservation
from .delegate import AgentDelegateObservation
from .error import AgentErrorObservation
from .files import FileReadObservation, FileWriteObservation
from .message import AgentMessageObservation, UserMessageObservation
from .recall import AgentRecallObservation
from .run import CmdOutputObservation, IPythonRunCellObservation
observations = (
CmdOutputObservation,
@ -40,6 +40,7 @@ __all__ = [
'Observation',
'NullObservation',
'CmdOutputObservation',
'IPythonRunCellObservation',
'BrowserOutputObservation',
'FileReadObservation',
'FileWriteObservation',

View File

@ -1,5 +1,6 @@
import copy
from dataclasses import dataclass
from opendevin.schema import ObservationType

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class BrowserOutputObservation(Observation):

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class AgentDelegateObservation(Observation):

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class AgentErrorObservation(Observation):

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class FileReadObservation(Observation):

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class UserMessageObservation(Observation):

View File

@ -1,9 +1,10 @@
from dataclasses import dataclass
from typing import List
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class AgentRecallObservation(Observation):

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from .base import Observation
from opendevin.schema import ObservationType
from .base import Observation
@dataclass
class CmdOutputObservation(Observation):
@ -22,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.'

View File

@ -1,7 +1,7 @@
from typing import List
from opendevin.logger import opendevin_logger as logger
from opendevin.exceptions import PlanInvalidStateError
from opendevin.logger import opendevin_logger as logger
OPEN_STATE = 'open'
COMPLETED_STATE = 'completed'

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