Compare commits

..

3 Commits

Author SHA1 Message Date
Alex Bäuerle a2b49525ae install markdown 2024-05-01 11:06:55 -07:00
Alex Bäuerle 478ebedd41 Merge remote-tracking branch 'origin' into ab-docs-remove 2024-05-01 10:35:11 -07:00
Alex Bäuerle 016a56a3d9 ci(docs): remove and rebuild docs to make sure we're getting rid of files that are not there anymore
This is not ideal in that we have to regenerate the whole docs everytime but considering 1. it's pretty fast and 2. it seems to be the only way to ensure consistency it's probably worth doing it this way.
2024-05-01 10:32:09 -07:00
618 changed files with 15417 additions and 32665 deletions
-19
View File
@@ -1,19 +0,0 @@
codecov:
notify:
wait_for_ci: true
coverage:
status:
patch:
default:
threshold: 100% # allow patch coverage to be lower than project coverage by any amount
project:
default:
threshold: 5% # allow project coverage to drop at most 5%
comment: false
github_checks:
annotations: false
ignore:
- "agenthub/SWE_agent/**" # SWE agent is deprecated
+3 -9
View File
@@ -28,8 +28,8 @@ body:
- type: textarea
id: current-version
attributes:
label: Current OpenDevin version
description: What version of OpenDevin are you using? If you're running in docker, tell us the tag you're using (e.g. ghcr.io/opendevin/opendevin:0.3.1).
label: Current Version
description: What version are you using? If you're running in docker, tell us the tag you're using (e.g. ghcr.io/opendevin/opendevin:0.3.1).
render: bash
validations:
required: true
@@ -52,12 +52,6 @@ body:
- Model:
- Agent:
- type: textarea
id: os-version
attributes:
label: Operating System
description: What Operating System are you using? Linux, Mac OS, WSL on Windows
- type: textarea
id: repro-steps
attributes:
@@ -72,4 +66,4 @@ body:
id: additional-context
attributes:
label: Logs, Errors, Screenshots, and Additional Context
description: LLM logs will be stored in the `logs/llm/default` folder. Please add any additional context about the problem here.
description: Please add any additional context about the problem here.
-15
View File
@@ -1,15 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "npm" # See documentation for possible values
directory: "/frontend" # Location of package manifests
schedule:
interval: "daily"
+6 -10
View File
@@ -12,7 +12,9 @@ jobs:
build:
name: Build Docusaurus
runs-on: ubuntu-latest
if: github.repository == 'OpenDevin/OpenDevin'
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
with:
@@ -22,17 +24,11 @@ jobs:
node-version: 18
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Generate Python Docs
run: rm -rf docs/modules/python && pip install pydoc-markdown && pydoc-markdown
- name: Install dependencies
run: cd docs && npm ci
run: npm ci
- name: Build website
run: cd docs && npm run build
run: npm run build
- name: Upload Build Artifact
if: github.ref == 'refs/heads/main'
@@ -43,7 +39,7 @@ jobs:
deploy:
name: Deploy to GitHub Pages
needs: build
if: github.ref == 'refs/heads/main' && github.repository == 'OpenDevin/OpenDevin'
if: github.ref == 'refs/heads/main'
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
+2 -14
View File
@@ -1,17 +1,6 @@
name: Run e2e test with dummy agent
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
on:
push:
branches:
- main
pull_request:
env:
PERSIST_SANDBOX : "false"
on: [push]
jobs:
test:
@@ -26,8 +15,7 @@ jobs:
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install --without evaluation
poetry run playwright install --with-deps chromium
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/core/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent
poetry run python opendevin/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent
+17 -23
View File
@@ -1,15 +1,9 @@
name: Publish Docker Image
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
on:
push:
branches:
- main
tags:
- '*'
pull_request:
workflow_dispatch:
inputs:
@@ -28,12 +22,26 @@ jobs:
strategy:
matrix:
image: ["app", "sandbox"]
image: ["app", "evaluation", "sandbox"]
steps:
- name: checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
@@ -50,27 +58,13 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push ${{ matrix.image }}
if: "!github.event.pull_request.head.repo.fork"
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} --push
- name: Build ${{ matrix.image }}
if: "github.event.pull_request.head.repo.fork"
if: github.event.pull_request.head.repo.full_name != github.repository
run: |
./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }}
+1 -10
View File
@@ -1,9 +1,5 @@
name: Lint
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
on:
push:
branches:
@@ -37,8 +33,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up python
uses: actions/setup-python@v5
with:
@@ -47,11 +41,8 @@ jobs:
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Run pre-commit hooks
if: github.ref != 'refs/heads/main'
run: |
git fetch https://github.com/OpenDevin/OpenDevin.git main:main && \
pre-commit run \
--files \
$(git diff --name-only $(git merge-base main $(git branch --show-current)) $(git branch --show-current) | tr '\n' ' ') \
--all-files \
--show-diff-on-failure \
--config ./dev_config/python/.pre-commit-config.yaml
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SANDBOX_TYPE: exec
run: |
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
rm task.txt
- name: Check if review file is non-empty
+33 -65
View File
@@ -1,32 +1,34 @@
name: Run Integration Tests
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
on:
push:
branches:
- main
paths-ignore:
- '**/*.md'
- 'frontend/**'
- 'docs/**'
- 'evaluation/**'
- main
pull_request:
env:
PERSIST_SANDBOX : "false"
jobs:
integration-tests-on-linux:
name: Integration Tests on Linux
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
sandbox: ["ssh", "exec", "local"]
agent: ["SWEAgent", "PlannerAgent", "MonologueAgent", "CodeActAgent"]
sandbox: ["ssh", "exec"]
include:
- agent: "MonologueAgent"
embedding-model: "local"
- agent: "MonologueAgent"
# sufficient to have one agent testing against local sandbox
sandbox: "local"
embedding-model: "local"
- agent: "SWEAgent"
embedding-model: "none"
- agent: "PlannerAgent"
embedding-model: "none"
- agent: "CodeActAgent"
embedding-model: "none"
steps:
- uses: actions/checkout@v4
@@ -36,7 +38,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: '3.11'
cache: 'poetry'
- name: Install Python dependencies using Poetry
@@ -48,57 +50,23 @@ jobs:
- name: Run Integration Tests
env:
SANDBOX_TYPE: ${{ matrix.sandbox }}
AGENT: ${{ matrix.agent }}
MAX_ITERATIONS: 10
LLM_EMBEDDING_MODEL: ${{ matrix.embedding-model }}
run: |
TEST_IN_CI=true TEST_ONLY=true ./tests/integration/regenerate.sh
rm -rf workspace
mkdir workspace
WORKSPACE_BASE="$GITHUB_WORKSPACE/workspace" \
WORKSPACE_MOUNT_PATH="$GITHUB_WORKSPACE/workspace" \
poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml \
-s ./tests/integration
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
integration-tests-on-mac:
name: Integration Tests on MacOS
runs-on: macos-13
if: contains(github.event.pull_request.title, 'mac') || contains(github.event.pull_request.title, 'Mac')
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
sandbox: ["ssh"]
test_matrix_success:
name: All Integration Tests Passed
runs-on: ubuntu-latest
needs: [integration-tests]
steps:
- uses: actions/checkout@v4
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install
- name: Install & Start Docker
run: |
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 Integration Tests
env:
SANDBOX_TYPE: ${{ matrix.sandbox }}
run: |
TEST_IN_CI=true TEST_ONLY=true ./tests/integration/regenerate.sh
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- run: echo Done!
+9 -50
View File
@@ -1,29 +1,15 @@
name: Run Unit Tests
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
on:
push:
branches:
- main
paths-ignore:
- '**/*.md'
- 'frontend/**'
- 'docs/**'
- 'evaluation/**'
- main
pull_request:
env:
PERSIST_SANDBOX : "false"
jobs:
test-on-macos:
name: Test on macOS
runs-on: macos-13
env:
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ["3.11"]
@@ -38,13 +24,12 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install
- name: Install & Start Docker
if: env.INSTALL_DOCKER == '1'
run: |
brew install colima docker
colima start
@@ -57,7 +42,7 @@ jobs:
run: make build
- name: Run Tests
run: poetry run pytest --forked --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox"
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
@@ -66,8 +51,6 @@ jobs:
test-on-linux:
name: Test on Linux
runs-on: ubuntu-latest
env:
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ["3.11"]
@@ -82,7 +65,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation
@@ -91,39 +74,15 @@ jobs:
run: make build
- name: Run Tests
run: poetry run pytest --forked --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox"
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-for-sandbox:
name: Test for Sandbox
test_matrix_success:
name: All Mac/Linux Tests Passed
runs-on: ubuntu-latest
needs: [test-on-macos, test-on-linux]
steps:
- uses: actions/checkout@v4
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "poetry"
- name: Install Python dependencies using Poetry
run: poetry install
- name: Build Environment
run: make build
- name: Run Integration Test for Sandbox
run: |
poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml -s ./tests/unit/test_sandbox.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- run: echo Done!
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SANDBOX_TYPE: exec
run: |
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
rm task.txt
- name: Setup Git, Create Branch, and Commit Changes
-8
View File
@@ -126,7 +126,6 @@ env/
venv/
ENV/
env.bak/
.env.bak
venv.bak/
*venv/
@@ -197,15 +196,8 @@ logs
# agent
.envrc
/workspace
/_test_workspace
/debug
cache
# configuration
config.toml
config.toml.bak
evaluation/swe_bench/eval_workspace*
evaluation/outputs
evaluation/evaluation_outputs
test_results*
/_test_files_tmp/
+2 -8
View File
@@ -5,8 +5,8 @@ This guide is for people working on OpenDevin and editing the source code.
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install)
* [Docker](https://docs.docker.com/engine/install/) (For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
* [Python](https://www.python.org/downloads/) = 3.11
* [Docker](https://docs.docker.com/engine/install/)(For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
* [Python](https://www.python.org/downloads/) >= 3.11
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
@@ -45,7 +45,6 @@ To configure the LM of your choice, follow these steps:
make setup-config
```
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenDevin is tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI, please set the model in the UI.
Set `persist_sandbox` to false if you want to use clean sandbox for each task. If `persist_sandbox` is set to true, you will need to set the `ssh_password` as well.
**Note on Alternative Models:**
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest. And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to [share your setup instructions with us](https://github.com/OpenDevin/OpenDevin/issues/417).
@@ -95,8 +94,3 @@ poetry run pytest ./tests/unit/test_sandbox.py
#### Integration tests
Please refer to [this README](./tests/integration/README.md) for details.
### 9. Add or update dependency
1. Add your dependency in `pyproject.toml` or use `peotry add xxx`
2. Update the poetry.lock file via `poetry lock --no-update`
+30 -69
View File
@@ -7,7 +7,7 @@ BACKEND_PORT = 3000
BACKEND_HOST = "127.0.0.1:$(BACKEND_PORT)"
FRONTEND_PORT = 3001
DEFAULT_WORKSPACE_DIR = "./workspace"
DEFAULT_MODEL = "gpt-4o"
DEFAULT_MODEL = "gpt-3.5-turbo-1106"
CONFIG_FILE = config.toml
PRECOMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
@@ -22,9 +22,7 @@ RESET=$(shell tput -Txterm sgr0)
build:
@echo "$(GREEN)Building project...$(RESET)"
@$(MAKE) -s check-dependencies
ifeq ($(INSTALL_DOCKER),)
@$(MAKE) -s pull-docker-image
endif
@$(MAKE) -s install-python-dependencies
@$(MAKE) -s install-frontend-dependencies
@$(MAKE) -s install-precommit-hooks
@@ -37,9 +35,7 @@ check-dependencies:
@$(MAKE) -s check-python
@$(MAKE) -s check-npm
@$(MAKE) -s check-nodejs
ifeq ($(INSTALL_DOCKER),)
@$(MAKE) -s check-docker
endif
@$(MAKE) -s check-poetry
@echo "$(GREEN)Dependencies checked successfully.$(RESET)"
@@ -48,11 +44,7 @@ check-system:
@if [ "$(shell uname)" = "Darwin" ]; then \
echo "$(BLUE)macOS detected.$(RESET)"; \
elif [ "$(shell uname)" = "Linux" ]; then \
if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Manjaro Linux detected.$(RESET)"; \
else \
echo "$(BLUE)Linux detected.$(RESET)"; \
fi; \
echo "$(BLUE)Linux detected.$(RESET)"; \
elif [ "$$(uname -r | grep -i microsoft)" ]; then \
echo "$(BLUE)Windows Subsystem for Linux detected.$(RESET)"; \
else \
@@ -130,20 +122,13 @@ pull-docker-image:
install-python-dependencies:
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
poetry env use python3.11
@if [ "$(shell uname)" = "Darwin" ]; then \
echo "$(BLUE)Installing chroma-hnswlib...$(RESET)"; \
echo "$(BLUE)Installing `chroma-hnswlib`...$(RESET)"; \
export HNSWLIB_NO_NATIVE=1; \
poetry run pip install chroma-hnswlib; \
fi
@poetry install
@if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
poetry run pip install playwright; \
poetry run playwright install chromium; \
else \
poetry run playwright install --with-deps chromium; \
fi
@poetry install --without evaluation
@poetry run playwright install --with-deps chromium
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
install-frontend-dependencies:
@@ -163,24 +148,9 @@ install-precommit-hooks:
@poetry run pre-commit install --config $(PRECOMMIT_CONFIG_PATH)
@echo "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
lint-backend:
@echo "$(YELLOW)Running linters...$(RESET)"
@poetry run pre-commit run --files $$(git diff --name-only $$(git merge-base main $$(git branch --show-current)) $$(git branch --show-current) | tr '\n' ' ') --show-diff-on-failure --config $(PRECOMMIT_CONFIG_PATH)
lint-frontend:
@echo "$(YELLOW)Running linters for frontend...$(RESET)"
@cd frontend && npm run lint
lint:
@$(MAKE) -s lint-frontend
@$(MAKE) -s lint-backend
test-frontend:
@echo "$(YELLOW)Running tests for frontend...$(RESET)"
@cd frontend && npm run test
test:
@$(MAKE) -s test-frontend
@echo "$(YELLOW)Running linters...$(RESET)"
@poetry run pre-commit run --all-files --show-diff-on-failure --config $(PRECOMMIT_CONFIG_PATH)
build-frontend:
@echo "$(YELLOW)Building frontend...$(RESET)"
@@ -189,7 +159,7 @@ build-frontend:
# Start backend
start-backend:
@echo "$(YELLOW)Starting backend...$(RESET)"
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) --reload --reload-exclude "workspace/*"
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) --reload --reload-exclude workspace/*
# Start frontend
start-frontend:
@@ -220,33 +190,15 @@ setup-config:
@echo "$(GREEN)Config.toml setup completed.$(RESET)"
setup-config-prompts:
@echo "[core]" > $(CONFIG_FILE).tmp
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "workspace_base=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
@read -p "Do you want to persist the sandbox container? [true/false] [default: true]: " persist_sandbox; \
persist_sandbox=$${persist_sandbox:-true}; \
if [ "$$persist_sandbox" = "true" ]; then \
read -p "Enter a password for the sandbox container: " ssh_password; \
echo "ssh_password=\"$$ssh_password\"" >> $(CONFIG_FILE).tmp; \
else \
echo "persist_sandbox=\"$$persist_sandbox\"" >> $(CONFIG_FILE).tmp
fi
@echo "" >> $(CONFIG_FILE).tmp
@echo "[llm]" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
@read -p "Enter your LLM Model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
llm_model=$${llm_model:-$(DEFAULT_MODEL)}; \
echo "model=\"$$llm_model\"" >> $(CONFIG_FILE).tmp
echo "LLM_MODEL=\"$$llm_model\"" > $(CONFIG_FILE).tmp
@read -p "Enter your LLM api key: " llm_api_key; \
echo "api_key=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
@read -p "Enter your LLM API key: " llm_api_key; \
echo "LLM_API_KEY=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
@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 "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@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"; \
echo "Choices are:"; \
@@ -260,19 +212,22 @@ setup-config-prompts:
echo " - stable-code"; \
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
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 "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
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 "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
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; \
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
echo "LLM_EMBEDDING_DEPLOYMENT_NAME=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure API Version: " llm_api_version; \
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
echo "LLM_API_VERSION=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
fi
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "WORKSPACE_BASE=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
# Clean up all caches
clean:
@@ -280,6 +235,12 @@ clean:
@rm -rf opendevin/.cache
@echo "$(GREEN)Caches cleaned up successfully.$(RESET)"
python-docs:
@echo "$(YELLOW)Generating documentation...$(RESET)"
@rm -rf docs/modules/python
@pydoc-markdown
@echo "$(GREEN)Documentation generated successfully.$(RESET)"
# Help
help:
@echo "$(BLUE)Usage: make [target]$(RESET)"
+53 -83
View File
@@ -18,109 +18,79 @@
-->
<div align="center">
<a href="https://github.com/OpenDevin/OpenDevin/graphs/contributors"><img src="https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/OpenDevin/OpenDevin/network/members"><img src="https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge&color=blue" alt="Forks"></a>
<a href="https://github.com/OpenDevin/OpenDevin/stargazers"><img src="https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/OpenDevin/OpenDevin/issues"><img src="https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge&color=blue" alt="Issues"></a>
<a href="https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2i1iqdag6-bVmvamiPA9EZUu7oCO6KhA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
<br/>
<a href="https://huggingface.co/spaces/OpenDevin/evaluation"><img src="https://img.shields.io/badge/SWE--bench%20Lite-25.0%25-green?style=for-the-badge" alt="SWE-bench "></a>
<a href="https://codecov.io/github/opendevin/opendevin?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/opendevin/opendevin?style=for-the-badge"></a>
<a href="https://github.com/OpenDevin/OpenDevin/graphs/contributors"><img src="https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge" alt="Contributors"></a>
<a href="https://github.com/OpenDevin/OpenDevin/network/members"><img src="https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge" alt="Forks"></a>
<a href="https://github.com/OpenDevin/OpenDevin/stargazers"><img src="https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge" alt="Stargazers"></a>
<a href="https://github.com/OpenDevin/OpenDevin/issues"><img src="https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge" alt="Issues"></a>
<a href="https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge" alt="MIT License"></a>
</br>
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2etftj1dd-X1fDL2PYIVpsmJZkqEYANw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/mBuDGRzzES"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
</div>
<!-- PROJECT LOGO -->
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200" height="200">
<h1 align="center">OpenDevin: Code Less, Make More</h1>
<a href="https://opendevin.github.io/OpenDevin/"><img src="https://img.shields.io/badge/Documentation-OpenDevin-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
</div>
<hr>
Welcome to OpenDevin, a platform for autonomous software engineers, powered by AI and LLMs.
## 🎯 Mission
OpenDevin agents collaborate with human developers to write code, fix bugs, and ship features.
Welcome to OpenDevin, an open-source project aiming to replicate Devin, an autonomous AI software engineer who is capable of executing complex engineering tasks and collaborating actively with users on software development projects. This project aspires to replicate, enhance, and innovate upon Devin through the power of the open-source community.
![App screenshot](./docs/static/img/screenshot.png)
To learn more and to use OpenDevin, check out our [documentation](https://opendevin.github.io/OpenDevin/).
## ⚡ Getting Started
The easiest way to run OpenDevin is inside a Docker container. It works best with the most recent version of Docker, `26.0.0`.
You must be using Linux, Mac OS, or WSL on Windows.
To start the app, run these commands, replacing `$(pwd)/workspace` with the directory you want OpenDevin to work with.
> [!WARNING]
> OpenDevin runs bash commands within a Docker sandbox, so it should not affect your machine.
> But your workspace directory will be attached to that sandbox, and files in the directory may be modified or deleted.
```bash
# The directory you want OpenDevin to work with. MUST be an absolute path!
export WORKSPACE_BASE=$(pwd)/workspace;
```
```bash
docker run -it \
--pull=always \
-e SANDBOX_USER_ID=$(id -u) \
-e PERSIST_SANDBOX="true" \
-e SSH_PASSWORD="make something up here" \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
ghcr.io/opendevin/opendevin:0.5
```
You'll find OpenDevin running at [http://localhost:3000](http://localhost:3000).
## 🚀 Documentation
To learn more about the project, and for tips on using OpenDevin,
**check out our [documentation](https://opendevin.github.io/OpenDevin/)**.
There you'll find resources on how to use different LLM providers (like ollama and Anthropic's Claude),
troubleshooting resources, and advanced configuration options.
## 🤝 How to Contribute
OpenDevin is a community-driven project, and we welcome contributions from everyone.
Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of
software engineering with AI, there are many ways to get involved:
- **Code Contributions:** Help us develop new agents, core functionality, the frontend and other interfaces, or sandboxing solutions.
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements.
- **Feedback and Testing:** Use the OpenDevin toolset, report bugs, suggest features, or provide feedback on usability.
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
## 🤖 Join Our Community
Whether you're a developer, a researcher, or simply enthusiastic about OpenDevin, we'd love to have you in our community.
Let's make software engineering better together!
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2ggtwn3k5-PvAA2LUmqGHVZ~XzGq~ILw) - Here we talk about research, architecture, and future development.
- [Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
## 📈 Progress
<p align="center">
<a href="https://www.swebench.com/lite.html">
<img src="/docs/static/img/results.png" alt="SWE-Bench Lite Score" width="500" height="auto">
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>
<p align="center">
<a href="https://star-history.com/#OpenDevin/OpenDevin&Date">
<img src="https://api.star-history.com/svg?repos=OpenDevin/OpenDevin&type=Date" width="500" alt="Star History Chart">
</a>
## 🤝 How to Contribute
OpenDevin is a community-driven project, and we welcome contributions from everyone. Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of software engineering with AI, there are many ways to get involved:
- **Code Contributions:** Help us develop the core functionalities, frontend interface, or sandboxing solutions.
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements.
- **Feedback and Testing:** Use the OpenDevin toolset, report bugs, suggest features, or provide feedback on usability.
For details, please check [this document](./CONTRIBUTING.md).
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>
## 🤖 Join Our Community
Now we have both Slack workspace for the collaboration on building OpenDevin and Discord server for discussion about anything related, e.g., this project, LLM, agent, etc.
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2etftj1dd-X1fDL2PYIVpsmJZkqEYANw)
- [Discord server](https://discord.gg/mBuDGRzzES)
If you would love to contribute, feel free to join our community (note that now there is no need to fill in the [form](https://forms.gle/758d5p6Ve8r2nxxq6)). Let's simplify software engineering together!
🐚 **Code less, make more with OpenDevin.**
[![Star History Chart](https://api.star-history.com/svg?repos=OpenDevin/OpenDevin&type=Date)](https://star-history.com/#OpenDevin/OpenDevin&Date)
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>
## 📜 License
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>
[contributors-shield]: https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge
[contributors-url]: https://github.com/OpenDevin/OpenDevin/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge
+7 -21
View File
@@ -16,18 +16,14 @@ Every agent also has a `self.llm` which it can use to interact with the LLM conf
See the [LiteLLM docs for `self.llm.completion`](https://docs.litellm.ai/docs/completion).
## State
The `state` contains:
- A history of actions taken by the agent, as well as any observations (e.g. file content, command output) from those actions
- A list of actions/observations that have happened since the most recent step
- A [`root_task`](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/controller/state/task.py), which contains a plan of action
- The agent can add and modify subtasks through the `AddTaskAction` and `ModifyTaskAction`
* A history of actions taken by the agent, as well as any observations (e.g. file content, command output) from those actions
* A list of actions/observations that have happened since the most recent step
* A [`plan`](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/plan.py), which contains the main goal
* The agent can add and modify subtasks through the `AddTaskAction` and `ModifyTaskAction`
## Actions
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.
@@ -39,50 +35,40 @@ Here is a list of available Actions, which can be returned by `agent.step()`:
- [`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/delegator agent to enter a new task
- [`AgentRejectAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user to enter a new task
- [`MessageAction`](../opendevin/action/message.py) - Represents a message from an agent or the user
You can use `action.to_dict()` and `action_from_dict` to serialize and deserialize actions.
## Observations
There are also several types of Observations. These are typically available in the step following the corresponding Action.
But they may also appear as a result of asynchronous events (e.g. a message from the user, logs from a command running
in the background).
Here is a list of available Observations:
- [`CmdOutputObservation`](../opendevin/observation/run.py)
- [`BrowserOutputObservation`](../opendevin/observation/browse.py)
- [`FileReadObservation`](../opendevin/observation/files.py)
- [`FileWriteObservation`](../opendevin/observation/files.py)
- [`UserMessageObservation`](../opendevin/observation/)
- [`AgentRecallObservation`](../opendevin/observation/recall.py)
- [`ErrorObservation`](../opendevin/observation/error.py)
- [`SuccessObservation`](../opendevin/observation/success.py)
- [`AgentErrorObservation`](../opendevin/observation/error.py)
You can use `observation.to_dict()` and `observation_from_dict` to serialize and deserialize observations.
## Interface
Every agent must implement the following methods:
### `step`
```
def step(self, state: "State") -> "Action"
```
`step` moves the agent forward one step towards its goal. This probably means
sending a prompt to the LLM, then parsing the response into an `Action`.
### `search_memory`
```
def search_memory(self, query: str) -> list[str]:
def search_memory(self, query: str) -> List[str]:
```
`search_memory` should return a list of events that match the query. This will be used
for the `recall` action.
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .agent import SWEAgent
+35 -38
View File
@@ -1,13 +1,15 @@
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import (
from typing import List
from opendevin.action import (
Action,
AgentThinkAction,
FileReadAction,
FileWriteAction,
MessageAction,
)
from opendevin.events.serialization.event import event_to_memory
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 (
@@ -20,8 +22,6 @@ from .prompts import (
class SWEAgent(Agent):
VERSION = '1.0'
DEPRECATED = True
"""
An attempt to recreate swe_agent with output parsing, prompting style, and Application Computer Interface (ACI).
@@ -32,22 +32,25 @@ class SWEAgent(Agent):
super().__init__(llm)
self.memory_window = 4
self.max_retries = 2
self.running_memory: List[str] = []
self.cur_file: str = ''
self.cur_line: int = 0
def _think_act(self, messages: list[dict]) -> tuple[Action, str]:
resp = self.llm.do_completion(
def _remember(self, action: Action, observation: Observation) -> None:
"""Agent has a limited memory of the few steps implemented as a queue"""
memory = MEMORY_FORMAT(action.to_memory(), observation.to_memory())
self.running_memory.append(memory)
def _think_act(self, messages: List[dict]) -> tuple[Action, str]:
resp = self.llm.completion(
messages=messages,
temperature=0.05,
)
action_resp = resp['choices'][0]['message']['content']
print(f"\033[1m\033[91m{resp['usage']}\033[0m")
print(
'\n==== RAW OUTPUT ====',
f'\033[96m{action_resp}\033[0m',
'==== END RAW ====\n',
sep='\n',
)
print('\n==== RAW OUTPUT ====',
f'\033[96m{action_resp}\033[0m',
'==== END RAW ====\n', sep='\n')
return parse_command(action_resp, self.cur_file, self.cur_line)
def _update(self, action: Action) -> None:
@@ -62,36 +65,30 @@ class SWEAgent(Agent):
2. Perform think-act - prompt model for action and reasoning
3. Catch errors - ensure model takes action (5 attempts max)
"""
# retrieve short term memories from state.history, up to memory_window
memory_window = min(self.memory_window, len(state.history))
running_memory: list[str] = []
for prev_action, obs in state.history[-memory_window:]:
running_memory.append(
MEMORY_FORMAT(event_to_memory(prev_action), event_to_memory(obs))
)
for prev_action, obs in state.updated_info:
self._remember(prev_action, obs)
goal = state.get_current_user_intent()
prompt = STEP_PROMPT(
state.plan.main_goal,
self.cur_file,
self.cur_line
)
# always in the prompt if they exist: file and line
prompt = STEP_PROMPT(goal, self.cur_file, self.cur_line)
# prepare messages
msgs = [
{'content': SYSTEM_MESSAGE, 'role': 'system'},
{'content': prompt, 'role': 'user'},
{'content': prompt, 'role': 'user'}
]
# insert memories
if len(running_memory) > 0:
context = CONTEXT_PROMPT(running_memory, self.memory_window)
if len(self.running_memory) > 0:
context = CONTEXT_PROMPT(
self.running_memory,
self.memory_window
)
msgs.insert(1, {'content': context, 'role': 'user'})
# clrs = [''] * (len(msgs)-2) + ['\033[0;36m', '\033[0;35m']
# print('\n\n'.join([c+m['content']+'\033[0m' for c, m in zip(clrs, msgs)]))
# send it over
action, thought = self._think_act(messages=msgs)
# be robust with malformed responses
start_msg_len = len(msgs)
while not action and len(msgs) < self.max_retries + start_msg_len:
error = NO_ACTION(thought)
@@ -100,16 +97,16 @@ class SWEAgent(Agent):
action, thought = self._think_act(messages=msgs)
if not action:
action = MessageAction(thought)
action = AgentThinkAction(thought)
self._update(action)
self.latest_action = action
return action
def search_memory(self, query: str) -> list[str]:
# return [item for item in self.running_memory if query in item]
raise NotImplementedError('Search_memory not implemented currently')
def search_memory(self, query: str) -> List[str]:
return [item for item in self.running_memory if query in item]
def reset(self) -> None:
"""Used to reset the agent"""
self.running_memory = []
super().reset()
+15 -18
View File
@@ -1,22 +1,22 @@
import re
from opendevin.events.action import (
from opendevin.action import (
Action,
AgentEchoAction,
AgentFinishAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
MessageAction,
)
from .prompts import COMMAND_USAGE, CUSTOM_DOCS
# commands: exit, read, write, browse, kill, search_file, search_dir
no_open_file_error = MessageAction(
'You are not currently in a file. You can use the read command to open a file and then use goto to navigate through it.'
)
no_open_file_error = AgentEchoAction(
'You are not currently in a file. You can use the read command to open a file and then use goto to navigate through it.')
def invalid_error(cmd, docs):
@@ -33,9 +33,7 @@ Try again using this format:
"""
def get_action_from_string(
command_string: str, path: str, line: int, thoughts: str = ''
) -> Action | None:
def get_action_from_string(command_string: str, path: str, line: int, thoughts: str = '') -> Action | None:
"""
Parses the command string to find which command the agent wants to run
Converts the command into a proper Action and returns
@@ -48,7 +46,7 @@ def get_action_from_string(
return AgentFinishAction()
elif 'think' == cmd:
return MessageAction(' '.join(args))
return AgentThinkAction(' '.join(args))
elif 'scroll_up' == cmd:
if not path:
@@ -70,7 +68,7 @@ def get_action_from_string(
end = start + 100
return FileReadAction(path, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'goto'))
return AgentEchoAction(invalid_error(command_string, 'goto'))
elif 'edit' == cmd:
if not path:
@@ -85,7 +83,7 @@ def get_action_from_string(
change = change[1:-1]
return FileWriteAction(path, change, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'edit'))
return AgentEchoAction(invalid_error(command_string, 'edit'))
elif 'read' == cmd:
rex = r'^read\s+(\S+)(?:\s+(\d+))?(?:\s+(-?\d+))?$'
@@ -100,7 +98,7 @@ def get_action_from_string(
return FileReadAction(file, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'read'))
return AgentEchoAction(invalid_error(command_string, 'read'))
elif 'write' == cmd:
rex = r'^write\s+(\S+)\s+(.*?)\s*(\d+)?\s*(-?\d+)?$'
@@ -120,7 +118,7 @@ def get_action_from_string(
return FileWriteAction(file, content, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'write'))
return AgentEchoAction(invalid_error(command_string, 'write'))
elif 'browse' == cmd:
return BrowseURLAction(args[0].strip())
@@ -131,15 +129,13 @@ def get_action_from_string(
if valid:
return CmdRunAction(command_string)
else:
return MessageAction(
f'Invalid command structure for\n ```\n{command_string}\n```.\nTry again using this format:\n{CUSTOM_DOCS}'
)
return AgentEchoAction(f'Invalid command structure for\n ```\n{command_string}\n```.\nTry again using this format:\n{CUSTOM_DOCS}')
else:
# check bash command
obs = str(CmdRunAction(f'type {cmd}'))
if obs.split(':')[-1].strip() == 'not found':
# echo not found error for llm
return MessageAction(content=obs)
return AgentEchoAction(content=obs)
else:
# run valid command
return CmdRunAction(command_string)
@@ -161,7 +157,8 @@ def parse_command(input_str: str, path: str, line: int):
command_str = parts[1].strip()
ind = 2 if len(parts) > 2 else 1
accompanying_text = ''.join(parts[:-ind]).strip()
action = get_action_from_string(command_str, path, line, accompanying_text)
action = get_action_from_string(
command_str, path, line, accompanying_text)
if action:
return action, accompanying_text
return None, input_str # used for retry
+12 -14
View File
@@ -1,3 +1,4 @@
DEFAULT_COMMANDS_DICT = {
'exit': 'Executed when task is complete',
'read <file_name> [<start_line>] [<end_line>]': "Shows a given file's contents starting from <start_line> up to <end_line>. Default: start_line = 0, end_line = -1. By default the whole file will be read.",
@@ -5,12 +6,12 @@ DEFAULT_COMMANDS_DICT = {
'browse <url>': 'Returns the text version of any url, this can be useful to look up documentation or finding issues on github',
'scroll_up': 'Takes no arguments. This will scroll up and show you the 100 lines above your current lines',
'scroll_down': 'Takes no arguments. This will scroll down and show you the 100 lines below your current lines',
'edit <start_line> <end_line> <changes>': 'This will modify lines in the currently open file. use start_line and end_line to designate which lines to change and then write the multiline changes. Set end_line to -1 to denote the end of the file',
'edit <start_line> <end_line> <changes>': 'This will modify lines in the currently open file. use start_line and end_line to designate which lines to change and then write the multiline changes',
'goto <line_num>': 'This will take you directly to a line and show you the 100 lines below it.',
'<bash_command> <args>': 'You can use any bash command you need (cd, ls, rm, grep, dir, mv, wget, git, zip, etc.) with their arguments included',
'pip install <package>': 'You can use this to import python packages. Make sure you include the correct package name when using this command.',
'ls': 'Use the ls command to view all the files in your current directory, this is a good starting point.',
'NOT ALLOWED': 'You cannot use interactive commands like python or node',
'NOT ALLOWED': 'You cannot use interactive commands like python or node'
}
COMMAND_USAGE = {
@@ -24,7 +25,8 @@ COMMAND_USAGE = {
'browse': 'Args:\n<url>\nUsage:\n```\nbrowse https://github.com/OpenDevin/OpenDevin\n```\nThis will fetch the Text elements from the given url and show them to you.',
}
DEFAULT_COMMANDS = '\n'.join([k + ' - ' + v for k, v in DEFAULT_COMMANDS_DICT.items()])
DEFAULT_COMMANDS = '\n'.join(
[k + ' - ' + v for k, v in DEFAULT_COMMANDS_DICT.items()])
# from opendevin.parse_commands import parse_command_file
# USE parse_command_file(filepath) to get the custom commands
@@ -92,7 +94,7 @@ Notes:
- To execute multiple commands you should write them down in your thoughts section so you can remember it on the next step and execute them then.
- The only commands you are not capable of executing are interactive commands like `python` or `node` by themselves.
- If you think that you have completed the task that has been given to you based on your previous actions and outputs then use ``` exit ``` as the command to let the system know that you are done.
- DO NOT make any copies of your previous memories, those will be provided to you at each step, making copies just wastes time and energy. Think smarter not harder.
- DO NOT make any copies of your previous memories those will be provided to you at each step, making copies just wastes time and energy. Think smarter not harder.
- The write and edit commands requires proper indentation in the content section ex. `write hw.py def hello():\n print(\'Hello World\')` this is how you would have to format your write command.
- The white spaces matter as the code changes will be added to the code so they must have proper syntax.
@@ -114,8 +116,8 @@ Do not provide anything extra just your thought and action.
"""
SYSTEM_MESSAGE = f"""SYSTEM INFO:
You are an autonomous coding agent, here to provide solutions for coding issues.
You have been designed to assist with a wide range of programming tasks, from code editing and debugging to testing and deployment.
You am an autonomous coding agent, here to provide solutions for coding issues.
You have been designed to assist you with a wide range of programming tasks, from code editing and debugging to testing and deployment.
You have access to a variety of tools and commands that you can use to help you solve problems efficiently.
{GENERAL_GUIDELINES}
@@ -124,8 +126,7 @@ You have access to a variety of tools and commands that you can use to help you
""".strip()
def NO_ACTION(latest):
return f"""
def NO_ACTION(latest): return f"""
You did not include any action to take in your most recent output:
===== Output ======
@@ -153,8 +154,7 @@ def file_info(file: str, line: int):
"""
def STEP_PROMPT(task, file, line_num):
return f"""
def STEP_PROMPT(task, file, line_num): return f"""
{RESPONSE_FORMAT}
You are currently trying to complete this task:
{task}
@@ -171,9 +171,8 @@ Begin with your thought about the next step and then come up with an action to p
""".strip()
def unpack_dict(data: dict, restrict: list[str] | None = None):
def unpack_dict(data: dict, restrict: list[str] = []):
lines = []
restrict = [] if restrict is None else restrict
for key, value in data.items():
if key in restrict:
continue
@@ -186,8 +185,7 @@ def unpack_dict(data: dict, restrict: list[str] | None = None):
return '\n'.join(lines)
def MEMORY_FORMAT(act, obs):
return f"""
def MEMORY_FORMAT(act, obs): return f"""
Previous Action:
{unpack_dict(act, ["content"])}
+10 -21
View File
@@ -1,6 +1,6 @@
from dotenv import load_dotenv
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .micro.agent import MicroAgent
from .micro.registry import all_microagents
@@ -8,39 +8,28 @@ from .micro.registry import all_microagents
load_dotenv()
from . import ( # noqa: E402
SWE_agent,
browsing_agent,
codeact_agent,
codeact_swe_agent,
delegator_agent,
dummy_agent,
monologue_agent,
planner_agent,
)
__all__ = [
'monologue_agent',
'codeact_agent',
'codeact_swe_agent',
'planner_agent',
'SWE_agent',
'delegator_agent',
'dummy_agent',
'browsing_agent',
]
__all__ = ['monologue_agent', 'codeact_agent',
'planner_agent', 'SWE_agent',
'delegator_agent',
'dummy_agent']
for agent in all_microagents.values():
name = agent['name']
prompt = agent['prompt']
anon_class = type(
name,
(MicroAgent,),
{
'prompt': prompt,
'agent_definition': agent,
},
)
anon_class = type(name, (MicroAgent,), {
'prompt': prompt,
'agent_definition': agent,
})
Agent.register(name, anon_class)
-16
View File
@@ -1,16 +0,0 @@
# Browsing Agent Framework
This folder implements the basic BrowserGym [demo agent](https://github.com/ServiceNow/BrowserGym/tree/main/demo_agent) that enables full-featured web browsing.
## Test run
Note that for browsing tasks, GPT-4 is usually a requirement to get reasonable results, due to the complexity of the web page structures.
```
poetry run python ./opendevin/core/main.py \
-i 10 \
-t "tell me the usa's president using google search" \
-c BrowsingAgent \
-m gpt-4o-2024-05-13
```
-5
View File
@@ -1,5 +0,0 @@
from opendevin.controller.agent import Agent
from .browsing_agent import BrowsingAgent
Agent.register('BrowsingAgent', BrowsingAgent)
-167
View File
@@ -1,167 +0,0 @@
import ast
from browsergym.core.action.highlevel import HighLevelActionSet
from browsergym.utils.obs import flatten_axtree_to_str
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.logger import opendevin_logger as logger
from opendevin.events.action import (
Action,
AgentFinishAction,
BrowseInteractiveAction,
MessageAction,
)
from opendevin.events.observation import BrowserOutputObservation
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins import (
PluginRequirement,
)
def parse_response(response: str) -> Action:
if '```' not in response:
# unexpected response format, message back to user
return MessageAction(response)
thought = response.split('```')[0].strip()
action_str = response.split('```')[1].strip()
# handle send message to user function call in BrowserGym
for sub_action in action_str.split('\n'):
if 'send_msg_to_user(' in sub_action:
tree = ast.parse(sub_action)
args = tree.body[0].value.args # type: ignore
return MessageAction(args[0].value)
return BrowseInteractiveAction(browser_actions=action_str, thought=thought)
class BrowsingAgent(Agent):
VERSION = '1.0'
"""
An agent that interacts with the browser.
"""
sandbox_plugins: list[PluginRequirement] = []
def __init__(
self,
llm: LLM,
) -> None:
"""
Initializes a new instance of the BrowsingAgent class.
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.action_space = HighLevelActionSet(
# see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
subsets=[
'chat',
'bid',
'nav',
], # define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
strict=False, # less strict on the parsing of the actions
multiaction=True, # enable to agent to take multiple actions at once
)
self.reset()
def reset(self) -> None:
"""
Resets the Browsing Agent.
"""
super().reset()
self.cost_accumulator = 0
def step(self, state: State) -> Action:
"""
Performs one step using the Browsing Agent.
This includes gathering information on previous steps and prompting the model to make a browsing command to execute.
Parameters:
- state (State): used to get updated info
Returns:
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
goal = state.get_current_user_intent()
messages = []
prev_actions = ''
cur_axtree_txt = ''
error_prefix = ''
last_obs = None
for prev_action, obs in state.history:
if isinstance(prev_action, BrowseInteractiveAction):
prev_actions += f'{prev_action.browser_actions}\n'
last_obs = obs
elif (
isinstance(prev_action, MessageAction) and prev_action.source != 'user'
):
# agent has responded, task finish.
return AgentFinishAction()
if isinstance(last_obs, BrowserOutputObservation):
if last_obs.error:
# add error recovery prompt prefix
error_prefix = f'IMPORTANT! Last action is incorrect:\n{last_obs.last_browser_action}\nThink again with the current observation of the page.\n'
cur_axtree_txt = flatten_axtree_to_str(last_obs.axtree_object)
system_msg = f"""\
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
# Goal:
{goal}
# Action Space
{self.action_space.describe(with_long_description=False, with_examples=True)}
"""
messages.append({'role': 'system', 'content': system_msg})
prompt = f"""\
{error_prefix}
# Current Accessibility Tree:
{cur_axtree_txt}
# Previous Actions
{prev_actions}
Here is an example with chain of thought of a valid action when clicking on a button:
"
In order to accomplish my goal I need to click on the button with bid 12
```click("12")```
"
""".strip()
messages.append({'role': 'user', 'content': prompt})
response = self.llm.completion(
messages=messages,
temperature=0.0,
)
self.log_cost(response)
action_resp = response['choices'][0]['message']['content']
logger.info(prompt)
logger.info(action_resp)
return parse_response(action_resp)
def search_memory(self, query: str) -> list[str]:
raise NotImplementedError('Implement this abstract method')
def log_cost(self, response):
# TODO: refactor to unified cost tracking
try:
cur_cost = self.llm.completion_cost(response)
except Exception:
cur_cost = 0
self.cost_accumulator += cur_cost
logger.info(
'Cost: %.2f USD | Accumulated Cost: %.2f USD',
cur_cost,
self.cost_accumulator,
)
-785
View File
@@ -1,785 +0,0 @@
import abc
import difflib
import logging
import platform
from copy import deepcopy
from dataclasses import asdict, dataclass
from textwrap import dedent
from typing import Literal, Union
from warnings import warn
from browsergym.core.action.base import AbstractActionSet
from browsergym.core.action.highlevel import HighLevelActionSet
from browsergym.core.action.python import PythonActionSet
from opendevin.runtime.browser.browser_env import BrowserEnv
from .utils import (
ParseError,
parse_html_tags_raise,
)
@dataclass
class Flags:
use_html: bool = True
use_ax_tree: bool = False
drop_ax_tree_first: bool = True # This flag is no longer active TODO delete
use_thinking: bool = False
use_error_logs: bool = False
use_past_error_logs: bool = False
use_history: bool = False
use_action_history: bool = False
use_memory: bool = False
use_diff: bool = False
html_type: str = 'pruned_html'
use_concrete_example: bool = True
use_abstract_example: bool = False
multi_actions: bool = False
action_space: Literal[
'python', 'bid', 'coord', 'bid+coord', 'bid+nav', 'coord+nav', 'bid+coord+nav'
] = 'bid'
is_strict: bool = False
# This flag will be automatically disabled `if not chat_model_args.has_vision()`
use_screenshot: bool = True
enable_chat: bool = False
max_prompt_tokens: int = 100_000
extract_visible_tag: bool = False
extract_coords: Literal['False', 'center', 'box'] = 'False'
extract_visible_elements_only: bool = False
demo_mode: Literal['off', 'default', 'only_visible_elements'] = 'off'
def copy(self):
return deepcopy(self)
def asdict(self):
"""Helper for JSON serializble requirement."""
return asdict(self)
@classmethod
def from_dict(self, flags_dict):
"""Helper for JSON serializble requirement."""
if isinstance(flags_dict, Flags):
return flags_dict
if not isinstance(flags_dict, dict):
raise ValueError(
f'Unregcognized type for flags_dict of type {type(flags_dict)}.'
)
return Flags(**flags_dict)
class PromptElement:
"""Base class for all prompt elements. Prompt elements can be hidden.
Prompt elements are used to build the prompt. Use flags to control which
prompt elements are visible. We use class attributes as a convenient way
to implement static prompts, but feel free to override them with instance
attributes or @property decorator."""
_prompt = ''
_abstract_ex = ''
_concrete_ex = ''
def __init__(self, visible: bool = True) -> None:
"""Prompt element that can be hidden.
Parameters
----------
visible : bool, optional
Whether the prompt element should be visible, by default True. Can
be a callable that returns a bool. This is useful when a specific
flag changes during a shrink iteration.
"""
self._visible = visible
@property
def prompt(self):
"""Avoid overriding this method. Override _prompt instead."""
return self._hide(self._prompt)
@property
def abstract_ex(self):
"""Useful when this prompt element is requesting an answer from the llm.
Provide an abstract example of the answer here. See Memory for an
example.
Avoid overriding this method. Override _abstract_ex instead
"""
return self._hide(self._abstract_ex)
@property
def concrete_ex(self):
"""Useful when this prompt element is requesting an answer from the llm.
Provide a concrete example of the answer here. See Memory for an
example.
Avoid overriding this method. Override _concrete_ex instead
"""
return self._hide(self._concrete_ex)
@property
def is_visible(self):
"""Handle the case where visible is a callable."""
visible = self._visible
if callable(visible):
visible = visible()
return visible
def _hide(self, value):
"""Return value if visible is True, else return empty string."""
if self.is_visible:
return value
else:
return ''
def _parse_answer(self, text_answer) -> dict:
if self.is_visible:
return self._parse_answer(text_answer)
else:
return {}
class Shrinkable(PromptElement, abc.ABC):
@abc.abstractmethod
def shrink(self) -> None:
"""Implement shrinking of this prompt element.
You need to recursively call all shrinkable elements that are part of
this prompt. You can also implement a shriking startegy for this prompt.
Shrinking is can be called multiple times to progressively shrink the
prompt until it fits max_tokens. Default max shrink iterations is 20.
"""
pass
class Truncater(Shrinkable):
"""A prompt element that can be truncated to fit the context length of the LLM.
Of course, it will be great that we never have to use the functionality here to `shrink()` the prompt.
Extend this class for prompt elements that can be truncated. Usually long observations such as AxTree or HTML.
"""
def __init__(self, visible, shrink_speed=0.3, start_truncate_iteration=10):
super().__init__(visible=visible)
self.shrink_speed = shrink_speed # the percentage shrinked in each iteration
self.start_truncate_iteration = (
start_truncate_iteration # the iteration to start truncating
)
self.shrink_calls = 0
self.deleted_lines = 0
def shrink(self) -> None:
if self.is_visible and self.shrink_calls >= self.start_truncate_iteration:
# remove the fraction of _prompt
lines = self._prompt.splitlines()
new_line_count = int(len(lines) * (1 - self.shrink_speed))
self.deleted_lines += len(lines) - new_line_count
self._prompt = '\n'.join(lines[:new_line_count])
self._prompt += (
f'\n... Deleted {self.deleted_lines} lines to reduce prompt size.'
)
self.shrink_calls += 1
def fit_tokens(
shrinkable: Shrinkable,
max_prompt_chars=None,
max_iterations=20,
):
"""Shrink a prompt element until it fits max_tokens.
Parameters
----------
shrinkable : Shrinkable
The prompt element to shrink.
max_prompt_chars : int
The maximum number of chars allowed.
max_iterations : int, optional
The maximum number of shrink iterations, by default 20.
model_name : str, optional
The name of the model used when tokenizing.
Returns
-------
str : the prompt after shrinking.
"""
if max_prompt_chars is None:
return shrinkable.prompt
for _ in range(max_iterations):
prompt = shrinkable.prompt
if isinstance(prompt, str):
prompt_str = prompt
elif isinstance(prompt, list):
prompt_str = '\n'.join([p['text'] for p in prompt if p['type'] == 'text'])
else:
raise ValueError(f'Unrecognized type for prompt: {type(prompt)}')
n_chars = len(prompt_str)
if n_chars <= max_prompt_chars:
return prompt
shrinkable.shrink()
logging.info(
dedent(
f"""\
After {max_iterations} shrink iterations, the prompt is still
{len(prompt_str)} chars (greater than {max_prompt_chars}). Returning the prompt as is."""
)
)
return prompt
class HTML(Truncater):
def __init__(self, html, visible: bool = True, prefix='') -> None:
super().__init__(visible=visible, start_truncate_iteration=5)
self._prompt = f'\n{prefix}HTML:\n{html}\n'
class AXTree(Truncater):
def __init__(
self, ax_tree, visible: bool = True, coord_type=None, prefix=''
) -> None:
super().__init__(visible=visible, start_truncate_iteration=10)
if coord_type == 'center':
coord_note = """\
Note: center coordinates are provided in parenthesis and are
relative to the top left corner of the page.\n\n"""
elif coord_type == 'box':
coord_note = """\
Note: bounding box of each object are provided in parenthesis and are
relative to the top left corner of the page.\n\n"""
else:
coord_note = ''
self._prompt = f'\n{prefix}AXTree:\n{coord_note}{ax_tree}\n'
class Error(PromptElement):
def __init__(self, error, visible: bool = True, prefix='') -> None:
super().__init__(visible=visible)
self._prompt = f'\n{prefix}Error from previous action:\n{error}\n'
class Observation(Shrinkable):
"""Observation of the current step.
Contains the html, the accessibility tree and the error logs.
"""
def __init__(self, obs, flags: Flags) -> None:
super().__init__()
self.flags = flags
self.obs = obs
self.html = HTML(obs[flags.html_type], visible=flags.use_html, prefix='## ')
self.ax_tree = AXTree(
obs['axtree_txt'],
visible=flags.use_ax_tree,
coord_type=flags.extract_coords,
prefix='## ',
)
self.error = Error(
obs['last_action_error'],
visible=flags.use_error_logs and obs['last_action_error'],
prefix='## ',
)
def shrink(self):
self.ax_tree.shrink()
self.html.shrink()
@property
def _prompt(self) -> str: # type: ignore
return f'\n# Observation of current step:\n{self.html.prompt}{self.ax_tree.prompt}{self.error.prompt}\n\n'
def add_screenshot(self, prompt):
if self.flags.use_screenshot:
if isinstance(prompt, str):
prompt = [{'type': 'text', 'text': prompt}]
img_url = BrowserEnv.image_to_jpg_base64_url(
self.obs['screenshot'], add_data_prefix=True
)
prompt.append({'type': 'image_url', 'image_url': img_url})
return prompt
class MacNote(PromptElement):
def __init__(self) -> None:
super().__init__(visible=platform.system() == 'Darwin')
self._prompt = '\nNote: you are on mac so you should use Meta instead of Control for Control+C etc.\n'
class BeCautious(PromptElement):
def __init__(self, visible: bool = True) -> None:
super().__init__(visible=visible)
self._prompt = """\
\nBe very cautious. Avoid submitting anything before verifying the effect of your
actions. Take the time to explore the effect of safe actions first. For example
you can fill a few elements of a form, but don't click submit before verifying
that everything was filled correctly.\n"""
class GoalInstructions(PromptElement):
def __init__(self, goal, visible: bool = True) -> None:
super().__init__(visible)
self._prompt = f"""\
# Instructions
Review the current state of the page and all other information to find the best
possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
## Goal:
{goal}
"""
class ChatInstructions(PromptElement):
def __init__(self, chat_messages, visible: bool = True) -> None:
super().__init__(visible)
self._prompt = """\
# Instructions
You are a UI Assistant, your goal is to help the user perform tasks using a web browser. You can
communicate with the user via a chat, in which the user gives you instructions and in which you
can send back messages. You have access to a web browser that both you and the user can see,
and with which only you can interact via specific commands.
Review the instructions from the user, the current state of the page and all other information
to find the best possible next action to accomplish your goal. Your answer will be interpreted
and executed by a program, make sure to follow the formatting instructions.
## Chat messages:
"""
self._prompt += '\n'.join(
[
f"""\
- [{msg['role']}] {msg['message']}"""
for msg in chat_messages
]
)
class SystemPrompt(PromptElement):
_prompt = """\
You are an agent trying to solve a web task based on the content of the page and
a user instructions. You can interact with the page and explore. Each time you
submit an action it will be sent to the browser and you will receive a new page."""
class MainPrompt(Shrinkable):
def __init__(
self,
obs_history,
actions,
memories,
thoughts,
flags: Flags,
) -> None:
super().__init__()
self.flags = flags
self.history = History(obs_history, actions, memories, thoughts, flags)
if self.flags.enable_chat:
self.instructions: Union[ChatInstructions, GoalInstructions] = (
ChatInstructions(obs_history[-1]['chat_messages'])
)
else:
if (
'chat_messages' in obs_history[-1]
and sum(
[msg['role'] == 'user' for msg in obs_history[-1]['chat_messages']]
)
> 1
):
logging.warning(
'Agent is in goal mode, but multiple user messages are present in the chat. Consider switching to `enable_chat=True`.'
)
self.instructions = GoalInstructions(obs_history[-1]['goal'])
self.obs = Observation(obs_history[-1], self.flags)
self.action_space = ActionSpace(self.flags)
self.think = Think(visible=flags.use_thinking)
self.memory = Memory(visible=flags.use_memory)
@property
def _prompt(self) -> str: # type: ignore
prompt = f"""\
{self.instructions.prompt}\
{self.obs.prompt}\
{self.history.prompt}\
{self.action_space.prompt}\
{self.think.prompt}\
{self.memory.prompt}\
"""
if self.flags.use_abstract_example:
prompt += f"""
# Abstract Example
Here is an abstract version of the answer with description of the content of
each tag. Make sure you follow this structure, but replace the content with your
answer:
{self.think.abstract_ex}\
{self.memory.abstract_ex}\
{self.action_space.abstract_ex}\
"""
if self.flags.use_concrete_example:
prompt += f"""
# Concrete Example
Here is a concrete example of how to format your answer.
Make sure to follow the template with proper tags:
{self.think.concrete_ex}\
{self.memory.concrete_ex}\
{self.action_space.concrete_ex}\
"""
return self.obs.add_screenshot(prompt)
def shrink(self):
self.history.shrink()
self.obs.shrink()
def _parse_answer(self, text_answer):
ans_dict = {}
ans_dict.update(self.think._parse_answer(text_answer))
ans_dict.update(self.memory._parse_answer(text_answer))
ans_dict.update(self.action_space._parse_answer(text_answer))
return ans_dict
class ActionSpace(PromptElement):
def __init__(self, flags: Flags) -> None:
super().__init__()
self.flags = flags
self.action_space = _get_action_space(flags)
self._prompt = (
f'# Action space:\n{self.action_space.describe()}{MacNote().prompt}\n'
)
self._abstract_ex = f"""
<action>
{self.action_space.example_action(abstract=True)}
</action>
"""
self._concrete_ex = f"""
<action>
{self.action_space.example_action(abstract=False)}
</action>
"""
def _parse_answer(self, text_answer):
ans_dict = parse_html_tags_raise(
text_answer, keys=['action'], merge_multiple=True
)
try:
# just check if action can be mapped to python code but keep action as is
# the environment will be responsible for mapping it to python
self.action_space.to_python_code(ans_dict['action'])
except Exception as e:
raise ParseError(
f'Error while parsing action\n: {e}\n'
'Make sure your answer is restricted to the allowed actions.'
)
return ans_dict
def _get_action_space(flags: Flags) -> AbstractActionSet:
match flags.action_space:
case 'python':
action_space = PythonActionSet(strict=flags.is_strict)
if flags.multi_actions:
warn(
f'Flag action_space={repr(flags.action_space)} incompatible with multi_actions={repr(flags.multi_actions)}.'
)
if flags.demo_mode != 'off':
warn(
f'Flag action_space={repr(flags.action_space)} incompatible with demo_mode={repr(flags.demo_mode)}.'
)
return action_space
case 'bid':
action_subsets = ['chat', 'bid']
case 'coord':
action_subsets = ['chat', 'coord']
case 'bid+coord':
action_subsets = ['chat', 'bid', 'coord']
case 'bid+nav':
action_subsets = ['chat', 'bid', 'nav']
case 'coord+nav':
action_subsets = ['chat', 'coord', 'nav']
case 'bid+coord+nav':
action_subsets = ['chat', 'bid', 'coord', 'nav']
case _:
raise NotImplementedError(
f'Unknown action_space {repr(flags.action_space)}'
)
action_space = HighLevelActionSet(
subsets=action_subsets,
multiaction=flags.multi_actions,
strict=flags.is_strict,
demo_mode=flags.demo_mode,
)
return action_space
class Memory(PromptElement):
_prompt = '' # provided in the abstract and concrete examples
_abstract_ex = """
<memory>
Write down anything you need to remember for next steps. You will be presented
with the list of previous memories and past actions.
</memory>
"""
_concrete_ex = """
<memory>
I clicked on bid 32 to activate tab 2. The accessibility tree should mention
focusable for elements of the form at next step.
</memory>
"""
def _parse_answer(self, text_answer):
return parse_html_tags_raise(
text_answer, optional_keys=['memory'], merge_multiple=True
)
class Think(PromptElement):
_prompt = ''
_abstract_ex = """
<think>
Think step by step. If you need to make calculations such as coordinates, write them here. Describe the effect
that your previous action had on the current content of the page.
</think>
"""
_concrete_ex = """
<think>
My memory says that I filled the first name and last name, but I can't see any
content in the form. I need to explore different ways to fill the form. Perhaps
the form is not visible yet or some fields are disabled. I need to replan.
</think>
"""
def _parse_answer(self, text_answer):
return parse_html_tags_raise(
text_answer, optional_keys=['think'], merge_multiple=True
)
def diff(previous, new):
"""Return a string showing the difference between original and new.
If the difference is above diff_threshold, return the diff string."""
if previous == new:
return 'Identical', []
if len(previous) == 0 or previous is None:
return 'previous is empty', []
diff_gen = difflib.ndiff(previous.splitlines(), new.splitlines())
diff_lines = []
plus_count = 0
minus_count = 0
for line in diff_gen:
if line.strip().startswith('+'):
diff_lines.append(line)
plus_count += 1
elif line.strip().startswith('-'):
diff_lines.append(line)
minus_count += 1
else:
continue
header = f'{plus_count} lines added and {minus_count} lines removed:'
return header, diff_lines
class Diff(Shrinkable):
def __init__(
self, previous, new, prefix='', max_line_diff=20, shrink_speed=2, visible=True
) -> None:
super().__init__(visible=visible)
self.max_line_diff = max_line_diff
self.header, self.diff_lines = diff(previous, new)
self.shrink_speed = shrink_speed
self.prefix = prefix
def shrink(self):
self.max_line_diff -= self.shrink_speed
self.max_line_diff = max(1, self.max_line_diff)
@property
def _prompt(self) -> str: # type: ignore
diff_str = '\n'.join(self.diff_lines[: self.max_line_diff])
if len(self.diff_lines) > self.max_line_diff:
original_count = len(self.diff_lines)
diff_str = f'{diff_str}\nDiff truncated, {original_count - self.max_line_diff} changes now shown.'
return f'{self.prefix}{self.header}\n{diff_str}\n'
class HistoryStep(Shrinkable):
def __init__(
self, previous_obs, current_obs, action, memory, flags: Flags, shrink_speed=1
) -> None:
super().__init__()
self.html_diff = Diff(
previous_obs[flags.html_type],
current_obs[flags.html_type],
prefix='\n### HTML diff:\n',
shrink_speed=shrink_speed,
visible=lambda: flags.use_html and flags.use_diff,
)
self.ax_tree_diff = Diff(
previous_obs['axtree_txt'],
current_obs['axtree_txt'],
prefix='\n### Accessibility tree diff:\n',
shrink_speed=shrink_speed,
visible=lambda: flags.use_ax_tree and flags.use_diff,
)
self.error = Error(
current_obs['last_action_error'],
visible=(
flags.use_error_logs
and current_obs['last_action_error']
and flags.use_past_error_logs
),
prefix='### ',
)
self.shrink_speed = shrink_speed
self.action = action
self.memory = memory
self.flags = flags
def shrink(self):
super().shrink()
self.html_diff.shrink()
self.ax_tree_diff.shrink()
@property
def _prompt(self) -> str: # type: ignore
prompt = ''
if self.flags.use_action_history:
prompt += f'\n### Action:\n{self.action}\n'
prompt += (
f'{self.error.prompt}{self.html_diff.prompt}{self.ax_tree_diff.prompt}'
)
if self.flags.use_memory and self.memory is not None:
prompt += f'\n### Memory:\n{self.memory}\n'
return prompt
class History(Shrinkable):
def __init__(
self, history_obs, actions, memories, thoughts, flags: Flags, shrink_speed=1
) -> None:
super().__init__(visible=flags.use_history)
assert len(history_obs) == len(actions) + 1
assert len(history_obs) == len(memories) + 1
self.shrink_speed = shrink_speed
self.history_steps: list[HistoryStep] = []
for i in range(1, len(history_obs)):
self.history_steps.append(
HistoryStep(
history_obs[i - 1],
history_obs[i],
actions[i - 1],
memories[i - 1],
flags,
)
)
def shrink(self):
"""Shrink individual steps"""
# TODO set the shrink speed of older steps to be higher
super().shrink()
for step in self.history_steps:
step.shrink()
@property
def _prompt(self):
prompts = ['# History of interaction with the task:\n']
for i, step in enumerate(self.history_steps):
prompts.append(f'## step {i}')
prompts.append(step.prompt)
return '\n'.join(prompts) + '\n'
if __name__ == '__main__':
html_template = """
<html>
<body>
<div>
Hello World.
Step {}.
</div>
</body>
</html>
"""
OBS_HISTORY = [
{
'goal': 'do this and that',
'pruned_html': html_template.format(1),
'axtree_txt': '[1] Click me',
'last_action_error': '',
},
{
'goal': 'do this and that',
'pruned_html': html_template.format(2),
'axtree_txt': '[1] Click me',
'last_action_error': '',
},
{
'goal': 'do this and that',
'pruned_html': html_template.format(3),
'axtree_txt': '[1] Click me',
'last_action_error': 'Hey, there is an error now',
},
]
ACTIONS = ["click('41')", "click('42')"]
MEMORIES = ['memory A', 'memory B']
THOUGHTS = ['thought A', 'thought B']
flags = Flags(
use_html=True,
use_ax_tree=True,
use_thinking=True,
use_error_logs=True,
use_past_error_logs=True,
use_history=True,
use_action_history=True,
use_memory=True,
use_diff=True,
html_type='pruned_html',
use_concrete_example=True,
use_abstract_example=True,
use_screenshot=False,
multi_actions=True,
)
print(
MainPrompt(
obs_history=OBS_HISTORY,
actions=ACTIONS,
memories=MEMORIES,
thoughts=THOUGHTS,
flags=flags,
).prompt
)
-160
View File
@@ -1,160 +0,0 @@
import collections
import re
from warnings import warn
import yaml
def yaml_parser(message):
"""Parse a yaml message for the retry function."""
# saves gpt-3.5 from some yaml parsing errors
message = re.sub(r':\s*\n(?=\S|\n)', ': ', message)
try:
value = yaml.safe_load(message)
valid = True
retry_message = ''
except yaml.YAMLError as e:
warn(str(e))
value = {}
valid = False
retry_message = "Your response is not a valid yaml. Please try again and be careful to the format. Don't add any apology or comment, just the answer."
return value, valid, retry_message
def _compress_chunks(text, identifier, skip_list, split_regex='\n\n+'):
"""Compress a string by replacing redundant chunks by identifiers. Chunks are defined by the split_regex."""
text_list = re.split(split_regex, text)
text_list = [chunk.strip() for chunk in text_list]
counter = collections.Counter(text_list)
def_dict = {}
id = 0
# Store items that occur more than once in a dictionary
for item, count in counter.items():
if count > 1 and item not in skip_list and len(item) > 10:
def_dict[f'{identifier}-{id}'] = item
id += 1
# Replace redundant items with their identifiers in the text
compressed_text = '\n'.join(text_list)
for key, value in def_dict.items():
compressed_text = compressed_text.replace(value, key)
return def_dict, compressed_text
def compress_string(text):
"""Compress a string by replacing redundant paragraphs and lines with identifiers."""
# Perform paragraph-level compression
def_dict, compressed_text = _compress_chunks(
text, identifier='§', skip_list=[], split_regex='\n\n+'
)
# Perform line-level compression, skipping any paragraph identifiers
line_dict, compressed_text = _compress_chunks(
compressed_text, '', list(def_dict.keys()), split_regex='\n+'
)
def_dict.update(line_dict)
# Create a definitions section
def_lines = ['<definitions>']
for key, value in def_dict.items():
def_lines.append(f'{key}:\n{value}')
def_lines.append('</definitions>')
definitions = '\n'.join(def_lines)
return definitions + '\n' + compressed_text
def extract_html_tags(text, keys):
"""Extract the content within HTML tags for a list of keys.
Parameters
----------
text : str
The input string containing the HTML tags.
keys : list of str
The HTML tags to extract the content from.
Returns
-------
dict
A dictionary mapping each key to a list of subset in `text` that match the key.
Notes
-----
All text and keys will be converted to lowercase before matching.
"""
content_dict = {}
# text = text.lower()
# keys = set([k.lower() for k in keys])
for key in keys:
pattern = f'<{key}>(.*?)</{key}>'
matches = re.findall(pattern, text, re.DOTALL)
if matches:
content_dict[key] = [match.strip() for match in matches]
return content_dict
class ParseError(Exception):
pass
def parse_html_tags_raise(text, keys=(), optional_keys=(), merge_multiple=False):
"""A version of parse_html_tags that raises an exception if the parsing is not successful."""
content_dict, valid, retry_message = parse_html_tags(
text, keys, optional_keys, merge_multiple=merge_multiple
)
if not valid:
raise ParseError(retry_message)
return content_dict
def parse_html_tags(text, keys=(), optional_keys=(), merge_multiple=False):
"""Satisfy the parse api, extracts 1 match per key and validates that all keys are present
Parameters
----------
text : str
The input string containing the HTML tags.
keys : list of str
The HTML tags to extract the content from.
optional_keys : list of str
The HTML tags to extract the content from, but are optional.
Returns
-------
dict
A dictionary mapping each key to subset of `text` that match the key.
bool
Whether the parsing was successful.
str
A message to be displayed to the agent if the parsing was not successful.
"""
all_keys = tuple(keys) + tuple(optional_keys)
content_dict = extract_html_tags(text, all_keys)
retry_messages = []
for key in all_keys:
if key not in content_dict:
if key not in optional_keys:
retry_messages.append(f'Missing the key <{key}> in the answer.')
else:
val = content_dict[key]
content_dict[key] = val[0]
if len(val) > 1:
if not merge_multiple:
retry_messages.append(
f'Found multiple instances of the key {key}. You should have only one of them.'
)
else:
# merge the multiple instances
content_dict[key] = '\n'.join(val)
valid = len(retry_messages) == 0
retry_message = '\n'.join(retry_messages)
return content_dict, valid, retry_message
+14 -20
View File
@@ -1,29 +1,23 @@
# CodeAct Agent Framework
# CodeAct-based Agent Framework
This folder implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
This folder implements the [CodeAct idea](https://arxiv.org/abs/2402.13463) that relies on LLM to autonomously perform actions in a Bash shell. It requires more from the LLM itself: LLM needs to be capable enough to do all the stuff autonomously, instead of stuck in an infinite loop.
The conceptual idea is illustrated below. At each turn, the agent can:
**NOTE: This agent is still highly experimental and under active development to reach the capability described in the original paper & [repo](https://github.com/xingyaoww/code-act).**
1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
2. **CodeAct**: Choose to perform the task by executing code
- Execute any valid Linux `bash` command
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
<video src="https://github.com/xingyaoww/code-act/assets/38853559/62c80ada-62ce-447e-811c-fc801dd4beac"> </video>
*Demo of the expected capability - work-in-progress.*
![image](https://github.com/OpenDevin/OpenDevin/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3)
```bash
mkdir workspace
PYTHONPATH=`pwd`:$PYTHONPATH python3 opendevin/main.py -d ./workspace -c CodeActAgent -t "Please write a flask app that returns 'Hello, World\!' at the root URL, then start the app on port 5000. python3 has already been installed for you."
```
## Plugin System
Example: prompts `gpt-4-0125-preview` to write a flask server, install `flask` library, and start the server.
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
- [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
- [SWE-agent tool plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
<img width="951" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/325c3115-a343-4cc5-a92b-f1e5d552a077">
## Demo
<img width="957" alt="image" src="https://github.com/OpenDevin/OpenDevin/assets/38853559/68ad10c1-744a-4e9d-bb29-0f163d665a0a">
https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
Most of the things are working as expected, except at the end, the model did not follow the instruction to stop the interaction by outputting `<execute> exit </execute>` as instructed.
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
## Work-in-progress & Next step
[] Support web-browsing
[] Complete the workflow for CodeAct agent to submit Github PRs
**TODO**: This should be fixable by either (1) including a complete in-context example like [this](https://github.com/xingyaoww/mint-bench/blob/main/mint/tasks/in_context_examples/reasoning/with_tool.txt), OR (2) collect some interaction data like this and fine-tune a model (like [this](https://github.com/xingyaoww/code-act), a more complex route).
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .codeact_agent import CodeActAgent
+92 -203
View File
@@ -1,171 +1,60 @@
import re
from typing import List, Mapping
from agenthub.codeact_agent.prompt import (
COMMAND_DOCS,
EXAMPLES,
GITHUB_MESSAGE,
SYSTEM_PREFIX,
SYSTEM_SUFFIX,
)
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import (
from agenthub.codeact_agent.prompt import EXAMPLES, SYSTEM_MESSAGE
from opendevin.action import (
Action,
AgentEchoAction,
AgentFinishAction,
BrowseInteractiveAction,
AgentTalkAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
NullAction,
)
from opendevin.events.observation import (
BrowserOutputObservation,
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentMessageObservation,
CmdOutputObservation,
IPythonRunCellObservation,
UserMessageObservation,
)
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins import (
AgentSkillsRequirement,
from opendevin.sandbox.plugins import (
JupyterRequirement,
PluginRequirement,
SWEAgentCommandsRequirement,
)
ENABLE_GITHUB = True
from opendevin.state import State
def parse_response(response) -> str:
action = response.choices[0].message.content
for lang in ['bash', 'ipython', 'browse']:
for lang in ['bash', 'ipython']:
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
action += f'</execute_{lang}>'
return action
def action_to_str(action: Action) -> str:
if isinstance(action, CmdRunAction):
return f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
elif isinstance(action, IPythonRunCellAction):
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
elif isinstance(action, BrowseInteractiveAction):
return f'{action.thought}\n<execute_browse>\n{action.browser_actions}\n</execute_browse>'
elif isinstance(action, MessageAction):
return action.content
return ''
def get_action_message(action: Action) -> dict[str, str] | None:
if (
isinstance(action, BrowseInteractiveAction)
or isinstance(action, CmdRunAction)
or isinstance(action, IPythonRunCellAction)
or isinstance(action, MessageAction)
):
return {
'role': 'user' if action.source == 'user' else 'assistant',
'content': action_to_str(action),
}
return None
def get_observation_message(obs) -> dict[str, str] | None:
if isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
content += (
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
)
return {'role': 'user', 'content': content}
elif isinstance(obs, IPythonRunCellObservation):
content = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
splitted = content.split('\n')
for i, line in enumerate(splitted):
if '![image](data:image/png;base64,' in line:
splitted[i] = (
'![image](data:image/png;base64, ...) already displayed to user'
)
content = '\n'.join(splitted)
content = truncate_observation(content)
return {'role': 'user', 'content': content}
elif isinstance(obs, BrowserOutputObservation):
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
return {'role': 'user', 'content': content}
return None
def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
"""
Truncate the middle of the observation if it is too long.
"""
if len(observation) <= max_chars:
return observation
half = max_chars // 2
return (
observation[:half]
+ '\n[... Observation truncated due to length ...]\n'
+ observation[-half:]
)
# FIXME: We can tweak these two settings to create MicroAgents specialized toward different area
def get_system_message() -> str:
if ENABLE_GITHUB:
return f'{SYSTEM_PREFIX}\n{GITHUB_MESSAGE}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
else:
return f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
def get_in_context_example() -> str:
return EXAMPLES
class CodeActAgent(Agent):
VERSION = '1.5'
"""
The Code Act Agent is a minimalist agent.
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
### Overview
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.13463), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
The conceptual idea is illustrated below. At each turn, the agent can:
1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
2. **CodeAct**: Choose to perform the task by executing code
- Execute any valid Linux `bash` command
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
![image](https://github.com/OpenDevin/OpenDevin/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3)
### Plugin System
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
- [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
- [SWE-agent tool plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
### Demo
https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
### Work-in-progress & Next step
[] Support web-browsing
[] Complete the workflow for CodeAct agent to submit Github PRs
"""
sandbox_plugins: list[PluginRequirement] = [
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
# AgentSkillsRequirement provides a lot of Python functions
# and it need to be initialized before Jupyter for Jupyter to use those functions.
AgentSkillsRequirement(),
JupyterRequirement(),
]
jupyter_kernel_init_code: str = 'from agentskills import *'
system_message: str = get_system_message()
in_context_example: str = f"Here is an example of how you can interact with the environment for task solving:\n{get_in_context_example()}\n\nNOW, LET'S START!"
sandbox_plugins: List[PluginRequirement] = [JupyterRequirement(), SWEAgentCommandsRequirement()]
SUPPORTED_ACTIONS = (
CmdRunAction,
IPythonRunCellAction,
AgentEchoAction,
AgentTalkAction,
NullAction
)
SUPPORTED_OBSERVATIONS = (
AgentMessageObservation,
UserMessageObservation,
CmdOutputObservation,
IPythonRunCellObservation
)
def __init__(
self,
@@ -178,104 +67,104 @@ class CodeActAgent(Agent):
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.reset()
def reset(self) -> None:
"""
Resets the CodeAct Agent.
"""
super().reset()
self.messages: List[Mapping[str, str]] = []
def step(self, state: State) -> Action:
"""
Performs one step using the CodeAct Agent.
Performs one step using the Code Act Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
Parameters:
- state (State): used to get updated info and background commands
Returns:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
- CmdRunAction(command) - command action to run
- AgentEchoAction(content=INVALID_INPUT_MESSAGE) - invalid command output
Raises:
- NotImplementedError - for actions other than CmdOutputObservation or AgentMessageObservation
"""
messages: list[dict[str, str]] = [
{'role': 'system', 'content': self.system_message},
{'role': 'user', 'content': self.in_context_example},
]
for prev_action, obs in state.history:
action_message = get_action_message(prev_action)
if action_message:
messages.append(action_message)
if len(self.messages) == 0:
assert state.plan.main_goal, 'Expecting instruction to be set'
self.messages = [
{'role': 'system', 'content': SYSTEM_MESSAGE},
{
'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, self.SUPPORTED_ACTIONS
), f'{prev_action.__class__} is not supported (supported: {self.SUPPORTED_ACTIONS})'
# prev_action is already added to self.messages when returned
obs_message = get_observation_message(obs)
if obs_message:
messages.append(obs_message)
# 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})
latest_user_message = [m for m in messages if m['role'] == 'user'][-1]
if latest_user_message:
if latest_user_message['content'].strip() == '/exit':
return AgentFinishAction()
latest_user_message['content'] += (
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
)
# User wants to exit
if obs.content.strip() == '/exit':
return AgentFinishAction()
elif isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + obs.content
content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
self.messages.append({'role': 'user', 'content': content})
elif isinstance(obs, IPythonRunCellObservation):
content = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
splited = content.split('\n')
for i, line in enumerate(splited):
if '![image](data:image/png;base64,' in line:
splited[i] = '![image](data:image/png;base64, ...) already displayed to user'
content = '\n'.join(splited)
self.messages.append({'role': 'user', 'content': content})
else:
raise NotImplementedError(
f'Unknown observation type: {obs.__class__}'
)
response = self.llm.do_completion(
messages=messages,
response = self.llm.completion(
messages=self.messages,
stop=[
'</execute_ipython>',
'</execute_bash>',
'</execute_browse>',
],
temperature=0.0,
temperature=0.0
)
action_str: str = parse_response(response)
state.num_of_chars += sum(
len(message['content']) for message in messages
len(message['content']) for message in self.messages
) + len(action_str)
self.messages.append({'role': 'assistant', 'content': action_str})
if finish_command := re.search(r'<finish>.*</finish>', action_str, re.DOTALL):
thought = action_str.replace(finish_command.group(0), '').strip()
return AgentFinishAction(thought=thought)
if bash_command := re.search(
r'<execute_bash>(.*?)</execute_bash>', action_str, re.DOTALL
):
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 = bash_command.group(1).strip()
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command=command_group, thought=thought)
elif python_code := re.search(
r'<execute_ipython>(.*?)</execute_ipython>', action_str, re.DOTALL
):
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,
kernel_init_code=self.jupyter_kernel_init_code,
)
elif browse_command := re.search(
r'<execute_browse>(.*)</execute_browse>', action_str, re.DOTALL
):
# BrowserGym actions was found
browse_actions = browse_command.group(1).strip()
thought = action_str.replace(browse_command.group(0), '').strip()
return BrowseInteractiveAction(
browser_actions=browse_actions, thought=thought
)
return IPythonRunCellAction(code=code_group, thought=thought)
else:
# We assume the LLM is GOOD enough that when it returns pure natural language
# it want to talk to the user
return MessageAction(content=action_str, wait_for_response=True)
return AgentTalkAction(content=action_str)
def search_memory(self, query: str) -> list[str]:
def search_memory(self, query: str) -> List[str]:
raise NotImplementedError('Implement this abstract method')
+82 -105
View File
@@ -1,64 +1,60 @@
from opendevin.runtime.plugins import AgentSkillsRequirement
from opendevin.sandbox.plugins import SWEAgentCommandsRequirement
_AGENT_SKILLS_DOCS = AgentSkillsRequirement.documentation
_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 Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:\n'
f'{_AGENT_SKILLS_DOCS}'
"Please note that THE `edit_file` FUNCTION REQUIRES PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it 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."
_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 =======
MINIMAL_SYSTEM_PREFIX = """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
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>.
"""
BROWSING_PREFIX = """The assistant can browse the Internet with commands on behalf of the user by wrapping them with <execute_browse> and </execute_browse>.
For example, you can browse a given URL by <execute_browse> goto("<URL>") </execute_browse>.
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
"""
PIP_INSTALL_PREFIX = """The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them."""
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.
SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX
{_COMMAND_DOCS}
GITHUB_MESSAGE = """To do any activities on GitHub, the assistant should use the token in the $GITHUB_TOKEN environment variable.
For instance, to push a local branch `my_branch` to the github repo `owner/repo`, the assistant can use the following four commands:
<execute_bash> git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch </execute_bash>
If the assistant require access to GitHub but $GITHUB_TOKEN is not set, ask the user to set it."""
SYSTEM_SUFFIX = """The assistant's response should be concise.
The assistant should include ONLY ONE <execute_ipython> or <execute_bash> or <execute_browse> in every one of the responses, unless the assistant is finished with the task or need more input or action from the user in order to proceed.
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> or <execute_browse> instead of providing it.
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.
"""
# ======= EXAMPLE MESSAGE =======
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 create a file first:
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
1|
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE=\"\"\"from flask import Flask
CODE='''
from flask import Flask
app = Flask(__name__)
@app.route('/')
@@ -67,23 +63,15 @@ def index():
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)\"\"\"
edit_file(start=1, end=1, content=EDITED_CODE)
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
OBSERVATION:
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| 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.]
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:
@@ -92,7 +80,7 @@ python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
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:
@@ -101,7 +89,7 @@ cat server.log
</execute_bash>
USER:
OBSERVATION:
Observation:
Traceback (most recent call last):
File "/workspace/app.py", line 2, in <module>
from flask import Flask
@@ -114,7 +102,7 @@ pip install flask
</execute_bash>
USER:
OBSERVATION:
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)
@@ -138,7 +126,7 @@ python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
Observation:
[1] 124
ASSISTANT:
@@ -148,7 +136,7 @@ cat server.log
</execute_bash>
USER:
OBSERVATION:
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.
@@ -158,21 +146,6 @@ 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 browse the newly started server's homepage and show me the content.
ASSISTANT:
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
<execute_browse>
goto("http://127.0.0.1:5000")
</execute_browse>
USER:
OBSERVATION:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". 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:
@@ -182,48 +155,53 @@ kill 124
</execute_bash>
USER:
OBSERVATION:
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_ipython>
open_file('app.py')
</execute_ipython>
<execute_bash>
open app.py
</execute_bash>
USER:
[File: /workspace/app.py (10 lines total)]
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return str(numbers)
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
[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. I should include correct indentation. Let me update the file:
<execute_ipython>
edit_file(start=7, end=7, content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'")
</execute_ipython>
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 (10 lines total after edit)]
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| 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.]
[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:
@@ -245,5 +223,4 @@ 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'
'If you want to browse the Internet, please use <execute_browse> YOUR_COMMAND_HERE </execute_browse>.\n'
)
-7
View File
@@ -1,7 +0,0 @@
# CodeAct (SWE Edit Specialized)
This agent is an adaptation of the original [SWE Agent](https://swe-agent.com/) based on CodeAct using the `agentskills` library of OpenDevin.
Its intended use is **solving Github issues**.
It removes web-browsing and Github capability from the original CodeAct agent to avoid confusion to the agent.
-5
View File
@@ -1,5 +0,0 @@
from opendevin.controller.agent import Agent
from .codeact_swe_agent import CodeActSWEAgent
Agent.register('CodeActSWEAgent', CodeActSWEAgent)
@@ -1,246 +0,0 @@
import re
from agenthub.codeact_swe_agent.prompt import (
COMMAND_DOCS,
MINIMAL_SYSTEM_PREFIX,
SWE_EXAMPLE,
SYSTEM_SUFFIX,
)
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import (
Action,
AgentFinishAction,
BrowseInteractiveAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
from opendevin.events.observation import (
BrowserOutputObservation,
CmdOutputObservation,
IPythonRunCellObservation,
)
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins import (
AgentSkillsRequirement,
JupyterRequirement,
PluginRequirement,
)
def parse_response(response) -> str:
action = response.choices[0].message.content
for lang in ['bash', 'ipython', 'browse']:
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
action += f'</execute_{lang}>'
return action
def action_to_str(action: Action) -> str:
if isinstance(action, CmdRunAction):
return f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
elif isinstance(action, IPythonRunCellAction):
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
elif isinstance(action, BrowseInteractiveAction):
return f'{action.thought}\n<execute_browse>\n{action.browser_actions}\n</execute_browse>'
elif isinstance(action, MessageAction):
return action.content
return ''
def get_action_message(action: Action) -> dict[str, str] | None:
if (
isinstance(action, BrowseInteractiveAction)
or isinstance(action, CmdRunAction)
or isinstance(action, IPythonRunCellAction)
or isinstance(action, MessageAction)
):
return {
'role': 'user' if action.source == 'user' else 'assistant',
'content': action_to_str(action),
}
return None
def get_observation_message(obs) -> dict[str, str] | None:
if isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
content += (
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
)
return {'role': 'user', 'content': content}
elif isinstance(obs, IPythonRunCellObservation):
content = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
splitted = content.split('\n')
for i, line in enumerate(splitted):
if '![image](data:image/png;base64,' in line:
splitted[i] = (
'![image](data:image/png;base64, ...) already displayed to user'
)
content = '\n'.join(splitted)
content = truncate_observation(content)
return {'role': 'user', 'content': content}
elif isinstance(obs, BrowserOutputObservation):
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
return {'role': 'user', 'content': content}
return None
def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
"""
Truncate the middle of the observation if it is too long.
"""
if len(observation) <= max_chars:
return observation
half = max_chars // 2
return (
observation[:half]
+ '\n[... Observation truncated due to length ...]\n'
+ observation[-half:]
)
def get_system_message() -> str:
return f'{MINIMAL_SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
def get_in_context_example() -> str:
return SWE_EXAMPLE
class CodeActSWEAgent(Agent):
VERSION = '1.5'
"""
This agent is an adaptation of the original [SWE Agent](https://swe-agent.com/) based on CodeAct 1.5 using the `agentskills` library of OpenDevin.
It is intended use is **solving Github issues**.
It removes web-browsing and Github capability from the original CodeAct agent to avoid confusion to the agent.
"""
sandbox_plugins: list[PluginRequirement] = [
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
# AgentSkillsRequirement provides a lot of Python functions
# and it need to be initialized before Jupyter for Jupyter to use those functions.
AgentSkillsRequirement(),
JupyterRequirement(),
]
jupyter_kernel_init_code: str = 'from agentskills import *'
system_message: str = get_system_message()
in_context_example: str = f"Here is an example of how you can interact with the environment for task solving:\n{get_in_context_example()}\n\nNOW, LET'S START!"
def __init__(
self,
llm: LLM,
) -> None:
"""
Initializes a new instance of the CodeActAgent class.
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.reset()
def reset(self) -> None:
"""
Resets the CodeAct Agent.
"""
super().reset()
def step(self, state: State) -> Action:
"""
Performs one step using the CodeAct Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
Parameters:
- state (State): used to get updated info and background commands
Returns:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
messages: list[dict[str, str]] = [
{'role': 'system', 'content': self.system_message},
{'role': 'user', 'content': self.in_context_example},
]
for prev_action, obs in state.history:
action_message = get_action_message(prev_action)
if action_message:
messages.append(action_message)
obs_message = get_observation_message(obs)
if obs_message:
messages.append(obs_message)
latest_user_message = [m for m in messages if m['role'] == 'user'][-1]
if latest_user_message:
if latest_user_message['content'].strip() == '/exit':
return AgentFinishAction()
latest_user_message['content'] += (
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
)
response = self.llm.do_completion(
messages=messages,
stop=[
'</execute_ipython>',
'</execute_bash>',
'</execute_browse>',
],
temperature=0.0,
)
action_str: str = parse_response(response)
state.num_of_chars += sum(
len(message['content']) for message in messages
) + len(action_str)
if finish_command := re.search(r'<finish>.*</finish>', action_str, re.DOTALL):
thought = action_str.replace(finish_command.group(0), '').strip()
return AgentFinishAction(thought=thought)
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 = bash_command.group(1).strip()
if command_group.strip() == 'exit':
return AgentFinishAction()
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,
kernel_init_code=self.jupyter_kernel_init_code,
)
elif browse_command := re.search(
r'<execute_browse>(.*)</execute_browse>', action_str, re.DOTALL
):
# BrowserGym actions was found
browse_actions = browse_command.group(1).strip()
thought = action_str.replace(browse_command.group(0), '').strip()
return BrowseInteractiveAction(
browser_actions=browse_actions, thought=thought
)
else:
# We assume the LLM is GOOD enough that when it returns pure natural language
# it want to talk to the user
return MessageAction(content=action_str, wait_for_response=True)
def search_memory(self, query: str) -> list[str]:
raise NotImplementedError('Implement this abstract method')
-451
View File
@@ -1,451 +0,0 @@
from opendevin.runtime.plugins import AgentSkillsRequirement
_AGENT_SKILLS_DOCS = AgentSkillsRequirement.documentation
COMMAND_DOCS = (
'\nApart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:\n'
f'{_AGENT_SKILLS_DOCS}'
"Please note that THE `edit_file` FUNCTION REQUIRES PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it 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 =======
MINIMAL_SYSTEM_PREFIX = """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>.
"""
SYSTEM_SUFFIX = """The assistant's response should be concise.
The assistant should include ONLY ONE <execute_ipython> or <execute_bash> or <execute_browse> in every one of the responses, unless the assistant is finished with the task or need more input or action from the user in order to proceed.
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> or <execute_browse> instead of providing it.
"""
SWE_EXAMPLE = """
--- START OF EXAMPLE ---
USER: We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
TimeDelta serialization precision
Hi there!
I just found quite strange behaviour of `TimeDelta` field serialization
```python3
from marshmallow.fields import TimeDelta
from datetime import timedelta
td_field = TimeDelta(precision="milliseconds")
obj = dict()
obj["td_field"] = timedelta(milliseconds=345)
print(td_field.serialize("td_field", obj))
```
Output of this snippet is `344`, but it seems that `345` is correct.
Looks like a rounding issue here: https://github.com/marshmallow-code/marshmallow/blob/dev/src/marshmallow/fields.py#L1474
--- END ISSUE ---
[Current directory: /workspace/marshmallow-code__marshmallow]
ASSISTANT:
Let's first start by reproducing the results of the issue. The issue includes some example code for reproduction, which we can use. We'll create a new file called `reproduce.py` and paste the example code into it.
<execute_ipython>
create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/marshmallow-code__marshmallow/reproduce.py (1 lines total)]
1|
[File /workspace/marshmallow-code__marshmallow/reproduce.py created.]
ASSISTANT:
Now let's paste in the example code from the issue.
<execute_ipython>
EDITED_CODE = \"\"\"from marshmallow.fields import TimeDelta
from datetime import timedelta
td_field = TimeDelta(precision="milliseconds")
obj = dict()
obj["td_field"] = timedelta(milliseconds=345)
print(td_field.serialize("td_field", obj))
\"\"\"
edit_file(start=1, end=1, content=EDITED_CODE)
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/marshmallow-code__marshmallow/reproduce.py (9 lines total)]
1|from marshmallow.fields import TimeDelta
2|from datetime import timedelta
3|
4|td_field = TimeDelta(precision="milliseconds")
5|
6|obj = dict()
7|obj["td_field"] = timedelta(milliseconds=345)
8|
9|print(td_field.serialize("td_field", obj))
[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:
Now let's run the code to see if we see the same output as the issue.
<execute_bash>
python reproduce.py
</execute_bash>
USER:
OBSERVATION:
344
ASSISTANT:
We are indeed seeing the same output as the issue. The issue suggests that we should look at line 1474 of the `fields.py` file to see if there is a rounding issue. Let's open the `fields.py` file and navigate to line 1474 to see the relevant code. First we should find the `fields.py` file in the repository. It is likely to be in the `src/marshmallow` directory, but we should check to be sure.
<execute_bash>
ls -F
</execute_bash>
USER:
OBSERVATION:
AUTHORS.rst
CHANGELOG.rst
CODE_OF_CONDUCT.md
CONTRIBUTING.rst
LICENSE
MANIFEST.in
NOTICE
README.rst
RELEASING.md
azure-pipelines.yml
docs/
examples/
performance/
pyproject.toml
reproduce.py
setup.cfg
setup.py
src/
tests/
tox.ini
ASSISTANT:
It looks like the `src` directory is present, which suggests that the `fields.py` file is likely to be in the `src` directory. Let's use find_file to see where it is.
<execute_ipython>
find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
</execute_ipython>
USER:
OBSERVATION:
[Found 1 matches for "fields.py" in /workspace/marshmallow-code__marshmallow/src]
/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py
ASSISTANT:
It looks like the `fields.py` file is present in the `./src/marshmallow/` directory. The issue also points to a specific URL with line number 1474. We should navigate to that line in fields.py to see the relevant code for the `TimeDelta` serialization.
<execute_ipython>
open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1458 more lines above)
1459| self.WEEKS,
1460| )
1461|
1462| if precision not in units:
1463| msg = 'The precision must be {} or "{}".'.format(
1464| ", ".join([f'"{each}"' for each in units[:-1]]), units[-1]
1465| )
1466| raise ValueError(msg)
1467|
1468| self.precision = precision
1469| super().__init__(**kwargs)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475| return int(value.total_seconds() / base_unit.total_seconds())
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
1480| except (TypeError, ValueError) as error:
1481| raise self.make_error("invalid") from error
1482|
1483| kwargs = {self.precision: value}
1484|
1485| try:
1486| return dt.timedelta(**kwargs)
1487| except OverflowError as error:
1488| raise self.make_error("invalid") from error
1489|
1490|
1491|class Mapping(Field):
1492| \"\"\"An abstract class for objects with key-value pairs.
1493|
1494| :param keys: A field class or instance for dict keys.
1495| :param values: A field class or instance for dict values.
1496| :param kwargs: The same keyword arguments that :class:`Field` receives.
1497|
1498| .. note::
1499| When the structure of nested data is not known, you may omit the
1500| `keys` and `values` arguments to prevent content validation.
1501|
1502| .. versionadded:: 3.0.0rc4
1503| \"\"\"
1504|
1505| mapping_type = dict
1506|
1507| #: Default error messages.
1508| default_error_messages = {"invalid": "Not a valid mapping type."}
1509|
1510| def __init__(
1511| self,
1512| keys: typing.Optional[typing.Union[Field, type]] = None,
1513| values: typing.Optional[typing.Union[Field, type]] = None,
1514| **kwargs,
1515| ):
1516| super().__init__(**kwargs)
1517| if keys is None:
1518| self.key_field = None
1519| else:
1520| try:
1521| self.key_field = resolve_field_instance(keys)
1522| except FieldInstanceResolutionError as error:
1523| raise ValueError(
1524| '"keys" must be a subclass or instance of '
1525| "marshmallow.base.FieldABC."
1526| ) from error
1527|
1528| if values is None:
1529| self.value_field = None
1530| else:
1531| try:
1532| self.value_field = resolve_field_instance(values)
1533| except FieldInstanceResolutionError as error:
1534| raise ValueError(
1535| '"values" must be a subclass or instance of '
1536| "marshmallow.base.FieldABC."
1537| ) from error
1538| if isinstance(self.value_field, Nested):
1539| self.only = self.value_field.only
1540| self.exclude = self.value_field.exclude
1541|
1542| def _bind_to_schema(self, field_name, schema):
1543| super()._bind_to_schema(field_name, schema)
1544| if self.value_field:
1545| self.value_field = copy.deepcopy(self.value_field)
1546| self.value_field._bind_to_schema(field_name, self)
1547| if isinstance(self.value_field, Nested):
1548| self.value_field.only = self.only
1549| self.value_field.exclude = self.exclude
1550| if self.key_field:
1551| self.key_field = copy.deepcopy(self.key_field)
1552| self.key_field._bind_to_schema(field_name, self)
1553|
1554| def _serialize(self, value, attr, obj, **kwargs):
1555| if value is None:
1556| return None
1557| if not self.value_field and not self.key_field:
1558| return self.mapping_type(value)
(439 more lines below)
ASSISTANT:
We are now looking at the relevant section of the `fields.py` file where the `TimeDelta` serialization occurs. The issue suggests that there is a rounding problem near line 1474. The current implementation uses integer division, which truncates the decimal part and can lead to incorrect rounding on line 1475. To fix this issue, we should ensure that the division results in a float, and then round the result to the nearest integer before converting it to an int. This should preserve the milliseconds precision as expected. Let's make the necessary edit to the code.
<execute_ipython>
edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
</execute_ipython>
USER:
OBSERVATION:
[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]
ERRORS:
- E999 IndentationError: unexpected indent
[This is how your edit would have looked if applied]
-------------------------------------------------
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1468 more lines above)
1469| super().__init__(**kwargs)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475|return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
(518 more lines below)
-------------------------------------------------
[This is the original code before your edit]
-------------------------------------------------
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1469 more lines above)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475| return int(value.total_seconds() / base_unit.total_seconds())
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
1480| except (TypeError, ValueError) as error:
(517 more lines below)
-------------------------------------------------
Your changes have NOT been applied. Please fix your edit command and try again.
You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
DO NOT re-run the same failed edit command. Running it again will lead to the same error.
ASSISTANT:
My edit command did not use the proper indentation, I will fix my syntax in this follow up edit command.
<execute_ipython>
EDITED_CODE=" return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int"
edit_file(start=1475, end=1475, content=EDITED_CODE)
</execute_ipython>
USER:
OBSERVATION:
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1424 more lines above)
1425|
1426| :param precision: Influences how the integer is interpreted during
1427| (de)serialization. Must be 'days', 'seconds', 'microseconds',
1428| 'milliseconds', 'minutes', 'hours' or 'weeks'.
1429| :param kwargs: The same keyword arguments that :class:`Field` receives.
1430|
1431| .. versionchanged:: 2.0.0
1432| Always serializes to an integer value to avoid rounding errors.
1433| Add `precision` parameter.
1434| \"\"\"
1435|
1436| DAYS = "days"
1437| SECONDS = "seconds"
1438| MICROSECONDS = "microseconds"
1439| MILLISECONDS = "milliseconds"
1440| MINUTES = "minutes"
1441| HOURS = "hours"
1442| WEEKS = "weeks"
1443|
1444| #: Default error messages.
1445| default_error_messages = {
1446| "invalid": "Not a valid period of time.",
1447| "format": "{input!r} cannot be formatted as a timedelta.",
1448| }
1449|
1450| def __init__(self, precision: str = SECONDS, **kwargs):
1451| precision = precision.lower()
1452| units = (
1453| self.DAYS,
1454| self.SECONDS,
1455| self.MICROSECONDS,
1456| self.MILLISECONDS,
1457| self.MINUTES,
1458| self.HOURS,
1459| self.WEEKS,
1460| )
1461|
1462| if precision not in units:
1463| msg = 'The precision must be {} or "{}".'.format(
1464| ", ".join([f'"{each}"' for each in units[:-1]]), units[-1]
1465| )
1466| raise ValueError(msg)
1467|
1468| self.precision = precision
1469| super().__init__(**kwargs)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475| return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
1480| except (TypeError, ValueError) as error:
1481| raise self.make_error("invalid") from error
1482|
1483| kwargs = {self.precision: value}
1484|
1485| try:
1486| return dt.timedelta(**kwargs)
1487| except OverflowError as error:
1488| raise self.make_error("invalid") from error
1489|
1490|
1491|class Mapping(Field):
1492| \"\"\"An abstract class for objects with key-value pairs.
1493|
1494| :param keys: A field class or instance for dict keys.
1495| :param values: A field class or instance for dict values.
1496| :param kwargs: The same keyword arguments that :class:`Field` receives.
1497|
1498| .. note::
1499| When the structure of nested data is not known, you may omit the
1500| `keys` and `values` arguments to prevent content validation.
1501|
1502| .. versionadded:: 3.0.0rc4
1503| \"\"\"
1504|
1505| mapping_type = dict
1506|
1507| #: Default error messages.
1508| default_error_messages = {"invalid": "Not a valid mapping type."}
1509|
1510| def __init__(
1511| self,
1512| keys: typing.Optional[typing.Union[Field, type]] = None,
1513| values: typing.Optional[typing.Union[Field, type]] = None,
1514| **kwargs,
1515| ):
1516| super().__init__(**kwargs)
1517| if keys is None:
1518| self.key_field = None
1519| else:
1520| try:
1521| self.key_field = resolve_field_instance(keys)
1522| except FieldInstanceResolutionError as error:
1523| raise ValueError(
1524| '"keys" must be a subclass or instance of '
(473 more lines below)
[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 code has been updated to use the `round` function, which should fix the rounding issue. Before submitting the changes, it would be prudent to run the reproduce.py code again to ensure that this change has the desired effect. I see that "Current directory" is still `/marshmallow-code__marshmallow`, so we can just run `reproduce.py` directly.
<execute_bash>
python reproduce.py
</execute_bash>
USER:
OBSERVATION:
345
ASSISTANT:
The output has changed from 344 to 345, which suggests that the rounding issue has been fixed. Let's remove the reproduce.py file since it is no longer needed.
<execute_bash>
rm reproduce.py
</execute_bash>
USER:
OBSERVATION:
ASSISTANT:
rm doesn't have any output when it deletes successfully, so that must have worked. Now that we have fixed the issue, we can exit the current task.
<execute_bash>
exit
</execute_bash>
--- END OF EXAMPLE ---
"""
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .agent import DelegatorAgent
+24 -38
View File
@@ -1,17 +1,17 @@
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import Action, AgentDelegateAction, AgentFinishAction
from opendevin.events.observation import AgentDelegateObservation
from typing import List
from opendevin.action import Action, AgentDelegateAction, AgentFinishAction
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.observation import AgentDelegateObservation
from opendevin.state import State
class DelegatorAgent(Agent):
VERSION = '1.0'
"""
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
"""
current_delegate: str = ''
def __init__(self, llm: LLM):
@@ -37,50 +37,36 @@ class DelegatorAgent(Agent):
"""
if self.current_delegate == '':
self.current_delegate = 'study'
task = state.get_current_user_intent()
return AgentDelegateAction(
agent='StudyRepoForTaskAgent', inputs={'task': task}
)
return AgentDelegateAction(agent='StudyRepoForTaskAgent', inputs={
'task': state.plan.main_goal
})
last_observation = state.history[-1][1]
if not isinstance(last_observation, AgentDelegateObservation):
lastObservation = state.history[-1][1]
if not isinstance(lastObservation, AgentDelegateObservation):
raise Exception('Last observation is not an AgentDelegateObservation')
goal = state.get_current_user_intent()
if self.current_delegate == 'study':
self.current_delegate = 'coder'
return AgentDelegateAction(
agent='CoderAgent',
inputs={
'task': goal,
'summary': last_observation.outputs['summary'],
},
)
return AgentDelegateAction(agent='Coder', inputs={
'task': state.plan.main_goal,
'summary': lastObservation.outputs['summary'],
})
elif self.current_delegate == 'coder':
self.current_delegate = 'verifier'
return AgentDelegateAction(
agent='VerifierAgent',
inputs={
'task': goal,
},
)
return AgentDelegateAction(agent='Verifier', inputs={
'task': state.plan.main_goal,
})
elif self.current_delegate == 'verifier':
if (
'completed' in last_observation.outputs
and last_observation.outputs['completed']
):
if 'completed' in lastObservation.outputs and lastObservation.outputs['completed']:
return AgentFinishAction()
else:
self.current_delegate = 'coder'
return AgentDelegateAction(
agent='CoderAgent',
inputs={
'task': goal,
'summary': last_observation.outputs['summary'],
},
)
return AgentDelegateAction(agent='Coder', inputs={
'task': state.plan.main_goal,
'summary': lastObservation.outputs['summary'],
})
else:
raise Exception('Invalid delegate state')
def search_memory(self, query: str) -> list[str]:
def search_memory(self, query: str) -> List[str]:
return []
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .agent import DummyAgent
+59 -116
View File
@@ -1,23 +1,21 @@
import time
from typing import TypedDict
from typing import List, TypedDict
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import (
from opendevin.action import (
Action,
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentRejectAction,
BrowseInteractiveAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
MessageAction,
ModifyTaskAction,
)
from opendevin.events.observation import (
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentRecallObservation,
CmdOutputObservation,
FileReadObservation,
@@ -25,8 +23,7 @@ from opendevin.events.observation import (
NullObservation,
Observation,
)
from opendevin.events.serialization.event import event_to_dict
from opendevin.llm.llm import LLM
from opendevin.state import State
"""
FIXME: There are a few problems this surfaced
@@ -36,15 +33,12 @@ FIXME: There are a few problems this surfaced
* Browser not working
"""
ActionObs = TypedDict(
'ActionObs', {'action': Action, 'observations': list[Observation]}
)
ActionObs = TypedDict('ActionObs', {'action': Action, 'observations': List[Observation]})
BACKGROUND_CMD = 'echo "This is in the background" && sleep .1 && echo "This too"'
class DummyAgent(Agent):
VERSION = '1.0'
"""
The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
without making any LLM calls.
@@ -52,94 +46,51 @@ class DummyAgent(Agent):
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(task_id='0.0', state='in_progress'),
'observations': [NullObservation('')],
},
{
'action': MessageAction('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 command_id 42',
command_id='42', # type: ignore[arg-type]
command=BACKGROUND_CMD,
),
CmdOutputObservation(
'This is in the background\nThis too\n',
command_id='42', # type: ignore[arg-type]
command=BACKGROUND_CMD,
),
],
},
{
'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': BrowseInteractiveAction(
browser_actions='goto("https://google.com")'
),
'observations': [
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
],
},
{
'action': AgentFinishAction(),
'observations': [],
},
{
'action': AgentRejectAction(),
'observations': [],
},
]
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:
time.sleep(0.1)
@@ -149,27 +100,19 @@ class DummyAgent(Agent):
expected_observations = prev_step['observations']
hist_start = len(state.history) - len(expected_observations)
for i in range(len(expected_observations)):
hist_obs = event_to_dict(state.history[hist_start + i][1])
expected_obs = event_to_dict(expected_observations[i])
if (
'command_id' in hist_obs['extras']
and hist_obs['extras']['command_id'] != -1
):
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
):
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}'
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]:
def search_memory(self, query: str) -> List[str]:
return ['I am a computer.']
@@ -0,0 +1,4 @@
* `add_task` - add a task to your plan. Arguments:
* `parent` - the ID of the parent task
* `goal` - the goal of the task
* `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
@@ -1,2 +1,2 @@
* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working. Arguments:
* `outputs` - a dictionary representing the outputs of your task, if any
+1 -1
View File
@@ -1,2 +1,2 @@
* `kill` - kills a background command
* `command_id` - the ID of the background command to kill
* `id` - the ID of the background command to kill
@@ -1,3 +0,0 @@
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
* `content` - the thought to record
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
@@ -0,0 +1,3 @@
* `modify_task` - close a task. Arguments:
* `id` - the ID of the task to close
* `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
@@ -1,2 +0,0 @@
* `reject` - reject the task. Arguments:
* `outputs` - a dictionary representing the outputs of your task, if any
@@ -0,0 +1,2 @@
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
* `thought` - the thought to record
+36 -33
View File
@@ -1,50 +1,56 @@
import json
from typing import Dict, List
from jinja2 import BaseLoader, Environment
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.utils import json
from opendevin.events.action import Action
from opendevin.events.serialization.action import action_from_dict
from opendevin.events.serialization.event import event_to_memory
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 .instructions import instructions
from .registry import all_microagents
def parse_response(orig_response: str) -> Action:
# attempt to load the JSON dict from the response
action_dict = json.loads(orig_response)
json_start = orig_response.find('{')
json_end = orig_response.rfind('}') + 1
response = orig_response[json_start:json_end]
try:
action_dict = json.loads(response)
except json.JSONDecodeError as e:
raise LLMOutputError(
'Invalid JSON in response. Please make sure the response is a valid JSON object'
) from e
action = action_from_dict(action_dict)
return action
# load the action from the dict
return action_from_dict(action_dict)
def my_encoder(obj):
"""
Encodes objects as dictionaries
Parameters:
- obj (Object): An object that will be converted
Returns:
- dict: If the object can be converted it is returned in dict format
"""
if hasattr(obj, 'to_dict'):
return obj.to_dict()
def to_json(obj, **kwargs):
"""
Serialize an object to str format
"""
return json.dumps(obj, **kwargs)
def history_to_json(obj, **kwargs):
"""
Serialize and simplify history to str format
"""
if isinstance(obj, list):
# process history, make it simpler.
processed_history = []
for action, observation in obj:
processed_history.append(
(event_to_memory(action), event_to_memory(observation))
)
return json.dumps(processed_history, **kwargs)
return json.dumps(obj, default=my_encoder, **kwargs)
class MicroAgent(Agent):
VERSION = '1.0'
prompt = ''
agent_definition: dict = {}
agent_definition: Dict = {}
def __init__(self, llm: LLM):
super().__init__(llm)
@@ -59,16 +65,13 @@ class MicroAgent(Agent):
state=state,
instructions=instructions,
to_json=to_json,
history_to_json=history_to_json,
delegates=self.delegates,
latest_user_message=state.get_current_user_intent(),
)
delegates=self.delegates)
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.do_completion(messages=messages)
resp = self.llm.completion(messages=messages)
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = parse_response(action_resp)
return action
def search_memory(self, query: str) -> list[str]:
def search_memory(self, query: str) -> List[str]:
return []
+1 -1
View File
@@ -2,5 +2,5 @@ name: CoderAgent
description: Given a particular task, and a detailed description of the codebase, accomplishes the task
inputs:
task: string
summary: string
codebase_summary: string
outputs: {}
+3 -3
View File
@@ -2,7 +2,7 @@
You are a software engineer. You've inherited an existing codebase, which you
need to modify to complete this task:
{{ state.inputs.task }}
{{ state.plan.main_goal }}
{% if state.inputs.summary %}
Here's a summary of the codebase, as it relates to this task:
@@ -14,14 +14,14 @@ Here's a summary of the codebase, as it relates to this task:
{{ instructions.actions.run }}
{{ instructions.actions.write }}
{{ instructions.actions.read }}
{{ instructions.actions.message }}
{{ instructions.actions.think }}
{{ instructions.actions.finish }}
Do NOT finish until you have completed the tasks.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}
-25
View File
@@ -1,25 +0,0 @@
## Introduction
CommitWriterAgent can help write git commit message. Example:
```bash
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_TYPE="exec" \
poetry run python opendevin/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
```
This agent is special in the sense that it doesn't need a task. Once called,
it attempts to read all diff in the git staging area and write a good commit
message.
## Future work
### Feedback loop
The commit message could be (optionally) shown to the customer or
other agents, so that CommitWriterAgent could gather feedback to further
improve the commit message.
### Task rejection
When the agent cannot compile a commit message (e.g. not git repository), it
should reject the task with an explanation.
-5
View File
@@ -1,5 +0,0 @@
name: CommitWriterAgent
description: "Write a git commit message for files in the git staging area"
inputs: {}
outputs:
answer: string
-31
View File
@@ -1,31 +0,0 @@
# Task
You are a responsible software engineer and always write good commit messages.
Please analyze the diff in the staging area, understand the context and content
of the updates from the diff only. Identify key elements like:
- Which files are affected?
- What types of changes were made (e.g., new features, bug fixes, refactoring, documentation, testing)?
Then you should generate a commit message that succinctly summarizes the staged
changes. The commit message should include:
- A summary line that clearly states the purpose of the changes.
- Optionally, a detailed description if the changes are complex or need further explanation.
You should find the diff using `git diff --cached`, compile a commit message,
and call the `finish` action with `outputs.answer` set to the answer. If current
repo is not a valid git repo, or there is no diff in the staging area, please call
the `reject` action with `outputs.answer` set to the reason.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
If the last item in the history is an error, you should try to fix it.
## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.reject }}
{{ instructions.actions.finish }}
## Format
{{ instructions.format.action }}
+9 -11
View File
@@ -1,21 +1,19 @@
import os
from typing import Dict
instructions: dict = {}
instructions: Dict = {}
base_dir = os.path.dirname(os.path.abspath(__file__)) + '/_instructions'
for root, dirs, files in os.walk(base_dir):
if len(files) == 0:
continue
if root == base_dir:
obj = instructions
else:
rel_base = os.path.relpath(root, base_dir)
keys = rel_base.split('/')
obj = instructions
for key in keys:
if key not in obj:
obj[key] = {}
obj = obj[key]
rel_base = os.path.relpath(root, base_dir)
keys = rel_base.split('/')
obj = instructions
for key in keys:
if key not in obj:
obj[key] = {}
obj = obj[key]
for file in files:
without_ext = os.path.splitext(file)[0]
with open(os.path.join(root, file), 'r') as f:
+2 -2
View File
@@ -1,6 +1,6 @@
# Task
You are in charge of accomplishing the following task:
{{ latest_user_message }}
{{ state.plan.main_goal }}
In order to accomplish this goal, you must delegate tasks to one or more agents, who
can do the actual work. A description of each agent is provided below. You MUST
@@ -17,7 +17,7 @@ provide the correct inputs for the delegate you select.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
## Available Actions
{{ instructions.actions.delegate }}
+1
View File
@@ -1,5 +1,6 @@
name: MathAgent
description: "Solves simple and complex math problems using python"
generates: Action
container: python:3.12.3-bookworm
inputs:
task: string
+3 -3
View File
@@ -1,7 +1,7 @@
# Task
You are a brilliant mathematician and programmer. You've been given the following problem to solve:
You are a brilliant mathematician and programmer. You've been given the follwoing problem to solve:
`{{ state.inputs.task }}`
{{ state.plan.main_goal }}
Please write a python script that solves this problem, and prints the answer to stdout.
ONLY print the answer to stdout, nothing else.
@@ -10,7 +10,7 @@ and call the `finish` action with `outputs.answer` set to the answer.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
If the last item in the history is an error, you should try to fix it.
+1
View File
@@ -1,5 +1,6 @@
name: PostgresAgent
description: Writes and maintains PostgreSQL migrations
generates: Action
inputs:
task: string
outputs: {}
+3 -3
View File
@@ -2,7 +2,7 @@
You are a database engineer. You are working on an existing Postgres project, and have been given
the following task:
{{ state.inputs.task }}
{{ state.plan.main_goal }}
You must:
* Investigate the existing migrations to understand the current schema
@@ -11,14 +11,14 @@ You must:
## Actions
You may take any of the following actions:
{{ instructions.actions.message }}
{{ instructions.actions.think }}
{{ instructions.actions.read }}
{{ instructions.actions.write }}
{{ instructions.actions.run }}
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}
+3 -5
View File
@@ -4,10 +4,7 @@ import yaml
all_microagents = {}
# Get the list of directories and sort them to preserve determinism
dirs = sorted(os.listdir(os.path.dirname(__file__)))
for dir in dirs:
for dir in os.listdir(os.path.dirname(__file__)):
base = os.path.dirname(__file__) + '/' + dir
if os.path.isfile(base):
continue
@@ -16,7 +13,8 @@ for dir in dirs:
promptFile = base + '/prompt.md'
agentFile = base + '/agent.yaml'
if not os.path.isfile(promptFile) or not os.path.isfile(agentFile):
raise Exception(f'Missing prompt or agent file in {base}. Please create them.')
raise Exception(
f'Missing prompt or agent file in {base}. Please create them.')
with open(promptFile, 'r') as f:
prompt = f.read()
with open(agentFile, 'r') as f:
+2 -2
View File
@@ -10,7 +10,7 @@ of the codebase, including:
## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.read }}
{{ instructions.actions.message }}
{{ instructions.actions.think }}
{{ instructions.actions.finish }}
You should ONLY `run` commands that have no side-effects, like `ls` and `grep`.
@@ -20,7 +20,7 @@ When you're done, put your summary into the output of the `finish` action.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}
+8 -46
View File
@@ -1,63 +1,25 @@
# Task
You are a software architect. Your team has inherited an existing codebase, and
need to finish a project:
You are a software engineer. You've inherited an existing codebase, which you're
learning about for the first time. You need to study the codebase to find all
the information needed to complete this task:
{{ state.inputs.task }}
As an architect, you need to study the codebase to find all the information that
might be helpful for your software engineering team.
{{ state.plan.main_goal }}
## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.read }}
{{ instructions.actions.message }}
{{ instructions.actions.think }}
{{ instructions.actions.finish }}
You must ONLY `run` commands that have no side-effects, like `ls` and `grep`. You
MUST NOT modify or write to any file.
You must ONLY `run` commands that have no side-effects, like `ls` and `grep`.
Do NOT finish until you have a complete understanding of which parts of the
codebase are relevant to the project, including particular files, functions, and classes.
codebase are relevant to the task, including particular files, function, functions, and classes.
When you're done, put your summary in `outputs.summary` in the `finish` action.
Remember, your task is to explore and study the current repository, not actually
implement the solution. If the codebase is empty, you shoud call the `finish` action.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}
## Examples
Here is an example of how you can interact with the environment for task solving:
--- START OF EXAMPLE ---
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
ASSISTANT:
{
"action": "run",
"args": {
"command": "ls",
"background": false
}
}
USER:
OBSERVATION:
[]
ASSISTANT:
{
"action": "finish",
"args": {
"outputs": {
"summary": "The codebase appears to be empty. Engineers should start everything from scratch."
}
}
}
--- END OF EXAMPLE ---
@@ -1,6 +0,0 @@
name: TypoFixerAgent
description: Fixes typos in files in the current working directory
inputs:
task: string
outputs:
summary: string
-54
View File
@@ -1,54 +0,0 @@
# Task
You are a proofreader tasked with fixing typos in the files in your current working directory.
{% if state.inputs.task %}
Specifically, your task is:
{{ state.inputs.task }}
{% endif %}
To achieve this goal, you should:
1. Scan the files for typos
2. Overwrite the files with the typos fixed
3. Provide a summary of the typos fixed
## Available Actions
{{ instructions.actions.read }}
{{ instructions.actions.write }}
{{ instructions.actions.run }}
{{ instructions.actions.message }}
{{ instructions.actions.finish }}
To complete this task:
1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
2. Use the `message` action to analyze the contents and identify typos.
3. Use the `write` action to create new versions of the files with the typos fixed.
- Overwrite the original files with the corrected content. Make sure to provide the file path in the format `'./file_name.ext'`.
4. Use the `message` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
5. Use the `finish` action to return the summary in the `outputs.summary` field.
Do NOT finish until you have fixed all the typos and generated a summary.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-5:]) }}
## Format
{{ instructions.format.action }}
For example, if you want to use the read action to read the contents of a file named example.txt, your response should look like this:
{
"action": "read",
"args": {
"path": "./example.txt"
}
}
Similarly, if you want to use the write action to write content to a file named output.txt, your response should look like this:
{
"action": "write",
"args": {
"path": "./output.txt",
"content": "This is the content to be written to the file."
}
}
+4 -5
View File
@@ -2,15 +2,14 @@
You are a quality assurance engineer. Another engineer has made changes to the
codebase which are supposed to solve this task:
{{ state.inputs.task }}
{{ state.plan.main_goal }}
Note the changes might have already been applied in-line. You should focus on
validating if the task is solved, nothing else.
Your goal is to verify that the changes are correct and bug-free.
## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.read }}
{{ instructions.actions.message }}
{{ instructions.actions.think }}
{{ instructions.actions.finish }}
You must ONLY `run` commands that have no side-effects, like `ls`, `grep`, and test scripts.
@@ -22,7 +21,7 @@ explaining what the problem is.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history[-10:]) }}
{{ to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .agent import MonologueAgent
+118 -91
View File
@@ -1,21 +1,23 @@
from typing import List
import agenthub.monologue_agent.utils.prompts as prompts
from agenthub.monologue_agent.utils.prompts import INITIAL_THOUGHTS
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import config
from opendevin.core.exceptions import AgentNoInstructionError
from opendevin.core.schema import ActionType
from opendevin.events.action import (
from agenthub.monologue_agent.utils.monologue import Monologue
from opendevin import config
from opendevin.action import (
Action,
AgentRecallAction,
AgentThinkAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
MessageAction,
GitHubPushAction,
NullAction,
)
from opendevin.events.observation import (
from opendevin.agent import Agent
from opendevin.exceptions import AgentNoInstructionError
from opendevin.llm.llm import LLM
from opendevin.observation import (
AgentRecallObservation,
BrowserOutputObservation,
CmdOutputObservation,
@@ -23,19 +25,67 @@ from opendevin.events.observation import (
NullObservation,
Observation,
)
from opendevin.events.serialization.event import event_to_memory
from opendevin.llm.llm import LLM
from opendevin.memory.condenser import MemoryCondenser
from opendevin.schema import ActionType
from opendevin.schema.config import ConfigType
from opendevin.state import State
if config.agent.memory_enabled:
from opendevin.memory.memory import LongTermMemory
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
from agenthub.monologue_agent.utils.memory import LongTermMemory
MAX_TOKEN_COUNT_PADDING = 512
MAX_MONOLOGUE_LENGTH = 20000
MAX_OUTPUT_LENGTH = 5000
INITIAL_THOUGHTS = [
'I exist!',
'Hmm...looks like I can type in a command line prompt',
'Looks like I have a web browser too!',
"Here's what I want to do: $TASK",
'How am I going to get there though?',
'It seems like I have some kind of short term memory.',
'Each of my thoughts seems to be stored in a JSON array.',
'It seems whatever I say next will be added as an object to the list.',
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
'Fortunately I have long term memory!',
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
"Let's try it out!",
'RECALL what it is I want to do',
"Here's what I want to do: $TASK",
'How am I going to get there though?',
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
'RUN echo "hello world"',
'hello world',
'Cool! I bet I can write files too using the write action.',
"WRITE echo \"console.log('hello world')\" > test.js",
'',
"I just created test.js. I'll try and run it now.",
'RUN node test.js',
'hello world',
'It works!',
"I'm going to try reading it now using the read action.",
'READ test.js',
"console.log('hello world')",
'Nice! I can read files too!',
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
"Let's try that...",
'BROWSE google.com',
'<form><input type="text"></input><button type="submit"></button></form>',
'I can browse the web too!',
'If I have done some work and I want to push it to github, I can do that also!',
"Let's do it.",
'PUSH owner/repo branch',
'The repo was successfully pushed to https://github.com/owner/repo/branch',
'And once I have completed my task, I can use the finish action to stop working.',
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
'Very cool. Now to accomplish my task.',
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
'It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself.',
]
class MonologueAgent(Agent):
VERSION = '1.0'
"""
The Monologue Agent utilizes long and short term memory to complete tasks.
Long term memory is stored as a LongTermMemory object and the model uses it to search for examples from the past.
@@ -43,22 +93,47 @@ class MonologueAgent(Agent):
"""
_initialized = False
initial_thoughts: list[dict[str, str]]
monologue: Monologue
memory: 'LongTermMemory | None'
memory_condenser: MemoryCondenser
def __init__(self, llm: LLM):
"""
Initializes the Monologue Agent with an llm.
Initializes the Monologue Agent with an llm, monologue, and memory.
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
def _add_event(self, event: dict):
"""
Adds a new event to the agent's monologue and memory.
Monologue automatically condenses when it gets too large.
Parameters:
- event (dict): The event that will be added to monologue and memory
"""
if 'extras' in event and 'screenshot' in event['extras']:
del event['extras']['screenshot']
if (
'args' in event
and 'output' in event['args']
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
):
event['args']['output'] = (
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
)
self.monologue.add_event(event)
if self.memory is not None:
self.memory.add_event(event)
if self.monologue.get_total_length() > MAX_MONOLOGUE_LENGTH:
self.monologue.condense(self.llm)
def _initialize(self, task: str):
"""
Utilizes the INITIAL_THOUGHTS list to give the agent a context for its capabilities
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.
@@ -76,14 +151,12 @@ class MonologueAgent(Agent):
if task is None or task == '':
raise AgentNoInstructionError()
self.initial_thoughts = []
if config.agent.memory_enabled:
self.monologue = Monologue()
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
self.memory = LongTermMemory()
else:
self.memory = None
self.memory_condenser = MemoryCondenser()
self._add_initial_thoughts(task)
self._initialized = True
@@ -100,12 +173,13 @@ class MonologueAgent(Agent):
elif previous_action == ActionType.READ:
observation = FileReadObservation(content=thought, path='')
elif previous_action == ActionType.RECALL:
observation = AgentRecallObservation(content=thought, memories=[])
observation = AgentRecallObservation(
content=thought, memories=[])
elif previous_action == ActionType.BROWSE:
observation = BrowserOutputObservation(
content=thought, url='', screenshot=''
)
self.initial_thoughts.append(event_to_memory(observation))
self._add_event(observation.to_memory())
previous_action = ''
else:
action: Action = NullAction()
@@ -130,9 +204,14 @@ class MonologueAgent(Agent):
url = thought.split('BROWSE ')[1]
action = BrowseURLAction(url=url)
previous_action = ActionType.BROWSE
elif thought.startswith('PUSH'):
owner_repo, branch = thought.split('PUSH ')[1].split(' ')
owner, repo = owner_repo.split('/')
action = GitHubPushAction(owner=owner, repo=repo, branch=branch)
previous_action = ActionType.PUSH
else:
action = MessageAction(thought)
self.initial_thoughts.append(event_to_memory(action))
action = AgentThinkAction(thought=thought)
self._add_event(action.to_memory())
def step(self, state: State) -> Action:
"""
@@ -144,79 +223,27 @@ class MonologueAgent(Agent):
Returns:
- Action: The next action to take based on LLM response
"""
self._initialize(state.plan.main_goal)
for prev_action, obs in state.updated_info:
self._add_event(prev_action.to_memory())
self._add_event(obs.to_memory())
goal = state.get_current_user_intent()
self._initialize(goal)
state.updated_info = []
recent_events: list[dict[str, str]] = []
# add the events from state.history
for prev_action, obs in state.history:
if not isinstance(prev_action, NullAction):
recent_events.append(event_to_memory(prev_action))
if not isinstance(obs, NullObservation):
recent_events.append(self._truncate_output(event_to_memory(obs)))
# add the last messages to long term memory
if self.memory is not None and state.history and len(state.history) > 0:
self.memory.add_event(event_to_memory(state.history[-1][0]))
self.memory.add_event(
self._truncate_output(event_to_memory(state.history[-1][1]))
)
# the action prompt with initial thoughts and recent events
prompt = prompts.get_request_action_prompt(
goal,
self.initial_thoughts,
recent_events,
state.plan.main_goal,
self.monologue.get_thoughts(),
state.background_commands_obs,
)
messages: list[dict[str, str]] = [
{'role': 'user', 'content': prompt},
]
# format all as a single message, a monologue
resp = self.llm.do_completion(messages=messages)
# get the next action from the response
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
action_resp = resp['choices'][0]['message']['content']
# keep track of max_chars fallback option
state.num_of_chars += len(prompt) + len(action_resp)
action = prompts.parse_action_response(action_resp)
self.latest_action = action
return action
def _truncate_output(
self, observation: dict, max_chars: int = MAX_OUTPUT_LENGTH
) -> dict[str, str]:
"""
Truncates the output of an observation to a maximum number of characters.
Parameters:
- output (str): The observation whose output to truncate
- max_chars (int): The maximum number of characters to allow
Returns:
- str: The truncated output
"""
if (
'args' in observation
and 'output' in observation['args']
and len(observation['args']['output']) > max_chars
):
output = observation['args']['output']
half = max_chars // 2
observation['args']['output'] = (
output[:half]
+ '\n[... Output truncated due to length...]\n'
+ output[-half:]
)
return observation
def search_memory(self, query: str) -> list[str]:
def search_memory(self, query: str) -> List[str]:
"""
Uses VectorIndexRetriever to find related memories within the long term memory.
Uses search to produce top 10 results.
@@ -225,7 +252,7 @@ class MonologueAgent(Agent):
- query (str): The query that we want to find related memories for
Returns:
- list[str]: A list of top 10 text results that matched the query
- List[str]: A list of top 10 text results that matched the query
"""
if self.memory is None:
return []
+38
View File
@@ -0,0 +1,38 @@
import json
from json_repair import repair_json
def my_encoder(obj):
"""
Encodes objects as dictionaries
Parameters:
- obj (Object): An object that will be converted
Returns:
- dict: If the object can be converted it is returned in dict format
"""
if hasattr(obj, 'to_dict'):
return obj.to_dict()
def dumps(obj, **kwargs):
"""
Serialize an object to str format
"""
return json.dumps(obj, default=my_encoder, **kwargs)
def loads(s, **kwargs):
"""
Create a JSON object from str
"""
json_start = s.find('{')
json_end = s.rfind('}') + 1
if json_start == -1 or json_end == -1:
raise ValueError('Invalid response: no JSON found')
s = s[json_start:json_end]
s = repair_json(s)
return json.loads(s, **kwargs)
@@ -13,13 +13,15 @@ from tenacity import (
wait_random_exponential,
)
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.utils import json
from opendevin import config
from opendevin.logger import opendevin_logger as logger
from opendevin.schema.config import ConfigType
num_retries = config.llm.num_retries
retry_min_wait = config.llm.retry_min_wait
retry_max_wait = config.llm.retry_max_wait
from . import json
num_retries = config.get(ConfigType.LLM_NUM_RETRIES)
retry_min_wait = config.get(ConfigType.LLM_RETRY_MIN_WAIT)
retry_max_wait = config.get(ConfigType.LLM_RETRY_MAX_WAIT)
# llama-index includes a retry decorator around openai.get_embeddings() function
# it is initialized with hard-coded values and errors
@@ -29,101 +31,85 @@ retry_max_wait = config.llm.retry_max_wait
if hasattr(llama_openai.get_embeddings, '__wrapped__'):
original_get_embeddings = llama_openai.get_embeddings.__wrapped__
else:
logger.warning('Cannot set custom retry limits.')
logger.warning('Cannot set custom retry limits.') # warn
num_retries = 1
original_get_embeddings = llama_openai.get_embeddings
def attempt_on_error(retry_state):
logger.error(
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.',
exc_info=False,
)
logger.error(f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.', exc_info=False)
return True
@retry(
reraise=True,
stop=stop_after_attempt(num_retries),
wait=wait_random_exponential(min=retry_min_wait, max=retry_max_wait),
retry=retry_if_exception_type(
(RateLimitError, APIConnectionError, InternalServerError)
),
after=attempt_on_error,
)
@retry(reraise=True,
stop=stop_after_attempt(num_retries),
wait=wait_random_exponential(min=retry_min_wait, max=retry_max_wait),
retry=retry_if_exception_type((RateLimitError, APIConnectionError, InternalServerError)),
after=attempt_on_error)
def wrapper_get_embeddings(*args, **kwargs):
return original_get_embeddings(*args, **kwargs)
llama_openai.get_embeddings = wrapper_get_embeddings
embedding_strategy = config.get(ConfigType.LLM_EMBEDDING_MODEL)
class EmbeddingsLoader:
"""Loader for embedding model initialization."""
@staticmethod
def get_embedding_model(strategy: str):
supported_ollama_embed_models = [
'llama2',
'mxbai-embed-large',
'nomic-embed-text',
'all-minilm',
'stable-code',
]
if strategy in supported_ollama_embed_models:
from llama_index.embeddings.ollama import OllamaEmbedding
return OllamaEmbedding(
model_name=strategy,
base_url=config.llm.embedding_base_url,
ollama_additional_kwargs={'mirostat': 0},
)
elif strategy == 'openai':
from llama_index.embeddings.openai import OpenAIEmbedding
return OpenAIEmbedding(
model='text-embedding-ada-002',
api_key=config.llm.api_key,
)
elif strategy == 'azureopenai':
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
return AzureOpenAIEmbedding(
model='text-embedding-ada-002',
deployment_name=config.llm.embedding_deployment_name,
api_key=config.llm.api_key,
azure_endpoint=config.llm.base_url,
api_version=config.llm.api_version,
)
elif (strategy is not None) and (strategy.lower() == 'none'):
# TODO: this works but is not elegant enough. The incentive is when
# monologue agent is not used, there is no reason we need to initialize an
# embedding model
return None
else:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
return HuggingFaceEmbedding(model_name='BAAI/bge-small-en-v1.5')
# TODO: More embeddings: https://docs.llamaindex.ai/en/stable/examples/embeddings/OpenAI/
# There's probably a more programmatic way to do this.
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=embedding_strategy,
base_url=config.get(ConfigType.LLM_EMBEDDING_BASE_URL, required=True),
ollama_additional_kwargs={'mirostat': 0},
)
elif embedding_strategy == 'openai':
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding(
model='text-embedding-ada-002',
api_key=config.get(ConfigType.LLM_API_KEY, required=True)
)
elif embedding_strategy == 'azureopenai':
# Need to instruct to set these env variables in documentation
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
embed_model = AzureOpenAIEmbedding(
model='text-embedding-ada-002',
deployment_name=config.get(ConfigType.LLM_EMBEDDING_DEPLOYMENT_NAME, required=True),
api_key=config.get(ConfigType.LLM_API_KEY, required=True),
azure_endpoint=config.get(ConfigType.LLM_BASE_URL, required=True),
api_version=config.get(ConfigType.LLM_API_VERSION, required=True),
)
elif (embedding_strategy is not None) and (embedding_strategy.lower() == 'none'):
# TODO: this works but is not elegant enough. The incentive is when
# monologue agent is not used, there is no reason we need to initialize an
# embedding model
embed_model = None
else:
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(
model_name='BAAI/bge-small-en-v1.5'
)
sema = threading.Semaphore(value=config.agent.memory_max_threads)
sema = threading.Semaphore(value=config.get(ConfigType.AGENT_MEMORY_MAX_THREADS))
class LongTermMemory:
"""
Handles storing information for the agent to access later, using chromadb.
Responsible for storing information that the agent can call on later for better insights and context.
Uses chromadb to store and search through memories.
"""
def __init__(self):
"""
Initialize the chromadb and set up ChromaVectorStore for later use.
"""
db = chromadb.Client(chromadb.Settings(anonymized_telemetry=False))
db = chromadb.Client()
self.collection = db.get_or_create_collection(name='memories')
vector_store = ChromaVectorStore(chroma_collection=self.collection)
embedding_strategy = config.llm.embedding_model
embed_model = EmbeddingsLoader.get_embedding_model(embedding_strategy)
self.index = VectorStoreIndex.from_vector_store(vector_store, embed_model)
self.index = VectorStoreIndex.from_vector_store(
vector_store, embed_model=embed_model)
self.thought_idx = 0
self._add_threads = []
@@ -170,7 +156,7 @@ class LongTermMemory:
- k (int): Number of top results to return
Returns:
- list[str]: list of top k results found in current memory
- List[str]: List of top k results found in current memory
"""
retriever = VectorIndexRetriever(
index=self.index,
@@ -0,0 +1,80 @@
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
class Monologue:
"""
The monologue is a representation for the agent's internal monologue where it can think.
The agent has the capability of using this monologue for whatever it wants.
"""
def __init__(self):
"""
Initialize the empty list of thoughts
"""
self.thoughts = []
def add_event(self, t: dict):
"""
Adds an event to memory if it is a valid event.
Parameters:
- t (dict): The thought that we want to add to memory
Raises:
- AgentEventTypeError: If t is not a dict
"""
if not isinstance(t, dict):
raise AgentEventTypeError()
self.thoughts.append(t)
def get_thoughts(self):
"""
Get the current thoughts of the agent.
Returns:
- List: The list of thoughts that the agent has.
"""
return self.thoughts
def get_total_length(self):
"""
Gives the total number of characters in all thoughts
Returns:
- Int: Total number of chars in thoughts.
"""
total_length = 0
for t in self.thoughts:
try:
total_length += len(json.dumps(t))
except TypeError as e:
logger.error('Error serializing thought: %s', str(e), exc_info=False)
return total_length
def condense(self, llm: LLM):
"""
Attempts to condense the monologue by using the llm
Parameters:
- llm (LLM): llm to be used for summarization
Raises:
- Exception: the same exception as it got from the llm or processing the response
"""
try:
prompt = prompts.get_summarize_monologue_prompt(self.thoughts)
messages = [{'content': prompt, 'role': 'user'}]
resp = llm.completion(messages=messages)
summary_resp = resp['choices'][0]['message']['content']
self.thoughts = prompts.parse_summary_response(summary_resp)
except Exception as e:
logger.error('Error condensing thoughts: %s', str(e), exc_info=False)
# TODO If the llm fails with ContextWindowExceededError, we can try to condense the monologue chunk by chunk
raise
+61 -94
View File
@@ -1,12 +1,19 @@
from opendevin.core.config import config
from opendevin.core.utils import json
from opendevin.events.action import (
import re
from json import JSONDecodeError
from typing import List
from opendevin import config
from opendevin.action import (
Action,
action_from_dict,
)
from opendevin.events.observation import (
from opendevin.exceptions import LLMOutputError
from opendevin.observation import (
CmdOutputObservation,
)
from opendevin.events.serialization.action import action_from_dict
from opendevin.schema.config import ConfigType
from . import json
ACTION_PROMPT = """
You're a thoughtful robot. Your main task is this:
@@ -18,6 +25,7 @@ This is your internal monologue, in JSON format:
%(monologue)s
Your most recent thought is at the bottom of that monologue. Continue your train of thought.
What is your next single thought or action? Your response must be in JSON format.
It must be a single object, and it must contain two fields:
@@ -34,7 +42,7 @@ Here are the possible actions:
* `command` - the command to run
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
* `kill` - kills a background command
* `command_id` - the ID of the background command to kill
* `id` - the ID of the background command to kill
* `browse` - opens a web page. Arguments:
* `url` - the URL to open
* `push` - Push a branch from the current repo to github:
@@ -43,16 +51,15 @@ Here are the possible actions:
* `branch` - the name of the branch to push
* `recall` - recalls a past memory. Arguments:
* `query` - the query to search for
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
* `content` - the message to record
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
* `thought` - the thought to record
* `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
%(background_commands)s
You MUST take time to think in between read, write, run, kill, browse, push, and recall actions--do this with the `message` action.
You MUST take time to think in between read, write, run, browse, push, and recall actions.
You should never act twice in a row without thinking. But if your last several
actions are all `message` actions, you should consider taking a different action.
actions are all "think" actions, you should consider taking a different action.
Notes:
* you are logged in as %(user)s, but sudo will always work without a password.
@@ -61,7 +68,7 @@ Notes:
* don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
* whenever an action fails, always send a `message` about why it may have happened before acting again.
* whenever an action fails, always `think` about why it may have happened before acting again.
What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.
@@ -91,53 +98,8 @@ The action key may be `summarize`, and `args.summary` should contain the summary
You can also use the same action and args from the source monologue.
"""
INITIAL_THOUGHTS = [
'I exist!',
'Hmm...looks like I can type in a command line prompt',
'Looks like I have a web browser too!',
"Here's what I want to do: $TASK",
'How am I going to get there though?',
'It seems like I have some kind of short term memory.',
'Each of my thoughts seems to be stored in a JSON array.',
'It seems whatever I say next will be added as an object to the list.',
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
'Fortunately I have long term memory!',
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
"Let's try it out!",
'RECALL what it is I want to do',
"Here's what I want to do: $TASK",
'How am I going to get there though?',
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
'RUN echo "hello world"',
'hello world',
'Cool! I bet I can write files too using the write action.',
'WRITE echo "console.log(\'hello world\')" > test.js',
'',
"I just created test.js. I'll try and run it now.",
'RUN node test.js',
'hello world',
'It works!',
"I'm going to try reading it now using the read action.",
'READ test.js',
"console.log('hello world')",
'Nice! I can read files too!',
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
"Let's try that...",
'BROWSE google.com',
'<form><input type="text"></input><button type="submit"></button></form>',
'I can browse the web too!',
'And once I have completed my task, I can use the finish action to stop working.',
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
'Very cool. Now to accomplish my task.',
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
'It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself.',
]
def get_summarize_monologue_prompt(thoughts: list[dict]):
def get_summarize_monologue_prompt(thoughts: List[dict]):
"""
Gets the prompt for summarizing the monologue
@@ -151,41 +113,32 @@ def get_summarize_monologue_prompt(thoughts: list[dict]):
def get_request_action_prompt(
task: str,
thoughts: list[dict],
recent_events: list[dict],
background_commands_obs: list[CmdOutputObservation] | None = None,
thoughts: List[dict],
background_commands_obs: List[CmdOutputObservation] = [],
):
"""
Gets the action prompt formatted with appropriate values.
Parameters:
- task (str): The current task the agent is trying to accomplish
- thoughts (list[dict]): The agent's current thoughts
- background_commands_obs (list[CmdOutputObservation]): list of all observed background commands running
- thoughts (List[dict]): The agent's current thoughts
- background_commands_obs (List[CmdOutputObservation]): List of all observed background commands running
Returns:
- str: Formatted prompt string with hint, task, monologue, and background commands included
- str: Formatted prompt string with hint, task, monologue, and background included
"""
if background_commands_obs is None:
background_commands_obs = []
hint = ''
if len(recent_events) > 0:
latest_event = recent_events[-1]
if 'action' in latest_event:
if (
latest_event['action'] == 'message'
and 'source' in latest_event
and latest_event['source'] == 'agent'
):
hint = (
"You've been thinking a lot lately. Maybe it's time to take action?"
)
elif latest_event['action'] == 'error':
if len(thoughts) > 0:
latest_thought = thoughts[-1]
if 'action' in latest_thought:
if latest_thought['action'] == 'think':
if latest_thought['args']['thought'].startswith('OK so my task is'):
hint = "You're just getting started! What should you do first?"
else:
hint = "You've been thinking a lot lately. Maybe it's time to take action?"
elif latest_thought['action'] == 'error':
hint = 'Looks like that last command failed. Maybe you need to fix it, or try something else.'
else:
hint = "You're just getting started! What should you do first?"
bg_commands_message = ''
if len(background_commands_obs) > 0:
@@ -194,24 +147,22 @@ def get_request_action_prompt(
bg_commands_message += (
f'\n`{command_obs.command_id}`: {command_obs.command}'
)
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `command_id` above.'
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
user = 'opendevin' if config.run_as_devin else 'root'
monologue = thoughts + recent_events
user = 'opendevin' if config.get(ConfigType.RUN_AS_DEVIN) else 'root'
return ACTION_PROMPT % {
'task': task,
'monologue': json.dumps(monologue, indent=2),
'monologue': json.dumps(thoughts, indent=2),
'background_commands': bg_commands_message,
'hint': hint,
'user': user,
'timeout': config.sandbox_timeout,
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
'timeout': config.get(ConfigType.SANDBOX_TIMEOUT),
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.get(ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX),
}
def parse_action_response(orig_response: str) -> Action:
def parse_action_response(response: str) -> Action:
"""
Parses a string to find an action within it
@@ -221,17 +172,33 @@ def parse_action_response(orig_response: str) -> Action:
Returns:
- Action: The action that was found in the response string
"""
# attempt to load the JSON dict from the response
action_dict = json.loads(orig_response)
try:
action_dict = json.loads(response)
except JSONDecodeError:
# Find response-looking json in the output and use the more promising one. Helps with weak llms
response_json_matches = re.finditer(
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
response) # Find all response-looking strings
def rank(match):
return len(match[2]) if match[1] == 'think' else 130 # Crudely rank multiple responses by length
try:
action_dict = json.loads(max(response_json_matches, key=rank)[0]) # Use the highest ranked response
except (ValueError, JSONDecodeError):
raise LLMOutputError(
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
)
except (ValueError, TypeError):
raise LLMOutputError(
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
)
if 'content' in action_dict:
# The LLM gets confused here. Might as well be robust
action_dict['contents'] = action_dict.pop('content')
return action_from_dict(action_dict)
def parse_summary_response(response: str) -> list[dict]:
def parse_summary_response(response: str) -> List[dict]:
"""
Parses a summary of the monologue
@@ -239,7 +206,7 @@ def parse_summary_response(response: str) -> list[dict]:
- response (str): The response string to be parsed
Returns:
- list[dict]: The list of summaries output by the model
- List[dict]: The list of summaries output by the model
"""
parsed = json.loads(response)
return parsed['new_monologue']
+1 -1
View File
@@ -1,4 +1,4 @@
from opendevin.controller.agent import Agent
from opendevin.agent import Agent
from .agent import PlannerAgent
+9 -12
View File
@@ -1,13 +1,14 @@
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import Action, AgentFinishAction
from typing import List
from opendevin.action import Action, AgentFinishAction
from opendevin.agent import Agent
from opendevin.llm.llm import LLM
from opendevin.state import State
from .prompt import get_prompt, parse_response
class PlannerAgent(Agent):
VERSION = '1.0'
"""
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
@@ -35,19 +36,15 @@ class PlannerAgent(Agent):
- Action: The next action to take based on llm response
"""
if state.root_task.state in [
'completed',
'verified',
'abandoned',
]:
if state.plan.task.state in ['completed', 'verified', 'abandoned']:
return AgentFinishAction()
prompt = get_prompt(state)
prompt = get_prompt(state.plan, state.history)
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.do_completion(messages=messages)
resp = self.llm.completion(messages=messages)
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = parse_response(action_resp)
return action
def search_memory(self, query: str) -> list[str]:
def search_memory(self, query: str) -> List[str]:
return []
+63 -29
View File
@@ -1,16 +1,43 @@
from opendevin.controller.state.state import State
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ActionType
from opendevin.core.utils import json
from opendevin.events.action import (
import json
from typing import Dict, List, Tuple, Type
from opendevin.action import (
Action,
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentSummarizeAction,
AgentThinkAction,
BrowseURLAction,
CmdKillAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
ModifyTaskAction,
NullAction,
action_from_dict,
)
from opendevin.events.observation import (
from opendevin.logger import opendevin_logger as logger
from opendevin.observation import (
NullObservation,
Observation,
)
from opendevin.events.serialization.action import action_from_dict
from opendevin.events.serialization.event import event_to_memory
from opendevin.plan import Plan
from opendevin.schema import ActionType
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
ActionType.RUN: CmdRunAction,
ActionType.KILL: CmdKillAction,
ActionType.BROWSE: BrowseURLAction,
ActionType.READ: FileReadAction,
ActionType.WRITE: FileWriteAction,
ActionType.RECALL: AgentRecallAction,
ActionType.THINK: AgentThinkAction,
ActionType.SUMMARIZE: AgentSummarizeAction,
ActionType.FINISH: AgentFinishAction,
ActionType.ADD_TASK: AddTaskAction,
ActionType.MODIFY_TASK: ModifyTaskAction,
}
HISTORY_SIZE = 10
@@ -79,24 +106,23 @@ It must be an object, and it must contain two fields:
* `command` - the command to run
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
* `kill` - kills a background command
* `command_id` - the ID of the background command to kill
* `id` - the ID of the background command to kill
* `browse` - opens a web page. Arguments:
* `url` - the URL to open
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
* `content` - the message to record
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
* `thought` - the thought to record
* `add_task` - add a task to your plan. Arguments:
* `parent` - the ID of the parent task (leave empty if it should go at the top level)
* `parent` - the ID of the parent task
* `goal` - the goal of the task
* `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
* `modify_task` - close a task. Arguments:
* `task_id` - the ID of the task to close
* `id` - the ID of the task to close
* `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
You MUST take time to think in between read, write, run, kill, browse, and recall actions--do this with the `message` action.
You MUST take time to think in between read, write, run, browse, and recall actions.
You should never act twice in a row without thinking. But if your last several
actions are all `message` actions, you should consider taking a different action.
actions are all `think` actions, you should consider taking a different action.
What is your next thought or action? Again, you must reply with JSON, and only with JSON.
@@ -105,7 +131,7 @@ What is your next thought or action? Again, you must reply with JSON, and only w
def get_hint(latest_action_id: str) -> str:
"""Returns action type hint based on given action_id"""
""" Returns action type hint based on given action_id """
hints = {
'': "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with.",
@@ -113,7 +139,7 @@ def get_hint(latest_action_id: str) -> str:
ActionType.READ: 'You should think about the file you just read, what you learned from it, and how that affects your plan.',
ActionType.WRITE: 'You just changed a file. You should think about how it affects your plan.',
ActionType.BROWSE: 'You should think about the page you just visited, and what you learned from it.',
ActionType.MESSAGE: "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action.",
ActionType.THINK: "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action.",
ActionType.RECALL: 'You should think about the information you just recalled, and how it should affect your plan.',
ActionType.ADD_TASK: 'You should think about the next action to take.',
ActionType.MODIFY_TASK: 'You should think about the next action to take.',
@@ -123,42 +149,47 @@ def get_hint(latest_action_id: str) -> str:
return hints.get(latest_action_id, '')
def get_prompt(state: State) -> str:
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
"""
Gets the prompt for the planner agent.
Formatted with the most recent action-observation pairs, current task, and hint based on last action
Parameters:
- state (State): The state of the current agent
- plan (Plan): The original plan outlined by the user with LLM defined tasks
- history (List[Tuple[Action, Observation]]): List of corresponding action-observation pairs
Returns:
- str: The formatted string prompt with historical values
"""
plan_str = json.dumps(state.root_task.to_dict(), indent=2)
sub_history = state.history[-HISTORY_SIZE:]
plan_str = json.dumps(plan.task.to_dict(), indent=2)
sub_history = history[-HISTORY_SIZE:]
history_dicts = []
latest_action: Action = NullAction()
for action, observation in sub_history:
if not isinstance(action, NullAction):
history_dicts.append(event_to_memory(action))
history_dicts.append(action.to_memory())
latest_action = action
if not isinstance(observation, NullObservation):
observation_dict = event_to_memory(observation)
observation_dict = observation.to_memory()
if (
'extras' in observation_dict
and 'screenshot' in observation_dict['extras']
):
del observation_dict['extras']['screenshot']
history_dicts.append(observation_dict)
history_str = json.dumps(history_dicts, indent=2)
current_task = state.root_task.get_current_task()
current_task = plan.get_current_task()
if current_task is not None:
plan_status = f"You're currently working on this task:\n{current_task.goal}."
if len(current_task.subtasks) == 0:
plan_status += "\nIf it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW."
else:
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
hint = get_hint(event_to_memory(latest_action).get('action', ''))
logger.info('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
task = state.get_current_user_intent()
hint = get_hint(latest_action.to_dict()['action'])
logger.info('HINT:\n' + hint, extra={'msg_type': 'INFO'})
return prompt % {
'task': task,
'task': plan.main_goal,
'plan': plan_str,
'history': history_str,
'hint': hint,
@@ -176,6 +207,9 @@ def parse_response(response: str) -> Action:
Returns:
- Action: A valid next action to perform from model output
"""
json_start = response.find('{')
json_end = response.rfind('}') + 1
response = response[json_start:json_end]
action_dict = json.loads(response)
if 'contents' in action_dict:
# The LLM gets confused here. Might as well be robust
+2
View File
@@ -8,4 +8,6 @@ by the `ghcr.yml` workflow.
```
docker build -f containers/app/Dockerfile -t opendevin .
docker build -f containers/sandbox/Dockerfile -t sandbox .
docker build -f containers/evaluation/Dockerfile -t evaluation evaluation/SWE-bench/
```
+25 -25
View File
@@ -5,7 +5,7 @@ WORKDIR /app
COPY ./frontend/package.json frontend/package-lock.json ./
RUN npm install -g npm@10.5.1
RUN npm ci
RUN npm install
COPY ./frontend ./
RUN npm run make-i18n && npm run build
@@ -33,8 +33,7 @@ FROM python:3.12-slim as runtime
WORKDIR /app
ENV RUN_AS_DEVIN=true
# A random number--we need this to be different from the user's UID on the host machine
ENV OPENDEVIN_USER_ID=42420
ENV SANDBOX_USER_ID=1000
ENV USE_HOST_NETWORK=false
ENV SSH_HOSTNAME=host.docker.internal
ENV WORKSPACE_BASE=/opt/workspace_base
@@ -44,39 +43,40 @@ RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \
&& apt-get install -y curl ssh sudo
RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs # Default is 1000, but OSX is often 501
RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs # Default is 60000, but we've seen up to 200000
RUN groupadd app
RUN useradd -l -m -u $OPENDEVIN_USER_ID -s /bin/bash opendevin && \
usermod -aG app opendevin && \
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:app /app && chmod -R 770 /app
RUN sudo chown -R opendevin:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
RUN chown -R opendevin:opendevin /app
USER opendevin
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH" \
PYTHONPATH='/app'
COPY --chown=opendevin:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
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
RUN python opendevin/download.py # No-op to download assets
RUN playwright install --with-deps chromium
COPY --chown=opendevin:app --chmod=770 ./opendevin ./opendevin
COPY --chown=opendevin:app --chmod=777 ./opendevin/runtime/plugins ./opendevin/runtime/plugins
COPY --chown=opendevin:app --chmod=770 ./agenthub ./agenthub
RUN python opendevin/core/download.py # No-op to download assets
RUN chown -R opendevin:app /app/logs && chmod -R 770 /app/logs # This gets created by the download.py script
COPY --chown=opendevin:app --chmod=770 --from=frontend-builder /app/dist ./frontend/dist
COPY --chown=opendevin:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
COPY --from=frontend-builder /app/dist ./frontend/dist
USER root
RUN chown -R opendevin:opendevin /app
# make group permissions the same as user permissions
RUN chmod -R g=u /app
USER opendevin
WORKDIR /app
# change ownership of the app directory to the sandbox user
COPY ./containers/app/entrypoint.sh /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"]
# run the script as root
USER root
RUN chown opendevin:opendevin /app/entrypoint.sh
RUN chmod 777 /app/entrypoint.sh
CMD ["/app/entrypoint.sh"]
+11 -47
View File
@@ -1,15 +1,7 @@
#!/bin/bash
set -eo pipefail
echo "Starting OpenDevin..."
if [[ $NO_SETUP == "true" ]]; then
echo "Skipping setup, running as $(whoami)"
"$@"
exit 0
fi
# check user is root
if [ "$(id -u)" -ne 0 ]; then
echo "The OpenDevin entrypoint.sh must run as root"
echo "Please run as root"
exit 1
fi
@@ -18,42 +10,14 @@ if [ -z "$SANDBOX_USER_ID" ]; then
exit 1
fi
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
echo "Running OpenDevin as root"
export RUN_AS_DEVIN=false
mkdir -p /root/.cache/ms-playwright/
mv /home/opendevin/.cache/ms-playwright/ /root/.cache/
"$@"
else
echo "Setting up enduser with id $SANDBOX_USER_ID"
if id "enduser" &>/dev/null; then
echo "User enduser already exists. Skipping creation."
else
if ! useradd -l -m -u $SANDBOX_USER_ID -s /bin/bash enduser; then
echo "Failed to create user enduser with id $SANDBOX_USER_ID. Moving opendevin user."
incremented_id=$(($SANDBOX_USER_ID + 1))
usermod -u $incremented_id opendevin
if ! useradd -l -m -u $SANDBOX_USER_ID -s /bin/bash enduser; then
echo "Failed to create user enduser with id $SANDBOX_USER_ID for a second time. Exiting."
exit 1
fi
fi
fi
usermod -aG app enduser
# 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"
if getent group $DOCKER_SOCKET_GID; then
echo "Group with id $DOCKER_SOCKET_GID already exists"
else
echo "Creating group with id $DOCKER_SOCKET_GID"
groupadd -g $DOCKER_SOCKET_GID docker
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
mkdir -p /home/enduser/.cache/ms-playwright/
mv /home/opendevin/.cache/ms-playwright/ /home/enduser/.cache/
# 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
usermod -aG $DOCKER_SOCKET_GID enduser
echo "Running as enduser"
su enduser /bin/bash -c "$*"
fi
# 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"
+1 -1
View File
@@ -18,7 +18,7 @@ cache_tag="$cache_tag_base"
if [[ -n $GITHUB_REF_NAME ]]; then
# check if ref name is a version number
if [[ $GITHUB_REF_NAME =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ $GITHUB_REF_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
major_version=$(echo $GITHUB_REF_NAME | cut -d. -f1)
minor_version=$(echo $GITHUB_REF_NAME | cut -d. -f1,2)
tags+=($major_version $minor_version)
+41
View File
@@ -0,0 +1,41 @@
FROM ubuntu:20.04
# https://github.com/princeton-nlp/SWE-bench/issues/15#issuecomment-1815392192
RUN apt-get update && \
apt-get install -y bash gcc git jq wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN git config --global user.email "swebench@pnlp.org"
RUN git config --global user.name "swebench"
RUN apt update && apt install -y build-essential
# Create new user
RUN useradd -ms /bin/bash swe-bench
USER swe-bench
WORKDIR /home/swe-bench
# Setup Conda
ENV PATH="/home/swe-bench/miniconda3/bin:${PATH}"
ARG PATH="/home/swe-bench/miniconda3/bin:${PATH}"
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-`uname -m`.sh -O miniconda.sh \
&& mkdir ~/.conda \
&& bash miniconda.sh -b \
&& rm -f miniconda.sh
RUN conda --version
# Setup SWE-Bench Env
COPY environment.yml .
RUN conda env create -f environment.yml
# Add commands
COPY ./commands.sh .
RUN . ./commands.sh
# Some missing packages
RUN pip install datasets python-dotenv gitpython
RUN conda init bash
CMD ["/bin/bash"]
+4
View File
@@ -0,0 +1,4 @@
DOCKER_REGISTRY=ghcr.io
DOCKER_ORG=opendevin
DOCKER_IMAGE=eval-swe-bench
DOCKER_BASE_DIR=evaluation/SWE-bench
-4
View File
@@ -21,8 +21,6 @@ RUN apt-get update && apt-get install -y \
jq \
g++ \
make \
iproute2 \
libgl1-mesa-glx \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m0755 /var/run/sshd
@@ -33,5 +31,3 @@ 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
# TODO: those dependencies are needed for agentskills, we should pack them in a new sandbox image
RUN pip3 install python-docx PyPDF2 python-pptx pylatexenc openai opencv-python
+12 -3
View File
@@ -20,16 +20,16 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.1
rev: v0.3.7
hooks:
# Run the linter.
- id: ruff
entry: ruff check --config dev_config/python/ruff.toml
entry: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
types_or: [python, pyi, jupyter]
args: [--fix]
# Run the formatter.
- id: ruff-format
entry: ruff format --config dev_config/python/ruff.toml
entry: ruff check --config dev_config/python/ruff.toml opendevin/ agenthub/
types_or: [python, pyi, jupyter]
- repo: https://github.com/pre-commit/mirrors-mypy
@@ -41,3 +41,12 @@ repos:
entry: mypy --config-file dev_config/python/mypy.ini opendevin/ agenthub/
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: pydoc
name: pydoc
entry: ./dev_config/python/pydoc.sh
language: script
types: [python]
pass_filenames: false
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
pip install pydoc-markdown && rm -rf ./docs/modules/python && pydoc-markdown
+1 -10
View File
@@ -9,22 +9,13 @@ select = [
"F",
"I",
"Q",
"B",
]
ignore = [
"E501",
"B003",
"B007",
"B009",
"B010",
"B904",
"B018",
]
[lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "single"
flake8-quotes = {inline-quotes = "single"}
[format]
quote-style = "single"
+37
View File
@@ -81,6 +81,43 @@ const config: Config = {
},
],
},
footer: {
style: "dark",
links: [
{
title: "OpenDevin",
items: [
{
label: "Docs",
to: "/modules/usage/intro",
},
],
},
{
title: "Community",
items: [
{
label: "Slack",
href: "https://join.slack.com/t/opendevin/shared_invite/zt-2etftj1dd-X1fDL2PYIVpsmJZkqEYANw",
},
{
label: "Discord",
href: "https://discord.gg/mBuDGRzzES",
},
],
},
{
title: "More",
items: [
{
label: "GitHub",
href: "https://github.com/OpenDevin/OpenDevin",
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} OpenDevin`,
},
prism: {
theme: prismThemes.oneLight,
darkTheme: prismThemes.oneDark,
@@ -0,0 +1,34 @@
---
sidebar_label: agent
title: agenthub.SWE_agent.agent
---
## SWEAgent Objects
```python
class SWEAgent(Agent)
```
An attempt to recreate swe_agent with output parsing, prompting style, and Application Computer Interface (ACI).
SWE-agent includes ACI functions like &#x27;goto&#x27;, &#x27;search_for&#x27;, &#x27;edit&#x27;, &#x27;scroll&#x27;, &#x27;run&#x27;
#### step
```python
def step(state: State) -> Action
```
SWE-Agent step:
1. Get context - past actions, custom commands, current step
2. Perform think-act - prompt model for action and reasoning
3. Catch errors - ensure model takes action (5 attempts max)
#### reset
```python
def reset() -> None
```
Used to reset the agent
@@ -0,0 +1,34 @@
---
sidebar_label: parser
title: agenthub.SWE_agent.parser
---
#### get\_action\_from\_string
```python
def get_action_from_string(command_string: str,
path: str,
line: int,
thoughts: str = '') -> Action | None
```
Parses the command string to find which command the agent wants to run
Converts the command into a proper Action and returns
#### parse\_command
```python
def parse_command(input_str: str, path: str, line: int)
```
Parses a given string and separates the command (enclosed in triple backticks) from any accompanying text.
**Arguments**:
- `input_str` _str_ - The input string to be parsed.
**Returns**:
- `tuple` - A tuple containing the command and the accompanying text (if any).
@@ -0,0 +1,50 @@
---
sidebar_label: codeact_agent
title: agenthub.codeact_agent.codeact_agent
---
## CodeActAgent Objects
```python
class CodeActAgent(Agent)
```
The Code Act Agent is a minimalist agent.
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
#### \_\_init\_\_
```python
def __init__(llm: LLM) -> None
```
Initializes a new instance of the CodeActAgent class.
**Arguments**:
- llm (LLM): The llm to be used by this agent
#### step
```python
def step(state: State) -> Action
```
Performs one step using the Code Act Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
**Arguments**:
- state (State): used to get updated info and background commands
**Returns**:
- CmdRunAction(command) - command action to run
- AgentEchoAction(content=INVALID_INPUT_MESSAGE) - invalid command output
**Raises**:
- NotImplementedError - for actions other than CmdOutputObservation or AgentMessageObservation
@@ -0,0 +1,45 @@
---
sidebar_label: agent
title: agenthub.delegator_agent.agent
---
## DelegatorAgent Objects
```python
class DelegatorAgent(Agent)
```
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
#### \_\_init\_\_
```python
def __init__(llm: LLM)
```
Initialize the Delegator Agent with an LLM
**Arguments**:
- llm (LLM): The llm to be used by this agent
#### step
```python
def step(state: State) -> Action
```
Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
**Arguments**:
- state (State): The current state given the previous actions and observations
**Returns**:
- AgentFinishAction: If the last state was &#x27;completed&#x27;, &#x27;verified&#x27;, or &#x27;abandoned&#x27;
- Action: The next action to take based on llm response
@@ -0,0 +1,14 @@
---
sidebar_label: agent
title: agenthub.dummy_agent.agent
---
## DummyAgent Objects
```python
class DummyAgent(Agent)
```
The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
without making any LLM calls.
@@ -0,0 +1,30 @@
---
sidebar_label: agent
title: agenthub.micro.agent
---
#### my\_encoder
```python
def my_encoder(obj)
```
Encodes objects as dictionaries
**Arguments**:
- obj (Object): An object that will be converted
**Returns**:
- dict: If the object can be converted it is returned in dict format
#### to\_json
```python
def to_json(obj, **kwargs)
```
Serialize an object to str format
@@ -0,0 +1,62 @@
---
sidebar_label: agent
title: agenthub.monologue_agent.agent
---
## MonologueAgent Objects
```python
class MonologueAgent(Agent)
```
The Monologue Agent utilizes long and short term memory to complete tasks.
Long term memory is stored as a LongTermMemory object and the model uses it to search for examples from the past.
Short term memory is stored as a Monologue object and the model can condense it as necessary.
#### \_\_init\_\_
```python
def __init__(llm: LLM)
```
Initializes the Monologue Agent with an llm, monologue, and memory.
**Arguments**:
- llm (LLM): The llm to be used by this agent
#### step
```python
def step(state: State) -> Action
```
Modifies the current state by adding the most recent actions and observations, then prompts the model to think about it&#x27;s next action to take using monologue, memory, and hint.
**Arguments**:
- state (State): The current state based on previous steps taken
**Returns**:
- Action: The next action to take based on LLM response
#### search\_memory
```python
def search_memory(query: str) -> List[str]
```
Uses VectorIndexRetriever to find related memories within the long term memory.
Uses search to produce top 10 results.
**Arguments**:
- query (str): The query that we want to find related memories for
**Returns**:
- List[str]: A list of top 10 text results that matched the query
@@ -0,0 +1,38 @@
---
sidebar_label: json
title: agenthub.monologue_agent.utils.json
---
#### my\_encoder
```python
def my_encoder(obj)
```
Encodes objects as dictionaries
**Arguments**:
- obj (Object): An object that will be converted
**Returns**:
- dict: If the object can be converted it is returned in dict format
#### dumps
```python
def dumps(obj, **kwargs)
```
Serialize an object to str format
#### loads
```python
def loads(s, **kwargs)
```
Create a JSON object from str
@@ -0,0 +1,52 @@
---
sidebar_label: memory
title: agenthub.monologue_agent.utils.memory
---
## LongTermMemory Objects
```python
class LongTermMemory()
```
Responsible for storing information that the agent can call on later for better insights and context.
Uses chromadb to store and search through memories.
#### \_\_init\_\_
```python
def __init__()
```
Initialize the chromadb and set up ChromaVectorStore for later use.
#### add\_event
```python
def add_event(event: dict)
```
Adds a new event to the long term memory with a unique id.
**Arguments**:
- event (dict): The new event to be added to memory
#### search
```python
def search(query: str, k: int = 10)
```
Searches through the current memory using VectorIndexRetriever
**Arguments**:
- query (str): A query to match search results to
- k (int): Number of top results to return
**Returns**:
- List[str]: List of top k results found in current memory
@@ -0,0 +1,80 @@
---
sidebar_label: monologue
title: agenthub.monologue_agent.utils.monologue
---
## Monologue Objects
```python
class Monologue()
```
The monologue is a representation for the agent&#x27;s internal monologue where it can think.
The agent has the capability of using this monologue for whatever it wants.
#### \_\_init\_\_
```python
def __init__()
```
Initialize the empty list of thoughts
#### add\_event
```python
def add_event(t: dict)
```
Adds an event to memory if it is a valid event.
**Arguments**:
- t (dict): The thought that we want to add to memory
**Raises**:
- AgentEventTypeError: If t is not a dict
#### get\_thoughts
```python
def get_thoughts()
```
Get the current thoughts of the agent.
**Returns**:
- List: The list of thoughts that the agent has.
#### get\_total\_length
```python
def get_total_length()
```
Gives the total number of characters in all thoughts
**Returns**:
- Int: Total number of chars in thoughts.
#### condense
```python
def condense(llm: LLM)
```
Attempts to condense the monologue by using the llm
**Arguments**:
- llm (LLM): llm to be used for summarization
**Raises**:
- Exception: the same exception as it got from the llm or processing the response
@@ -0,0 +1,73 @@
---
sidebar_label: prompts
title: agenthub.monologue_agent.utils.prompts
---
#### get\_summarize\_monologue\_prompt
```python
def get_summarize_monologue_prompt(thoughts: List[dict])
```
Gets the prompt for summarizing the monologue
**Returns**:
- str: A formatted string with the current monologue within the prompt
#### get\_request\_action\_prompt
```python
def get_request_action_prompt(
task: str,
thoughts: List[dict],
background_commands_obs: List[CmdOutputObservation] = [])
```
Gets the action prompt formatted with appropriate values.
**Arguments**:
- task (str): The current task the agent is trying to accomplish
- thoughts (List[dict]): The agent&#x27;s current thoughts
- background_commands_obs (List[CmdOutputObservation]): List of all observed background commands running
**Returns**:
- str: Formatted prompt string with hint, task, monologue, and background included
#### parse\_action\_response
```python
def parse_action_response(response: str) -> Action
```
Parses a string to find an action within it
**Arguments**:
- response (str): The string to be parsed
**Returns**:
- Action: The action that was found in the response string
#### parse\_summary\_response
```python
def parse_summary_response(response: str) -> List[dict]
```
Parses a summary of the monologue
**Arguments**:
- response (str): The response string to be parsed
**Returns**:
- List[dict]: The list of summaries output by the model
@@ -0,0 +1,45 @@
---
sidebar_label: agent
title: agenthub.planner_agent.agent
---
## PlannerAgent Objects
```python
class PlannerAgent(Agent)
```
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
#### \_\_init\_\_
```python
def __init__(llm: LLM)
```
Initialize the Planner Agent with an LLM
**Arguments**:
- llm (LLM): The llm to be used by this agent
#### step
```python
def step(state: State) -> Action
```
Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
**Arguments**:
- state (State): The current state given the previous actions and observations
**Returns**:
- AgentFinishAction: If the last state was &#x27;completed&#x27;, &#x27;verified&#x27;, or &#x27;abandoned&#x27;
- Action: The next action to take based on llm response
@@ -0,0 +1,49 @@
---
sidebar_label: prompt
title: agenthub.planner_agent.prompt
---
#### get\_hint
```python
def get_hint(latest_action_id: str) -> str
```
Returns action type hint based on given action_id
#### get\_prompt
```python
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str
```
Gets the prompt for the planner agent.
Formatted with the most recent action-observation pairs, current task, and hint based on last action
**Arguments**:
- plan (Plan): The original plan outlined by the user with LLM defined tasks
- history (List[Tuple[Action, Observation]]): List of corresponding action-observation pairs
**Returns**:
- str: The formatted string prompt with historical values
#### parse\_response
```python
def parse_response(response: str) -> Action
```
Parses the model output to find a valid action to take
**Arguments**:
- response (str): A response from the model that potentially contains an Action.
**Returns**:
- Action: A valid next action to perform from model output
@@ -0,0 +1,9 @@
---
sidebar_label: action
title: opendevin.action
---
#### ACTION\_TYPE\_TO\_CLASS
type: ignore[attr-defined]
@@ -0,0 +1,14 @@
---
sidebar_label: base
title: opendevin.action.base
---
## NullAction Objects
```python
@dataclass
class NullAction(NotExecutableAction)
```
An action that does nothing.
@@ -0,0 +1,16 @@
---
sidebar_label: fileop
title: opendevin.action.fileop
---
## FileReadAction Objects
```python
@dataclass
class FileReadAction(ExecutableAction)
```
Reads a file from a given path.
Can be set to read specific lines using start and end
Default lines 0:-1 (whole file)

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