mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Merge remote-tracking branch 'origin' into ab-docs-remove
This commit is contained in:
commit
478ebedd41
2
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@ -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
21
.github/workflows/dummy-agent-test.yml
vendored
Normal 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
|
||||
17
.github/workflows/ghcr.yml
vendored
17
.github/workflows/ghcr.yml
vendored
@ -42,8 +42,21 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Delete huge unnecessary tools folder
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
|
||||
- name: Build and push ${{ matrix.image }}
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
6
.github/workflows/run-unit-tests.yml
vendored
6
.github/workflows/run-unit-tests.yml
vendored
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
22
Makefile
22
Makefile
@ -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; \
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from opendevin.agent import Agent
|
||||
|
||||
from .agent import SWEAgent
|
||||
|
||||
Agent.register('SWEAgent', SWEAgent)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from opendevin.agent import Agent
|
||||
|
||||
from .codeact_agent import CodeActAgent
|
||||
|
||||
Agent.register('CodeActAgent', CodeActAgent)
|
||||
|
||||
@ -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 ' already displayed to user'
|
||||
content = '\n'.join(splited)
|
||||
self.messages.append({'role': 'user', 'content': content})
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Unknown observation type: {obs.__class__}'
|
||||
)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=self.messages,
|
||||
stop=['</execute>'],
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
],
|
||||
temperature=0.0
|
||||
)
|
||||
action_str: str = parse_response(response)
|
||||
state.num_of_chars += sum(len(message['content'])
|
||||
for message in self.messages) + len(action_str)
|
||||
state.num_of_chars += sum(
|
||||
len(message['content']) for message in self.messages
|
||||
) + len(action_str)
|
||||
self.messages.append({'role': 'assistant', 'content': action_str})
|
||||
|
||||
command = re.search(r'<execute>(.*)</execute>', action_str, re.DOTALL)
|
||||
if command is not None:
|
||||
if bash_command := re.search(r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL):
|
||||
# remove the command from the action string to get thought
|
||||
thought = action_str.replace(bash_command.group(0), '').strip()
|
||||
# a command was found
|
||||
command_group = command.group(1)
|
||||
command_group = bash_command.group(1).strip()
|
||||
if command_group.strip() == 'exit':
|
||||
return AgentFinishAction()
|
||||
return CmdRunAction(command=command_group)
|
||||
# # execute the code
|
||||
# # TODO: does exit_code get loaded into Message?
|
||||
# exit_code, observation = self.env.execute(command_group)
|
||||
# self._history.append(Message(Role.ASSISTANT, observation))
|
||||
return CmdRunAction(command=command_group, thought=thought)
|
||||
elif python_code := re.search(r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL):
|
||||
# a code block was found
|
||||
code_group = python_code.group(1).strip()
|
||||
thought = action_str.replace(python_code.group(0), '').strip()
|
||||
return IPythonRunCellAction(code=code_group, thought=thought)
|
||||
else:
|
||||
# we could provide a error message for the model to continue similar to
|
||||
# https://github.com/xingyaoww/mint-bench/blob/main/mint/envs/general_env.py#L18-L23
|
||||
# observation = INVALID_INPUT_MESSAGE
|
||||
# self._history.append(Message(Role.ASSISTANT, observation))
|
||||
return AgentEchoAction(
|
||||
content=INVALID_INPUT_MESSAGE
|
||||
) # warning message to itself
|
||||
# We assume the LLM is GOOD enough that when it returns pure natural language
|
||||
# it want to talk to the user
|
||||
return AgentTalkAction(content=action_str)
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
raise NotImplementedError('Implement this abstract method')
|
||||
|
||||
226
agenthub/codeact_agent/prompt.py
Normal file
226
agenthub/codeact_agent/prompt.py
Normal 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'
|
||||
)
|
||||
@ -1,4 +1,5 @@
|
||||
from opendevin.agent import Agent
|
||||
|
||||
from .agent import DelegatorAgent
|
||||
|
||||
Agent.register('DelegatorAgent', DelegatorAgent)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
from opendevin.agent import Agent
|
||||
|
||||
from .agent import DummyAgent
|
||||
|
||||
Agent.register('DummyAgent', DummyAgent)
|
||||
@ -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
14
agenthub/micro/README.md
Normal 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.
|
||||
@ -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
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from typing import Dict
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
instructions: Dict = {}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
all_microagents = {}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from opendevin.agent import Agent
|
||||
|
||||
from .agent import MonologueAgent
|
||||
|
||||
Agent.register('MonologueAgent', MonologueAgent)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import json
|
||||
|
||||
from json_repair import repair_json
|
||||
|
||||
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from opendevin.agent import Agent
|
||||
|
||||
from .agent import PlannerAgent
|
||||
|
||||
Agent.register('PlannerAgent', PlannerAgent)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
23
containers/app/entrypoint.sh
Executable 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"
|
||||
@ -27,3 +27,7 @@ RUN mkdir -p -m0755 /var/run/sshd
|
||||
|
||||
# symlink python3 to python
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||
|
||||
# install basic dependencies for CodeActAgent
|
||||
RUN pip3 install --upgrade pip
|
||||
RUN pip3 install jupyterlab notebook jupyter_kernel_gateway flake8
|
||||
|
||||
@ -7,6 +7,7 @@ select = [
|
||||
"E",
|
||||
"W",
|
||||
"F",
|
||||
"I",
|
||||
"Q",
|
||||
]
|
||||
|
||||
|
||||
@ -3,13 +3,12 @@ sidebar_label: agent
|
||||
title: agenthub.dummy_agent.agent
|
||||
---
|
||||
|
||||
Module for a Dummy agent.
|
||||
|
||||
## DummyAgent Objects
|
||||
|
||||
```python
|
||||
class DummyAgent(Agent)
|
||||
```
|
||||
|
||||
A dummy agent that does nothing but can be used in testing.
|
||||
The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
|
||||
without making any LLM calls.
|
||||
|
||||
|
||||
@ -12,3 +12,12 @@ class CmdOutputObservation(Observation)
|
||||
|
||||
This data class represents the output of a command.
|
||||
|
||||
## IPythonRunCellObservation Objects
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class IPythonRunCellObservation(Observation)
|
||||
```
|
||||
|
||||
This data class represents the output of a IPythonRunCellAction.
|
||||
|
||||
|
||||
@ -13,9 +13,13 @@ class ActionTypeSchema(BaseModel)
|
||||
|
||||
Initializes the agent. Only sent by client.
|
||||
|
||||
#### USER\_MESSAGE
|
||||
|
||||
Sends a message from the user. Only sent by the client.
|
||||
|
||||
#### START
|
||||
|
||||
Starts a new development task. Only sent by the client.
|
||||
Starts a new development task OR send chat from the user. Only sent by the client.
|
||||
|
||||
#### READ
|
||||
|
||||
@ -29,6 +33,10 @@ Writes the content to a file.
|
||||
|
||||
Runs a command.
|
||||
|
||||
#### RUN\_IPYTHON
|
||||
|
||||
Runs a IPython cell.
|
||||
|
||||
#### KILL
|
||||
|
||||
Kills a background command.
|
||||
@ -45,6 +53,10 @@ Searches long-term memory
|
||||
|
||||
Allows the agent to make a plan, set a goal, or record thoughts
|
||||
|
||||
#### TALK
|
||||
|
||||
Allows the agent to respond to the user.
|
||||
|
||||
#### DELEGATE
|
||||
|
||||
Delegates a task to another agent.
|
||||
|
||||
@ -21,6 +21,10 @@ The HTML content of a URL
|
||||
|
||||
The output of a command
|
||||
|
||||
#### RUN\_IPYTHON
|
||||
|
||||
Runs a IPython cell.
|
||||
|
||||
#### RECALL
|
||||
|
||||
The result of a search
|
||||
|
||||
@ -17,6 +17,10 @@ Initial state of the task.
|
||||
|
||||
The task is running.
|
||||
|
||||
#### AWAITING\_USER\_INPUT
|
||||
|
||||
The task is awaiting user input.
|
||||
|
||||
#### PAUSED
|
||||
|
||||
The task is paused.
|
||||
|
||||
@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from conftest import agents
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
||||
from opendevin import config
|
||||
|
||||
19
frontend/package-lock.json
generated
19
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-accessible-treeview": "^2.8.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-icons": "^5.0.1",
|
||||
@ -64,6 +65,7 @@
|
||||
"@types/node": "^18.0.0 ",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
|
||||
@ -15,6 +15,10 @@ const AgentStatusMap: { [k: string]: { message: string; indicator: string } } =
|
||||
message: "Agent is running task...",
|
||||
indicator: "bg-green-500",
|
||||
},
|
||||
[AgentTaskState.AWAITING_USER_INPUT]: {
|
||||
message: "Agent is awaiting user input...",
|
||||
indicator: "bg-orange-500",
|
||||
},
|
||||
[AgentTaskState.PAUSED]: {
|
||||
message: "Agent has paused.",
|
||||
indicator: "bg-yellow-500",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,13 +2,17 @@ import Editor, { Monaco } from "@monaco-editor/react";
|
||||
import { Tab, Tabs } from "@nextui-org/react";
|
||||
import type { editor } from "monaco-editor";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { VscCode } from "react-icons/vsc";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { selectFile } from "#/services/fileService";
|
||||
import { setCode } from "#/state/codeSlice";
|
||||
import { RootState } from "#/store";
|
||||
import FileExplorer from "./file-explorer/FileExplorer";
|
||||
|
||||
function CodeEditor(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [selectedFileName, setSelectedFileName] = useState("");
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@ -64,14 +68,21 @@ function CodeEditor(): JSX.Element {
|
||||
title={selectedFileName}
|
||||
/>
|
||||
</Tabs>
|
||||
<div className="flex grow">
|
||||
<Editor
|
||||
height="100%"
|
||||
path={selectedFileName.toLocaleLowerCase()}
|
||||
defaultValue=""
|
||||
value={code}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
<div className="flex grow items-center justify-center">
|
||||
{selectedFileName === "" ? (
|
||||
<div className="flex flex-col items-center text-neutral-400">
|
||||
<VscCode size={100} />
|
||||
{t(I18nKey.CODE_EDITOR$EMPTY_MESSAGE)}
|
||||
</div>
|
||||
) : (
|
||||
<Editor
|
||||
height="100%"
|
||||
path={selectedFileName.toLocaleLowerCase()}
|
||||
defaultValue=""
|
||||
value={code}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
77
frontend/src/components/Jupyter.tsx
Normal file
77
frontend/src/components/Jupyter.tsx
Normal 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(") {
|
||||
// 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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import { AllTabs, TabOption, TabType } from "#/types/TabOption";
|
||||
import Browser from "./Browser";
|
||||
import CodeEditor from "./CodeEditor";
|
||||
import Planner from "./Planner";
|
||||
import Jupyter from "./Jupyter";
|
||||
|
||||
function Workspace() {
|
||||
const { t } = useTranslation();
|
||||
@ -20,12 +21,13 @@ function Workspace() {
|
||||
const screenshotSrc = useSelector(
|
||||
(state: RootState) => state.browser.screenshotSrc,
|
||||
);
|
||||
|
||||
const jupyterCells = useSelector((state: RootState) => state.jupyter.cells);
|
||||
const [activeTab, setActiveTab] = useState<TabType>(TabOption.CODE);
|
||||
const [changes, setChanges] = useState<Record<TabType, boolean>>({
|
||||
[TabOption.PLANNER]: false,
|
||||
[TabOption.CODE]: false,
|
||||
[TabOption.BROWSER]: false,
|
||||
[TabOption.JUPYTER]: false,
|
||||
});
|
||||
|
||||
const tabData = useMemo(
|
||||
@ -45,6 +47,11 @@ function Workspace() {
|
||||
icon: <IoIosGlobe size={18} />,
|
||||
component: <Browser key="browser" />,
|
||||
},
|
||||
[TabOption.JUPYTER]: {
|
||||
name: t(I18nKey.WORKSPACE$JUPYTER_TAB_LABEL),
|
||||
icon: <VscCode size={18} />,
|
||||
component: <Jupyter key="jupyter" />,
|
||||
},
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
@ -73,6 +80,14 @@ function Workspace() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [screenshotSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== TabOption.JUPYTER && jupyterCells.length > 0) {
|
||||
// FIXME: This is a temporary solution to show the jupyter tab when the first cell is added
|
||||
// Only need to show the tab only when a cell is added
|
||||
setChanges((prev) => ({ ...prev, [TabOption.JUPYTER]: true }));
|
||||
}
|
||||
}, [jupyterCells]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-0 grow">
|
||||
<div
|
||||
|
||||
@ -3,15 +3,17 @@ import {
|
||||
IoIosArrowBack,
|
||||
IoIosArrowForward,
|
||||
IoIosRefresh,
|
||||
IoIosCloudUpload,
|
||||
} from "react-icons/io";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { WorkspaceFile, getWorkspace } from "#/services/fileService";
|
||||
import { WorkspaceFile, getWorkspace, uploadFile } from "#/services/fileService";
|
||||
import IconButton from "../IconButton";
|
||||
import ExplorerTree from "./ExplorerTree";
|
||||
import { removeEmptyNodes } from "./utils";
|
||||
|
||||
interface ExplorerActionsProps {
|
||||
onRefresh: () => void;
|
||||
onUpload: () => void;
|
||||
toggleHidden: () => void;
|
||||
isHidden: boolean;
|
||||
}
|
||||
@ -19,6 +21,7 @@ interface ExplorerActionsProps {
|
||||
function ExplorerActions({
|
||||
toggleHidden,
|
||||
onRefresh,
|
||||
onUpload,
|
||||
isHidden,
|
||||
}: ExplorerActionsProps) {
|
||||
return (
|
||||
@ -29,17 +32,30 @@ function ExplorerActions({
|
||||
)}
|
||||
>
|
||||
{!isHidden && (
|
||||
<IconButton
|
||||
icon={
|
||||
<IoIosRefresh
|
||||
size={16}
|
||||
className="text-neutral-400 hover:text-neutral-100 transition"
|
||||
/>
|
||||
}
|
||||
testId="refresh"
|
||||
ariaLabel="Refresh workspace"
|
||||
onClick={onRefresh}
|
||||
/>
|
||||
<>
|
||||
<IconButton
|
||||
icon={
|
||||
<IoIosRefresh
|
||||
size={16}
|
||||
className="text-neutral-400 hover:text-neutral-100 transition"
|
||||
/>
|
||||
}
|
||||
testId="refresh"
|
||||
ariaLabel="Refresh workspace"
|
||||
onClick={onRefresh}
|
||||
/>
|
||||
<IconButton
|
||||
icon={
|
||||
<IoIosCloudUpload
|
||||
size={16}
|
||||
className="text-neutral-400 hover:text-neutral-100 transition"
|
||||
/>
|
||||
}
|
||||
testId="upload"
|
||||
ariaLabel="Upload File"
|
||||
onClick={onUpload}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
@ -56,8 +72,8 @@ function ExplorerActions({
|
||||
/>
|
||||
)
|
||||
}
|
||||
testId="close"
|
||||
ariaLabel="Close workspace"
|
||||
testId="toggle"
|
||||
ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
|
||||
onClick={toggleHidden}
|
||||
/>
|
||||
</div>
|
||||
@ -71,12 +87,33 @@ interface FileExplorerProps {
|
||||
function FileExplorer({ onFileClick }: FileExplorerProps) {
|
||||
const [workspace, setWorkspace] = React.useState<WorkspaceFile>();
|
||||
const [isHidden, setIsHidden] = React.useState(false);
|
||||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const getWorkspaceData = async () => {
|
||||
const wsFile = await getWorkspace();
|
||||
setWorkspace(removeEmptyNodes(wsFile));
|
||||
};
|
||||
|
||||
const selectFileInput = () => {
|
||||
fileInputRef.current?.click(); // Trigger the file browser
|
||||
};
|
||||
|
||||
const uploadFileData = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files ? event.target.files[0] : null;
|
||||
if (!file) {
|
||||
console.log("No file selected.");
|
||||
return;
|
||||
}
|
||||
console.log("File selected:", file);
|
||||
try {
|
||||
const response = await uploadFile(file);
|
||||
console.log(response);
|
||||
await getWorkspaceData(); // Refresh the workspace to show the new file
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
await getWorkspaceData();
|
||||
@ -105,8 +142,15 @@ function FileExplorer({ onFileClick }: FileExplorerProps) {
|
||||
isHidden={isHidden}
|
||||
toggleHidden={() => setIsHidden((prev) => !prev)}
|
||||
onRefresh={getWorkspaceData}
|
||||
onUpload={selectFileInput}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={uploadFileData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import LoadPreviousSessionModal from "./LoadPreviousSessionModal";
|
||||
import { clearMsgs, fetchMsgs } from "../../../services/session";
|
||||
import { sendChatMessageFromEvent } from "../../../services/chatService";
|
||||
import { addChatMessageFromEvent } from "../../../services/chatService";
|
||||
import { handleAssistantMessage } from "../../../services/actions";
|
||||
import toast from "../../../utils/toast";
|
||||
|
||||
@ -37,7 +37,7 @@ vi.mock("../../../services/session", async (importOriginal) => ({
|
||||
|
||||
vi.mock("../../../services/chatService", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../../services/chatService")>()),
|
||||
sendChatMessageFromEvent: vi.fn(),
|
||||
addChatMessageFromEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../services/actions", async (importOriginal) => ({
|
||||
@ -94,7 +94,7 @@ describe("LoadPreviousSession", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMsgs).toHaveBeenCalledTimes(1);
|
||||
expect(sendChatMessageFromEvent).toHaveBeenCalledTimes(1);
|
||||
expect(addChatMessageFromEvent).toHaveBeenCalledTimes(1);
|
||||
expect(handleAssistantMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
// modal should close right after fetching messages
|
||||
@ -117,7 +117,7 @@ describe("LoadPreviousSession", () => {
|
||||
await waitFor(async () => {
|
||||
await expect(() => fetchMsgs()).rejects.toThrow();
|
||||
expect(handleAssistantMessage).not.toHaveBeenCalled();
|
||||
expect(sendChatMessageFromEvent).not.toHaveBeenCalled();
|
||||
expect(addChatMessageFromEvent).not.toHaveBeenCalled();
|
||||
// error toast should be shown
|
||||
expect(toast.stickyError).toHaveBeenCalledWith(
|
||||
"ws",
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
import { sendChatMessageFromEvent } from "#/services/chatService";
|
||||
import { addChatMessageFromEvent } from "#/services/chatService";
|
||||
import { clearMsgs, fetchMsgs } from "#/services/session";
|
||||
import toast from "#/utils/toast";
|
||||
import BaseModal from "../base-modal/BaseModal";
|
||||
@ -28,7 +28,7 @@ function LoadPreviousSessionModal({
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.role === "user") {
|
||||
sendChatMessageFromEvent(message.payload);
|
||||
addChatMessageFromEvent(message.payload);
|
||||
}
|
||||
|
||||
if (message.role === "assistant") {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Settings } from "#/services/settings";
|
||||
import AgentTaskState from "#/types/AgentTaskState";
|
||||
import { act, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import AgentTaskState from "#/types/AgentTaskState";
|
||||
import { Settings } from "#/services/settings";
|
||||
import SettingsForm from "./SettingsForm";
|
||||
|
||||
const onModelChangeMock = vi.fn();
|
||||
@ -106,7 +106,6 @@ describe("SettingsForm", () => {
|
||||
});
|
||||
|
||||
expect(onModelChangeMock).toHaveBeenCalledWith("model3");
|
||||
expect(onAPIKeyChangeMock).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("should call the onAgentChange handler when the agent changes", () => {
|
||||
|
||||
@ -38,7 +38,8 @@ function SettingsForm({
|
||||
useEffect(() => {
|
||||
if (
|
||||
curTaskState === AgentTaskState.RUNNING ||
|
||||
curTaskState === AgentTaskState.PAUSED
|
||||
curTaskState === AgentTaskState.PAUSED ||
|
||||
curTaskState === AgentTaskState.AWAITING_USER_INPUT
|
||||
) {
|
||||
setDisabled(true);
|
||||
} else {
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { fetchAgents, fetchModels } from "#/api";
|
||||
import { initializeAgent } from "#/services/agent";
|
||||
import { Settings, getSettings, saveSettings } from "#/services/settings";
|
||||
import toast from "#/utils/toast";
|
||||
import { act, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { Mock } from "vitest";
|
||||
import toast from "#/utils/toast";
|
||||
import { Settings, getSettings, saveSettings } from "#/services/settings";
|
||||
import { initializeAgent } from "#/services/agent";
|
||||
import { fetchAgents, fetchModels } from "#/api";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
|
||||
const toastSpy = vi.spyOn(toast, "settingsChanged");
|
||||
@ -129,6 +129,7 @@ describe("SettingsModal", () => {
|
||||
expect(saveSettings).toHaveBeenCalledWith({
|
||||
...initialSettings,
|
||||
LLM_MODEL: "model3",
|
||||
LLM_API_KEY: "", // reset after model change
|
||||
});
|
||||
});
|
||||
|
||||
@ -160,6 +161,7 @@ describe("SettingsModal", () => {
|
||||
expect(initializeAgent).toHaveBeenCalledWith({
|
||||
...initialSettings,
|
||||
LLM_MODEL: "model3",
|
||||
LLM_API_KEY: "", // reset after model change
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import { Spinner } from "@nextui-org/react";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchAgents, fetchModels } from "#/api";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
@ -9,10 +13,6 @@ import {
|
||||
saveSettings,
|
||||
} from "#/services/settings";
|
||||
import toast from "#/utils/toast";
|
||||
import { Spinner } from "@nextui-org/react";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import BaseModal from "../base-modal/BaseModal";
|
||||
import SettingsForm from "./SettingsForm";
|
||||
|
||||
@ -76,8 +76,14 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
i18next.changeLanguage(settings.LANGUAGE);
|
||||
initializeAgent(settings); // reinitialize the agent with the new settings
|
||||
|
||||
const sensitiveKeys = ['LLM_API_KEY'];
|
||||
|
||||
Object.entries(updatedSettings).forEach(([key, value]) => {
|
||||
toast.settingsChanged(`${key} set to "${value}"`);
|
||||
if (!sensitiveKeys.includes(key)) {
|
||||
toast.settingsChanged(`${key} set to "${value}"`);
|
||||
} else {
|
||||
toast.settingsChanged(`${key} has been updated securely.`);
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
@ -86,6 +92,13 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const isDisabled =
|
||||
Object.entries(settings)
|
||||
// filter api key
|
||||
.filter(([key]) => key !== "LLM_API_KEY")
|
||||
.some(([, value]) => !value) ||
|
||||
JSON.stringify(settings) === JSON.stringify(currentSettings);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
@ -96,9 +109,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
|
||||
action: handleSaveSettings,
|
||||
isDisabled:
|
||||
Object.values(settings).some((value) => !value) ||
|
||||
JSON.stringify(settings) === JSON.stringify(currentSettings),
|
||||
isDisabled,
|
||||
closeAfterAction: true,
|
||||
className: "bg-primary rounded-lg",
|
||||
},
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)));
|
||||
},
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -17,15 +17,19 @@ const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[];
|
||||
/**
|
||||
* Get the settings from local storage or use the default settings if not found
|
||||
*/
|
||||
export const getSettings = (): Settings => ({
|
||||
LLM_MODEL: localStorage.getItem("LLM_MODEL") || DEFAULT_SETTINGS.LLM_MODEL,
|
||||
AGENT: localStorage.getItem("AGENT") || DEFAULT_SETTINGS.AGENT,
|
||||
LANGUAGE: localStorage.getItem("LANGUAGE") || DEFAULT_SETTINGS.LANGUAGE,
|
||||
LLM_API_KEY:
|
||||
localStorage.getItem(
|
||||
`API_KEY_${localStorage.getItem("LLM_MODEL") || DEFAULT_SETTINGS.LLM_MODEL}`,
|
||||
) || DEFAULT_SETTINGS.LLM_API_KEY,
|
||||
});
|
||||
export const getSettings = (): Settings => {
|
||||
const model = localStorage.getItem("LLM_MODEL");
|
||||
const agent = localStorage.getItem("AGENT");
|
||||
const language = localStorage.getItem("LANGUAGE");
|
||||
const apiKey = localStorage.getItem(`API_KEY_${model}`);
|
||||
|
||||
return {
|
||||
LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL,
|
||||
AGENT: agent || DEFAULT_SETTINGS.AGENT,
|
||||
LANGUAGE: language || DEFAULT_SETTINGS.LANGUAGE,
|
||||
LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the settings to local storage. Only valid settings are saved.
|
||||
|
||||
27
frontend/src/state/jupyterSlice.ts
Normal file
27
frontend/src/state/jupyterSlice.ts
Normal 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;
|
||||
@ -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({
|
||||
|
||||
@ -2,7 +2,10 @@ enum ActionType {
|
||||
// Initializes the agent. Only sent by client.
|
||||
INIT = "initialize",
|
||||
|
||||
// Starts a new development task. Only sent by the client.
|
||||
// Sends a message from the user
|
||||
USER_MESSAGE = "user_message",
|
||||
|
||||
// Starts a new development task
|
||||
START = "start",
|
||||
|
||||
// Reads the contents of a file.
|
||||
@ -14,6 +17,9 @@ enum ActionType {
|
||||
// Runs a command.
|
||||
RUN = "run",
|
||||
|
||||
// Runs a IPython command.
|
||||
RUN_IPYTHON = "run_ipython",
|
||||
|
||||
// Kills a background command.
|
||||
KILL = "kill",
|
||||
|
||||
@ -26,6 +32,9 @@ enum ActionType {
|
||||
// Allows the agent to make a plan, set a goal, or record thoughts.
|
||||
THINK = "think",
|
||||
|
||||
// Allows the agent to respond to the user. Only sent by the agent.
|
||||
TALK = "talk",
|
||||
|
||||
// If you're absolutely certain that you've completed your task and have tested your work,
|
||||
// use the finish action to stop working.
|
||||
FINISH = "finish",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
enum AgentTaskState {
|
||||
INIT = "init",
|
||||
RUNNING = "running",
|
||||
AWAITING_USER_INPUT = "awaiting_user_input",
|
||||
PAUSED = "paused",
|
||||
STOPPED = "stopped",
|
||||
FINISHED = "finished",
|
||||
|
||||
@ -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",
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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'
|
||||
]
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from .agent_controller import AgentController
|
||||
from .action_manager import ActionManager
|
||||
from .agent_controller import AgentController
|
||||
|
||||
__all__ = [
|
||||
'AgentController',
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
|
||||
from opendevin.schema import ObservationType
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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.'
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user