Compare commits

..

1 Commits

Author SHA1 Message Date
Xingyao Wang
cd18ab215f fix ssh_box error parsing 2024-05-16 23:15:26 +08:00
701 changed files with 17001 additions and 58464 deletions

View File

@@ -1,19 +1,14 @@
codecov:
notify:
wait_for_ci: true
# our project is large, so 6 builds are typically uploaded. this waits till 5/6
# See https://docs.codecov.com/docs/notifications#section-preventing-notifications-until-after-n-builds
after_n_builds: 5
coverage:
status:
patch:
default:
threshold: 100% # allow patch coverage to be lower than project coverage by any amount
threshold: 10% # allow patch coverage to be lower than project coverage by at most 10%
project:
default:
threshold: 5% # allow project coverage to drop at most 5%
comment: false
github_checks:
annotations: false

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: If you want to share the chat history you can click the thumbs-down (👎) button above the input field and you will get a shareable link (you can also click thumbs up when things are going well of course!). LLM logs will be stored in the `logs/llm/default` folder. Please add any additional context about the problem here.
description: LLM logs will be stored in the `logs/llm/default` folder. Please add any additional context about the problem here.

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Question
about: Use this template to ask a question regarding the project.
title: ''
labels: question
assignees: ''
---
## Describe your question
<!--A clear and concise description of what you want to know.-->
## Additional context
<!--Add any other context about the question here, like what you've tried so far.-->

View File

@@ -1,22 +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"
open-pull-requests-limit: 20
- package-ecosystem: "npm" # See documentation for possible values
directory: "/frontend" # Location of package manifests
schedule:
interval: "daily"
open-pull-requests-limit: 20
- package-ecosystem: "npm" # See documentation for possible values
directory: "/docs" # Location of package manifests
schedule:
interval: "daily"
open-pull-requests-limit: 20

View File

@@ -1,5 +0,0 @@
**What is the problem that this fixes or functionality that this introduces? Does it fix any open issues?**
**Give a brief summary of what the PR does, explaining any non-trivial design decisions**
**Other references**

View File

@@ -1,4 +1,4 @@
name: Run E2E test with dummy agent
name: Run e2e test with dummy agent
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -10,9 +10,6 @@ on:
- main
pull_request:
env:
PERSIST_SANDBOX : "false"
jobs:
test:
runs-on: ubuntu-latest
@@ -30,13 +27,4 @@ jobs:
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: |
set -e
poetry run python opendevin/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
- name: Check exit code
run: |
if [ $? -ne 0 ]; then
echo "Test failed"
exit 1
else
echo "Test passed"
fi
poetry run python opendevin/core/main.py -t "do a flip" -m ollama/not-a-model -d ./workspace/ -c DummyAgent

View File

@@ -1,4 +1,4 @@
name: Build Publish and Test Docker Image
name: Publish Docker Image
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -7,7 +7,7 @@ concurrency:
on:
push:
branches:
- main
- main
tags:
- '*'
pull_request:
@@ -19,23 +19,19 @@ on:
default: ''
jobs:
ghcr_build:
ghcr_build_and_push:
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.capture-tags.outputs.tags }}
permissions:
contents: read
packages: write
strategy:
matrix:
image: ["sandbox", "opendevin"]
platform: ["amd64", "arm64"]
image: ["app", "sandbox"]
steps:
- name: Checkout
- name: checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
@@ -44,6 +40,7 @@ jobs:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
@@ -60,204 +57,26 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v3
- name: Build and export image
id: build
run: ./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} ${{ matrix.platform }}
- name: Capture tags
id: capture-tags
run: |
tags=$(cat tags.txt)
echo "tags=$tags"
echo "tags=$tags" >> $GITHUB_OUTPUT
- name: Upload Docker image as artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
path: /tmp/${{ matrix.image }}_image_${{ matrix.platform }}.tar
test-for-sandbox:
name: Test for Sandbox
runs-on: ubuntu-latest
needs: ghcr_build
env:
PERSIST_SANDBOX: "false"
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: make install-python-dependencies
- name: Download sandbox Docker image
uses: actions/download-artifact@v4
with:
name: sandbox-docker-image-amd64
path: /tmp/
- name: Load sandbox image and run sandbox tests
run: |
# Load the Docker image and capture the output
output=$(docker load -i /tmp/sandbox_image_amd64.tar)
# Extract the first image name from the output
image_name=$(echo "$output" | grep -oP 'Loaded image: \K.*' | head -n 1)
# Print the full name of the image
echo "Loaded Docker image: $image_name"
SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true 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 }}
integration-tests-on-linux:
name: Integration Tests on Linux
runs-on: ubuntu-latest
needs: ghcr_build
env:
PERSIST_SANDBOX: "false"
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
sandbox: ["ssh", "local"]
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: make install-python-dependencies
- name: Download sandbox Docker image
uses: actions/download-artifact@v4
with:
name: sandbox-docker-image-amd64
path: /tmp/
- name: Load sandbox image and run integration tests
env:
SANDBOX_BOX_TYPE: ${{ matrix.sandbox }}
run: |
# Load the Docker image and capture the output
output=$(docker load -i /tmp/sandbox_image_amd64.tar)
# Extract the first image name from the output
image_name=$(echo "$output" | grep -oP 'Loaded image: \K.*' | head -n 1)
# Print the full name of the image
echo "Loaded Docker image: $image_name"
SANDBOX_CONTAINER_IMAGE=$image_name 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 }}
ghcr_push:
runs-on: ubuntu-latest
# don't push if integration tests or sandbox tests fail
needs: [ghcr_build, integration-tests-on-linux, test-for-sandbox]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
env:
tags: ${{ needs.ghcr_build.outputs.tags }}
permissions:
contents: read
packages: write
strategy:
matrix:
image: ["sandbox", "opendevin"]
platform: ["amd64", "arm64"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v2
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download Docker images
uses: actions/download-artifact@v4
with:
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
path: /tmp/${{ matrix.platform }}
- name: Load images and push to registry
- name: Build and push ${{ matrix.image }}
if: "!github.event.pull_request.head.repo.fork"
run: |
mv /tmp/${{ matrix.platform }}/${{ matrix.image }}_image_${{ matrix.platform }}.tar .
loaded_image=$(docker load -i ${{ matrix.image }}_image_${{ matrix.platform }}.tar | grep "Loaded image:" | head -n 1 | awk '{print $3}')
echo "loaded image = $loaded_image"
tags=$(echo ${tags} | tr ' ' '\n')
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
echo "image name = $image_name"
for tag in $tags; do
echo "tag = $tag"
docker tag $loaded_image $image_name:${tag}_${{ matrix.platform }}
docker push $image_name:${tag}_${{ matrix.platform }}
done
./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} --push
create_manifest:
- name: Build ${{ matrix.image }}
if: "github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }}
docker_build_success:
name: Docker Build Success
runs-on: ubuntu-latest
needs: [ghcr_build, ghcr_push]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
env:
tags: ${{ needs.ghcr_build.outputs.tags }}
strategy:
matrix:
image: ["sandbox", "opendevin"]
permissions:
contents: read
packages: write
needs: ghcr_build_and_push
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push multi-platform manifest
run: |
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
echo "image name = $image_name"
tags=$(echo ${tags} | tr ' ' '\n')
for tag in $tags; do
echo 'tag = $tag'
docker buildx imagetools create --tag $image_name:$tag \
$image_name:${tag}_amd64 \
$image_name:${tag}_arm64
done
- run: echo Done!

View File

@@ -47,4 +47,11 @@ jobs:
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Run pre-commit hooks
run: pre-commit run --files opendevin/**/* agenthub/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
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' ' ') \
--show-diff-on-failure \
--config ./dev_config/python/.pre-commit-config.yaml

View File

@@ -44,24 +44,12 @@ jobs:
echo "" >> task.txt
echo "Diff file is: ${{ github.event.pull_request.number }}.diff" >> task.txt
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
export PATH="/github/home/.local/bin:$PATH"
poetry install --without evaluation
poetry run playwright install --with-deps chromium
- name: Run OpenDevin
env:
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SANDBOX_BOX_TYPE: ssh
SANDBOX_TYPE: exec
run: |
# Append path to launch poetry
export PATH="/github/home/.local/bin:$PATH"
# Append path to correctly import package, note: must set pwd at first
export PYTHONPATH=$(pwd):$PYTHONPATH
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE poetry run python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
rm task.txt
- name: Check if review file is non-empty

View File

@@ -0,0 +1,76 @@
name: Run Integration Tests
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
on:
push:
branches:
- main
pull_request:
jobs:
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
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
- 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 Tests
env:
SANDBOX_TYPE: ${{ matrix.sandbox }}
AGENT: ${{ matrix.agent }}
MAX_ITERATIONS: 10
LLM_EMBEDDING_MODEL: ${{ matrix.embedding-model }}
run: |
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 }}
test_matrix_success:
name: All Integration Tests Passed
runs-on: ubuntu-latest
needs: [integration-tests]
steps:
- run: echo Done!

View File

@@ -7,52 +7,15 @@ concurrency:
on:
push:
branches:
- main
paths-ignore:
- '**/*.md'
- 'frontend/**'
- 'docs/**'
- 'evaluation/**'
- main
pull_request:
env:
PERSIST_SANDBOX : "false"
jobs:
fe-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
working-directory: ./frontend
run: npm ci
- name: Run tests and collect coverage
working-directory: ./frontend
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-on-macos:
name: Test on macOS
runs-on: macos-12
runs-on: macos-13
env:
INSTALL_DOCKER: "1" # Set to '0' to skip Docker installation
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ["3.11"]
@@ -67,7 +30,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
@@ -75,18 +38,8 @@ jobs:
- name: Install & Start Docker
if: env.INSTALL_DOCKER == '1'
run: |
# Uninstall colima to upgrade to the latest version
if brew list colima &>/dev/null; then
brew uninstall colima
# unlinking colima dependency: go
brew uninstall go@1.21
fi
rm -rf ~/.colima ~/.lima
brew install --HEAD colima
brew services start colima
brew install docker
colima delete
colima start --network-address --arch x86_64 --cpu=1 --memory=1
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
@@ -96,7 +49,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 -k "not test_sandbox"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
@@ -106,7 +59,7 @@ jobs:
name: Test on Linux
runs-on: ubuntu-latest
env:
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ["3.11"]
@@ -121,7 +74,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
@@ -130,9 +83,45 @@ 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 -k "not test_sandbox"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-for-sandbox:
name: Test for Sandbox
runs-on: ubuntu-latest
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 }}
test_matrix_success:
name: All Mac/Linux Tests Passed
runs-on: ubuntu-latest
needs: [test-on-macos, test-on-linux, test-for-sandbox]
steps:
- run: echo Done!

View File

@@ -36,27 +36,14 @@ jobs:
echo "BODY:" >> task.txt
echo "${ISSUE_BODY}" >> task.txt
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
export PATH="/github/home/.local/bin:$PATH"
poetry install --without evaluation
poetry run playwright install --with-deps chromium
- name: Run OpenDevin
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SANDBOX_BOX_TYPE: ssh
SANDBOX_TYPE: exec
run: |
# Append path to launch poetry
export PATH="/github/home/.local/bin:$PATH"
# Append path to correctly import package, note: must set pwd at first
export PYTHONPATH=$(pwd):$PYTHONPATH
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE poetry run python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
rm task.txt
- name: Setup Git, Create Branch, and Commit Changes

View File

@@ -1,48 +0,0 @@
name: Update pyproject.toml Version and Tags
on:
release:
types:
- published
jobs:
update-pyproject-and-tags:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install toml
- name: Get release tag
id: get_release_tag
run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Update pyproject.toml with release tag
run: |
python -c "
import toml
with open('pyproject.toml', 'r') as f:
data = toml.load(f)
data['tool']['poetry']['version'] = '${{ env.RELEASE_TAG }}'
with open('pyproject.toml', 'w') as f:
toml.dump(data, f)
"
- name: Commit and push pyproject.toml changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "Update pyproject.toml version to ${{ env.RELEASE_TAG }}"
branch: main
file_pattern: pyproject.toml

20
.gitignore vendored
View File

@@ -126,7 +126,6 @@ env/
venv/
ENV/
env.bak/
.env.bak
venv.bak/
*venv/
@@ -161,14 +160,9 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.vscode/
.cursorignore
# evaluation
evaluation/evaluation_outputs
evaluation/outputs
evaluation/swe_bench/eval_workspace*
evaluation/SWE-bench/data
evaluation/webarena/scripts/webarena_env.sh
# frontend
@@ -181,8 +175,6 @@ frontend/yarn.lock
# testing
frontend/coverage
test_results*
/_test_files_tmp/
# production
frontend/build
@@ -210,10 +202,8 @@ cache
# configuration
config.toml
config.toml.bak
containers/agnostic_sandbox
# swe-bench-eval
image_build_logs
run_instance_logs
evaluation/swe_bench/eval_workspace
evaluation/outputs
evaluation/evaluation_outputs
test_results*
/_test_files_tmp/

View File

@@ -61,7 +61,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@all-hands.dev
contact@rbren.io
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@@ -1,32 +1,13 @@
# Contributing
Thanks for your interest in contributing to OpenDevin! We welcome and appreciate contributions.
## How Can I Contribute?
There are many ways that you can contribute:
1. **Download and use** OpenDevin, and send [issues](https://github.com/OpenDevin/OpenDevin/issues) when you encounter something that isn't working or a feature that you'd like to see.
2. **Send feedback** after each session by [clicking the thumbs-up thumbs-down buttons](https://opendevin.github.io/OpenDevin/modules/usage/feedback), so we can see where things are working and failing, and also build an open dataset for training code agents.
3. **Improve the Codebase** by sending PRs (see details below). In particular, we have some [good first issue](https://github.com/OpenDevin/OpenDevin/labels/good%20first%20issue) issues that may be ones to start on.
## Understanding OpenDevin's CodeBase
To understand the codebase, please refer to the README in each module:
- [frontend](./frontend/README.md)
- [agenthub](./agenthub/README.md)
- [evaluation](./evaluation/README.md)
- [opendevin](./opendevin/README.md)
- [server](./opendevin/server/README.md)
When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites.
At the moment, we have two kinds of tests: `unit` and `integration`. Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
## Sending Pull Requests to OpenDevin
Thanks for your interest in contributing to OpenDevin! We welcome and appreciate contributions.
To report bugs, create a [GitHub issue](https://github.com/OpenDevin/OpenDevin/issues/new/choose).
## Contribution Guide
### 1. Fork the Official Repository
Fork the [OpenDevin repository](https://github.com/OpenDevin/OpenDevin) into your own account.
Clone your own forked repository into your local environment:
Fork [OpenDevin repository](https://github.com/OpenDevin/OpenDevin) into your own account.
Clone your own forked repository into your local environment.
```shell
git clone git@github.com:<YOUR-USERNAME>/OpenDevin.git
@@ -35,23 +16,21 @@ git clone git@github.com:<YOUR-USERNAME>/OpenDevin.git
### 2. Configure Git
Set the official repository as your [upstream](https://www.atlassian.com/git/tutorials/git-forks-and-upstreams) to synchronize with the latest update in the official repository.
Add the original repository as upstream:
Add the original repository as upstream
```shell
cd OpenDevin
git remote add upstream git@github.com:OpenDevin/OpenDevin.git
```
Verify that the remote is set:
Verify that the remote is set.
```shell
git remote -v
```
You should see both `origin` and `upstream` in the output.
### 3. Synchronize with Official Repository
Synchronize latest commit with official repository before coding:
Synchronize latest commit with official repository before coding.
```shell
git fetch upstream
@@ -60,48 +39,25 @@ git merge upstream/main
git push origin main
```
### 4. Set up the Development Environment
### 4. Create a New Branch And Open a Pull Request
After you finish implementation, open forked repository. The source branch is your new branch, and the target branch is `OpenDevin/OpenDevin` `main` branch. Then PR should appears in [OpenDevin PRs](https://github.com/OpenDevin/OpenDevin/pulls).
We have a separate doc [Development.md](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) that tells you how to set up a development workflow.
### 5. Write Code and Commit It
Once you have done this, you can write code, test it, and commit it to a branch (replace `my_branch` with an appropriate name):
```shell
git checkout -b my_branch
git add .
git commit
git push origin my_branch
```
### 6. Open a Pull Request
* On GitHub, go to the page of your forked repository, and create a Pull Request:
- Click on `Branches`
- Click on the `...` beside your branch and click on `New pull request`
- Set `base repository` to `OpenDevin/OpenDevin`
- Set `base` to `main`
- Click `Create pull request`
The PR should appear in [OpenDevin PRs](https://github.com/OpenDevin/OpenDevin/pulls).
Then the OpenDevin team will review your code.
Then OpenDevin team will review your code.
## PR Rules
### 1. Pull Request title
As described [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes:
As described in [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes:
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation only changes
- `style`: Changes that do not affect the meaning of the code (white space, formatting, missing semicolons, etc.)
- `doc`: Documentation only changes
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `style`: A refactoring that improves code style
- `perf`: A code change that improves performance
- `test`: Adding missing tests or correcting existing tests
- `build`: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- `ci`: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- `ci`: Changes to CI configuration files and scripts (example scopes: `.github`, `ci` (Buildkite))
- `chore`: Other changes that don't modify src or test files
- `revert`: Reverts a previous commit
@@ -111,7 +67,24 @@ For example, a PR title could be:
You may also check out previous PRs in the [PR list](https://github.com/OpenDevin/OpenDevin/pulls).
### 2. Pull Request description
- If your PR is small (such as a typo fix), you can go brief.
- If it contains a lot of changes, it's better to write more details.
As described in [here](https://github.com/OpenDevin/OpenDevin/labels), we create several labels. Every PR should be tagged with the corresponding labels.
### 2. Pull Request description
- If your PR is small (such as a typo fix), you can go brief.
- If it is large and you have changed a lot, it's better to write more details.
## How to begin
Please refer to the README in each module:
- [frontend](./frontend/README.md)
- [agenthub](./agenthub/README.md)
- [evaluation](./evaluation/README.md)
- [opendevin](./opendevin/README.md)
- [server](./opendevin/server/README.md)
- [mock server](./opendevin/mock/README.md)
## Tests
Please navigate to `tests` folder to see existing test suites.
At the moment, we have two kinds of tests: `unit` and `integration`. Please refer to the README for each test suite. These tests also run on CI to ensure quality of
the project.

View File

@@ -1,16 +1,14 @@
# Development Guide
This guide is for people working on OpenDevin and editing the source code.
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/OpenDevin/OpenDevin/blob/main/CONTRIBUTING.md) on how to clone and setup the project initially before moving on.
Otherwise, you can clone the OpenDevin project directly.
## Start the server for development
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [ Ubuntu <= 22.04]
* [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
* 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
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
* netcat => sudo apt-get install netcat
Make sure you have all these dependencies installed before moving on to `make build`.
@@ -29,40 +27,41 @@ mamba install conda-forge::poetry
```
### 2. Build and Setup The Environment
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run on your system:
```bash
make build
```
- **Build the Project:** Begin by building the project, which includes setting up the environment and installing dependencies. This step ensures that OpenDevin is ready to run smoothly on your system.
```bash
make build
```
### 3. Configuring the Language Model
OpenDevin supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest.
To configure the LM of your choice, run:
To configure the LM of your choice, follow these steps:
1. **Using the Makefile: The Effortless Approach**
With a single command, you can have a smooth LM setup for your OpenDevin experience. Simply run:
```bash
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.
Note: If you have previously run OpenDevin using the docker command, you may have already set some environmental variables in your terminal. The final configurations are set from highest to lowest priority:
Environment variables > config.toml variables > default variables
**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 by creating instructions and adding it [to our documentation](https://github.com/OpenDevin/OpenDevin/tree/main/docs/modules/usage/llms).
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).
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers).
### 4. Running the application
#### Option A: Run the Full Application
Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin:
```bash
make run
```
There is also [documentation for running with local models using ollama](./docs/documentation/LOCAL_LLM_GUIDE.md).
### 4. Run the Application
- **Run the Application:** Once the setup is complete, launching OpenDevin is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenDevin without any hassle.
```bash
make run
```
### 5. Individual Server Startup
#### Option B: Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
```bash
make start-backend
@@ -74,16 +73,18 @@ make run
```
### 6. LLM Debugging
If you encounter any issues with the Language Model (LM) or you're simply curious, you can inspect the actual LLM prompts and responses. To do so, export DEBUG=1 in the environment and restart the backend.
OpenDevin will then log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
If you encounter any issues with the Language Model (LM) or you're simply curious, you can inspect the actual LLM prompts and responses. To do so, export DEBUG=1 in the environment and restart the backend. OpenDevin will then log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
### 7. Help
Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
```bash
make help
```
- **Get Some Help:** Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenDevin.
```bash
make help
```
### 8. Testing
#### Unit tests
```bash
@@ -91,8 +92,5 @@ 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 `poetry add xxx`
2. Update the poetry.lock file via `poetry lock --no-update`
Please refer to [this README](./tests/integration/README.md) for details.

View File

@@ -2,15 +2,14 @@ SHELL=/bin/bash
# Makefile for OpenDevin project
# Variables
DOCKER_IMAGE = ghcr.io/opendevin/sandbox:main
DOCKER_IMAGE = ghcr.io/opendevin/sandbox
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"
CONFIG_FILE = config.toml
PRE_COMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
PYTHON_VERSION = 3.11
PRECOMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
# ANSI color codes
GREEN=$(shell tput -Txterm setaf 2)
@@ -28,7 +27,7 @@ ifeq ($(INSTALL_DOCKER),)
endif
@$(MAKE) -s install-python-dependencies
@$(MAKE) -s install-frontend-dependencies
@$(MAKE) -s install-pre-commit-hooks
@$(MAKE) -s install-precommit-hooks
@$(MAKE) -s build-frontend
@echo "$(GREEN)Build completed successfully.$(RESET)"
@@ -63,10 +62,10 @@ check-system:
check-python:
@echo "$(YELLOW)Checking Python installation...$(RESET)"
@if command -v python$(PYTHON_VERSION) > /dev/null; then \
echo "$(BLUE)$(shell python$(PYTHON_VERSION) --version) is already installed.$(RESET)"; \
@if command -v python3.11 > /dev/null; then \
echo "$(BLUE)$(shell python3.11 --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Python $(PYTHON_VERSION) is not installed. Please install Python $(PYTHON_VERSION) to continue.$(RESET)"; \
echo "$(RED)Python 3.11 is not installed. Please install Python 3.11 to continue.$(RESET)"; \
exit 1; \
fi
@@ -113,13 +112,13 @@ check-poetry:
echo "$(BLUE)$(shell poetry --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Poetry 1.8 or later is required. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python$(PYTHON_VERSION) -$(RESET)"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python3 -$(RESET)"; \
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
exit 1; \
fi; \
else \
echo "$(RED)Poetry is not installed. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python$(PYTHON_VERSION) -$(RESET)"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python3.11 -$(RESET)"; \
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
exit 1; \
fi
@@ -131,13 +130,8 @@ pull-docker-image:
install-python-dependencies:
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
@if [ -z "${TZ}" ]; then \
echo "Defaulting TZ (timezone) to UTC"; \
export TZ="UTC"; \
fi
poetry env use python$(PYTHON_VERSION)
@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
@@ -147,14 +141,7 @@ install-python-dependencies:
poetry run pip install playwright; \
poetry run playwright install chromium; \
else \
if [ ! -f cache/playwright_chromium_is_installed.txt ]; then \
echo "Running playwright install --with-deps chromium..."; \
poetry run playwright install --with-deps chromium; \
mkdir -p cache; \
touch cache/playwright_chromium_is_installed.txt; \
else \
echo "Setup already done. Skipping playwright installation."; \
fi \
poetry run playwright install --with-deps chromium; \
fi
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
@@ -169,15 +156,15 @@ install-frontend-dependencies:
npm run make-i18n
@echo "$(GREEN)Frontend dependencies installed successfully.$(RESET)"
install-pre-commit-hooks:
install-precommit-hooks:
@echo "$(YELLOW)Installing pre-commit hooks...$(RESET)"
@git config --unset-all core.hooksPath || true
@poetry run pre-commit install --config $(PRE_COMMIT_CONFIG_PATH)
@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 opendevin/**/* agenthub/**/* evaluation/**/* --show-diff-on-failure --config $(PRE_COMMIT_CONFIG_PATH)
@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)"
@@ -208,10 +195,11 @@ start-frontend:
@echo "$(YELLOW)Starting frontend...$(RESET)"
@cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start
# Common setup for running the app (non-callable)
_run_setup:
# Run the app
run:
@echo "$(YELLOW)Running the app...$(RESET)"
@if [ "$(OS)" = "Windows_NT" ]; then \
echo "$(RED) Windows is not supported, use WSL instead!$(RESET)"; \
echo "$(RED)`make run` is not supported on Windows. Please run `make start-frontend` and `make start-backend` separately.$(RESET)"; \
exit 1; \
fi
@mkdir -p logs
@@ -220,21 +208,9 @@ _run_setup:
@echo "$(YELLOW)Waiting for the backend to start...$(RESET)"
@until nc -z localhost $(BACKEND_PORT); do sleep 0.1; done
@echo "$(GREEN)Backend started successfully.$(RESET)"
# Run the app (standard mode)
run:
@echo "$(YELLOW)Running the app...$(RESET)"
@$(MAKE) -s _run_setup
@cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run start -- --port $(FRONTEND_PORT)
@echo "$(GREEN)Application started successfully.$(RESET)"
# Run the app (WSL mode)
run-wsl:
@echo "$(YELLOW)Running the app in WSL mode...$(RESET)"
@$(MAKE) -s _run_setup
@cd frontend && echo "$(BLUE)Starting frontend with npm (WSL mode)...$(RESET)" && npm run dev_wsl -- --port $(FRONTEND_PORT)
@echo "$(GREEN)Application started successfully in WSL mode.$(RESET)"
# Setup config.toml
setup-config:
@echo "$(YELLOW)Setting up config.toml...$(RESET)"
@@ -245,20 +221,10 @@ setup-config:
setup-config-prompts:
@echo "[core]" > $(CONFIG_FILE).tmp
@read -p "Enter your workspace directory (as absolute path) [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
@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: false]: " persist_sandbox; \
persist_sandbox=$${persist_sandbox:-false}; \
if [ "$$persist_sandbox" = "true" ]; then \
read -p "Enter a password for the sandbox container: " ssh_password; \
echo "ssh_password=\"$$ssh_password\"" >> $(CONFIG_FILE).tmp; \
echo "persist_sandbox=$$persist_sandbox" >> $(CONFIG_FILE).tmp; \
else \
echo "persist_sandbox=$$persist_sandbox" >> $(CONFIG_FILE).tmp; \
fi
@echo "" >> $(CONFIG_FILE).tmp
@echo "[llm]" >> $(CONFIG_FILE).tmp
@@ -319,4 +285,4 @@ help:
@echo " $(GREEN)help$(RESET) - Display this help message, providing information on available targets."
# Phony targets
.PHONY: build check-dependencies check-python check-npm check-docker check-poetry pull-docker-image install-python-dependencies install-frontend-dependencies install-pre-commit-hooks lint start-backend start-frontend run run-wsl setup-config setup-config-prompts help
.PHONY: build check-dependencies check-python check-npm check-docker check-poetry pull-docker-image install-python-dependencies install-frontend-dependencies install-precommit-hooks lint start-backend start-frontend run setup-config setup-config-prompts help

View File

@@ -26,15 +26,15 @@
<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>
<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>
<br/>
<a href="https://xwang.dev/blog/2024/opendevin-codeact-1.0-swebench/"><img src="https://img.shields.io/badge/SWE--bench%20Lite-21.0%25-green?style=for-the-badge" alt="SWE-bench "></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/modules/usage/intro"><img src="https://img.shields.io/badge/Documentation-OpenDevin-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://huggingface.co/spaces/OpenDevin/evaluation"><img src="https://img.shields.io/badge/Evaluation-Benchmark%20on%20HF%20Space-green?style=for-the-badge" alt="Evaluation Benchmark"></a>
<a href="https://opendevin.github.io/OpenDevin/"><img src="https://img.shields.io/badge/Documenation-OpenDevin-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
</div>
<hr>
@@ -44,18 +44,16 @@ OpenDevin agents collaborate with human developers to write code, fix bugs, and
![App screenshot](./docs/static/img/screenshot.png)
## ⚡ Getting Started
OpenDevin 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 OpenDevin in a docker container, run the following commands in your terminal:
> [!WARNING]
> When you run the following command, files in `./workspace` may be modified or deleted.
## ⚡ Quick Start
You can run OpenDevin with Docker. It works best with the most recent
version of Docker, `26.0.0`.
```bash
WORKSPACE_BASE=$(pwd)/workspace
docker run -it \
#The directory you want OpenDevin to modify. MUST be an absolute path!
export WORKSPACE_BASE=$(pwd)/workspace;
docker run \
-it \
--pull=always \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
@@ -63,31 +61,13 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name opendevin-app-$(date +%Y%m%d%H%M%S) \
ghcr.io/opendevin/opendevin
ghcr.io/opendevin/opendevin:0.5
```
> [!NOTE]
> By default, this command pulls the `latest` tag, which represents the most recent release of OpenDevin. You have other options as well:
> - For a specific release version, use `ghcr.io/opendevin/opendevin:<OpenDevin_version>` (replace <OpenDevin_version> with the desired version number).
> - For the most up-to-date development version, use `ghcr.io/opendevin/opendevin:main`. This version may be **(unstable!)** and is recommended for testing or development purposes only.
>
> Choose the tag that best suits your needs based on stability requirements and desired features.
You'll find OpenDevin running at [http://localhost:3000](http://localhost:3000) with access to `./workspace`. To have OpenDevin operate on your code, place it in `./workspace`.
OpenDevin will only have access to this workspace folder. The rest of your system will not be affected as it runs in a secured docker sandbox.
Upon opening OpenDevin, you must select the appropriate `Model` and enter the `API Key` within the settings that should pop up automatically. These can be set at any time by selecting
the `Settings` button (gear icon) in the UI. If the required `Model` does not exist in the list, you can manually enter it in the text box.
For the development workflow, see [Development.md](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md).
Are you having trouble? Check out our [Troubleshooting Guide](https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting).
## 🚀 Documentation
To learn more about the project, and for tips on using OpenDevin,
**check out our [documentation](https://opendevin.github.io/OpenDevin/modules/usage/intro)**.
**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.
@@ -109,10 +89,15 @@ For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
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-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA) - Here we talk about research, architecture, and future development.
- [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">
</a>
</p>
<p align="center">
<a href="https://star-history.com/#OpenDevin/OpenDevin&Date">
@@ -134,16 +119,3 @@ Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more informati
[issues-url]: https://github.com/OpenDevin/OpenDevin/issues
[license-shield]: https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge
[license-url]: https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE
## 📚 Cite
```
@misc{opendevin2024,
author = {{OpenDevin Team}},
title = {{OpenDevin: An Open Platform for AI Software Developers as Generalist Agents}},
year = {2024},
version = {v1.0},
howpublished = {\url{https://github.com/OpenDevin/OpenDevin}},
note = {Accessed: ENTER THE DATE YOU ACCESSED THE PROJECT}
}
```

View File

@@ -2,15 +2,15 @@
In this folder, there may exist multiple implementations of `Agent` that will be used by the framework.
For example, `agenthub/codeact_agent`, etc.
For example, `agenthub/monologue_agent`, `agenthub/metagpt_agent`, `agenthub/codeact_agent`, etc.
Contributors from different backgrounds and interests can choose to contribute to any (or all!) of these directions.
## Constructing an Agent
The abstraction for an agent can be found [here](../opendevin/controller/agent.py).
The abstraction for an agent can be found [here](../opendevin/agent.py).
Agents are run inside of a loop. At each iteration, `agent.step()` is called with a
[State](../opendevin/controller/state/state.py) input, and the agent must output an [Action](../opendevin/events/action).
[State](../opendevin/state.py) input, and the agent must output an [Action](../opendevin/action).
Every agent also has a `self.llm` which it can use to interact with the LLM configured by the user.
See the [LiteLLM docs for `self.llm.completion`](https://docs.litellm.ai/docs/completion).
@@ -28,33 +28,39 @@ The `state` contains:
Here is a list of available Actions, which can be returned by `agent.step()`:
- [`CmdRunAction`](../opendevin/events/action/commands.py) - Runs a command inside a sandboxed terminal
- [`IPythonRunCellAction`](../opendevin/events/action/commands.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../opendevin/runtime/plugins) as a requirement.
- [`FileReadAction`](../opendevin/events/action/files.py) - Reads the content of a file
- [`FileWriteAction`](../opendevin/events/action/files.py) - Writes new content to a file
- [`BrowseURLAction`](../opendevin/events/action/browse.py) - Gets the content of a URL
- [`AddTaskAction`](../opendevin/events/action/tasks.py) - Adds a subtask to the plan
- [`ModifyTaskAction`](../opendevin/events/action/tasks.py) - Changes the state of a subtask.
- [`AgentFinishAction`](../opendevin/events/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
- [`AgentRejectAction`](../opendevin/events/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
- [`AgentFinishAction`](../opendevin/events/action/agent.py) - Stops the control loop, allowing the user to enter a new task
- [`MessageAction`](../opendevin/events/action/message.py) - Represents a message from an agent or the user
- [`CmdRunAction`](../opendevin/action/bash.py) - Runs a command inside a sandboxed terminal
- [`CmdKillAction`](../opendevin/action/bash.py) - Kills a background command
- [`IPythonRunCellAction`](../opendevin/action/bash.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../opendevin/sandbox/plugins) as a requirement.
- [`FileReadAction`](../opendevin/action/fileop.py) - Reads the content of a file
- [`FileWriteAction`](../opendevin/action/fileop.py) - Writes new content to a file
- [`BrowseURLAction`](../opendevin/action/browse.py) - Gets the content of a URL
- [`AgentRecallAction`](../opendevin/action/agent.py) - Searches memory (e.g. a vector database)
- [`AddTaskAction`](../opendevin/action/tasks.py) - Adds a subtask to the plan
- [`ModifyTaskAction`](../opendevin/action/tasks.py) - Changes the state of a subtask
- [`AgentThinkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history (as well as the chat log)
- [`AgentTalkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history and talk to the user.
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/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).
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/events/observation/commands.py)
- [`BrowserOutputObservation`](../opendevin/events/observation/browse.py)
- [`FileReadObservation`](../opendevin/events/observation/files.py)
- [`FileWriteObservation`](../opendevin/events/observation/files.py)
- [`ErrorObservation`](../opendevin/events/observation/error.py)
- [`SuccessObservation`](../opendevin/events/observation/success.py)
- [`CmdOutputObservation`](../opendevin/observation/run.py)
- [`BrowserOutputObservation`](../opendevin/observation/browse.py)
- [`FileReadObservation`](../opendevin/observation/files.py)
- [`FileWriteObservation`](../opendevin/observation/files.py)
- [`AgentRecallObservation`](../opendevin/observation/recall.py)
- [`ErrorObservation`](../opendevin/observation/error.py)
- [`SuccessObservation`](../opendevin/observation/success.py)
You can use `observation.to_dict()` and `observation_from_dict` to serialize and deserialize observations.
@@ -70,3 +76,14 @@ 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]:
```
`search_memory` should return a list of events that match the query. This will be used
for the `recall` action.
You can optionally just return `[]` for this method, meaning the agent has no long-term memory.

View File

@@ -0,0 +1,405 @@
# GPT-4 Examples:
### Notes:
- The final result is not completed as I only gave it 10 steps, but shows very promising use of the tools.
- This whole run costs: $0.35 using gpt-4-turbo
- Input Tokens: 29,734
- Completion Tokens: 1,636
Interesting things:
1. Using comments as an initial placeholder for the next move and then having that output in memory is a very cool behavior.
2. It makes very small iterative changes due to the prompting.
3. A lot of the logic for the game is present but not put together well
4. There are a lot of odd quirks with the code that it has written.
5. Some odd agent behavior with putting random lines of code in random places?
6. The structure of the feedback loop needs some improvement so the agent can see the changes it has made better in context.
7. Might be beneficial to add in some line numbers so the model knows what lines it is looking at.
```
==============
STEP 0
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=91, prompt_tokens=2047, total_tokens=2138)
==== RAW OUTPUT ====
Thoughts:
To start implementing Conway's Game of Life in Python using the pygame library, I first need to set up a Python script file where I can write the code. I'll create a new Python file named `game_of_life.py`. This file will serve as the base for writing the game logic and using pygame functionalities.
Action:
"""
write game_of_life.py # This is a placeholder for the game of life implementation using pygame.
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='# This is a placeholder for the game of life implementation using pygame.', start=0, end=-1, thoughts="Thoughts:\nTo start implementing Conway's Game of Life in Python using the pygame library, I first need to set up a Python script file where I can write the code. I'll create a new Python file named `game_of_life.py`. This file will serve as the base for writing the game logic and using pygame functionalities.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 0:-1.
==============
STEP 1
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=101, prompt_tokens=2351, total_tokens=2452)
==== RAW OUTPUT ====
Thoughts:
The placeholder for the game of life implementation using pygame has been set up in the file `game_of_life.py`. The next step is to start implementing the basic structure of the game. This includes importing necessary libraries, initializing the pygame module, setting up the display window, and defining the main game loop. I will begin by importing pygame and initializing it, which are the foundational steps for any pygame application.
Action:
"""
edit 0 0 import pygame
pygame.init()
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='import pygame\n\npygame.init()', start=0, end=0, thoughts='Thoughts:\nThe placeholder for the game of life implementation using pygame has been set up in the file `game_of_life.py`. The next step is to start implementing the basic structure of the game. This includes importing necessary libraries, initializing the pygame module, setting up the display window, and defining the main game loop. I will begin by importing pygame and initializing it, which are the foundational steps for any pygame application.\n\nAction:', action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 0:0.
==============
STEP 2
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=123, prompt_tokens=2552, total_tokens=2675)
==== RAW OUTPUT ====
Thoughts:
Previously, I initialized pygame in the `game_of_life.py` file. The next step is to set up the display window for the game. This involves defining the size of the window and setting the title. This is crucial as it provides the visual interface for the game. I will also add a comment to indicate that this is the setup for the display window.
Action:
"""
edit 2 2
# Setup the display window
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption("Conway's Game of Life")
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='# Setup the display window\nscreen_size = (800, 600)\nscreen = pygame.display.set_mode(screen_size)\npygame.display.set_caption("Conway\'s Game of Life")', start=2, end=2, thoughts='Thoughts:\nPreviously, I initialized pygame in the `game_of_life.py` file. The next step is to set up the display window for the game. This involves defining the size of the window and setting the title. This is crucial as it provides the visual interface for the game. I will also add a comment to indicate that this is the setup for the display window.\n\nAction:', action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 2:2.
==============
STEP 3
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=144, prompt_tokens=2777, total_tokens=2921)
==== RAW OUTPUT ====
Thoughts:
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame and setting up the display window. The next step is to define the main game loop where the game's logic will be continuously updated and rendered. This loop will handle events, update the game state, and redraw the screen. I will start by defining the main loop structure and a basic event handling mechanism to allow for quitting the game when the user presses the close button.
Action:
"""
edit 5 5 # Main game loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='# Main game loop\nrunning = True\nwhile running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False', start=5, end=5, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame and setting up the display window. The next step is to define the main game loop where the game's logic will be continuously updated and rendered. This loop will handle events, update the game state, and redraw the screen. I will start by defining the main loop structure and a basic event handling mechanism to allow for quitting the game when the user presses the close button.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 5:5.
==============
STEP 4
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=222, prompt_tokens=3022, total_tokens=3244)
==== RAW OUTPUT ====
Thoughts:
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, and defining the main game loop. The next step is to implement the logic for Conway's Game of Life. This involves creating a grid to represent the game state, initializing it with a random pattern, and defining the rules for cell survival, birth, and death. I will start by defining the grid size and initializing the grid with random states.
Action:
"""
edit 7 7 # Define grid size and initialize grid
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
import random
for y in range(grid_size[0]):
for x in range(grid_size[1]):
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='# Define grid size and initialize grid\ngrid_size = (50, 50) # Grid size as a tuple (rows, columns)\ngrid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros\nimport random\nfor y in range(grid_size[0]):\n for x in range(grid_size[1]):\n grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)', start=7, end=7, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, and defining the main game loop. The next step is to implement the logic for Conway's Game of Life. This involves creating a grid to represent the game state, initializing it with a random pattern, and defining the rules for cell survival, birth, and death. I will start by defining the grid size and initializing the grid with random states.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 7:7.
==============
STEP 5
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=234, prompt_tokens=3158, total_tokens=3392)
==== RAW OUTPUT ====
Thoughts:
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, and initializing the grid with random states. The next step is to implement the rules for Conway's Game of Life. This involves checking each cell's neighbors to determine its next state based on the number of alive neighbors. I will start by defining a function to count the alive neighbors for a given cell.
Action:
"""
edit 9 9 def count_alive_neighbors(grid, x, y):
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
count = 0
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
count += grid[ny][nx]
return count
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='def count_alive_neighbors(grid, x, y):\n directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]\n count = 0\n for dx, dy in directions:\n nx, ny = x + dx, y + dy\n if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:\n count += grid[ny][nx]\n return count', start=9, end=9, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, and initializing the grid with random states. The next step is to implement the rules for Conway's Game of Life. This involves checking each cell's neighbors to determine its next state based on the number of alive neighbors. I will start by defining a function to count the alive neighbors for a given cell.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 9:9.
==============
STEP 6
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=257, prompt_tokens=3290, total_tokens=3547)
==== RAW OUTPUT ====
Thoughts:
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating a function to count alive neighbors. The next step is to implement the rules for Conway's Game of Life. This involves updating the grid state based on the number of alive neighbors each cell has. I will define a function `update_grid` that applies the rules of the game to update the grid for the next generation.
Action:
"""
write game_of_life.py def update_grid(grid):
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
for y in range(grid_size[0]):
for x in range(grid_size[1]):
alive_neighbors = count_alive_neighbors(grid, x, y)
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
new_grid[y][x] = 1
elif grid[y][x] == 0 and alive_neighbors == 3:
new_grid[y][x] = 1
return new_grid
11
11
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='def update_grid(grid):\n new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]\n for y in range(grid_size[0]):\n for x in range(grid_size[1]):\n alive_neighbors = count_alive_neighbors(grid, x, y)\n if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):\n new_grid[y][x] = 1\n elif grid[y][x] == 0 and alive_neighbors == 3:\n new_grid[y][x] = 1\n return new_grid', start=11, end=11, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating a function to count alive neighbors. The next step is to implement the rules for Conway's Game of Life. This involves updating the grid state based on the number of alive neighbors each cell has. I will define a function `update_grid` that applies the rules of the game to update the grid for the next generation.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 11:11.
==============
STEP 7
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=124, prompt_tokens=3416, total_tokens=3540)
==== RAW OUTPUT ====
Thoughts:
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population.
Action:
"""
edit 6 6
grid = update_grid(grid)
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='grid = update_grid(grid)', start=6, end=6, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 6:6.
==============
STEP 8
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=166, prompt_tokens=3393, total_tokens=3559)
==== RAW OUTPUT ====
Thoughts:
So far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population. I need to ensure that the `update_grid` function is called within the main game loop and that the display is updated accordingly. I will start by checking the current content of the main game loop to see where to integrate the `update_grid` function.
Action:
"""
goto 6
"""
==== END RAW ====
ACTION:
FileReadAction(path='game_of_life.py', start=6, end=106, thoughts="Thoughts:\nSo far, I have set up the basic structure for Conway's Game of Life using pygame, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to integrate the `update_grid` function into the main game loop to continuously update the state of the grid based on the game rules. This will allow the game to evolve over time, showing different generations of the cell population. I need to ensure that the `update_grid` function is called within the main game loop and that the display is updated accordingly. I will start by checking the current content of the main game loop to see where to integrate the `update_grid` function.\n\nAction:", action=<ActionType.READ: 'read'>)
OBSERVATION:
grid = update_grid(grid)
running = True
# Define grid size and initialize grid
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
def count_alive_neighbors(grid, x, y):
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
def update_grid(grid):
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
for y in range(grid_size[0]):
for x in range(grid_size[1]):
alive_neighbors = count_alive_neighbors(grid, x, y)
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
new_grid[y][x] = 1
elif grid[y][x] == 0 and alive_neighbors == 3:
new_grid[y][x] = 1
return new_grid
count = 0
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
count += grid[ny][nx]
return count
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
import random
for y in range(grid_size[0]):
for x in range(grid_size[1]):
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.set_caption("Conway's Game of Life")
pygame.init()
==============
STEP 9
PLAN:
Please make conway's game of life in python using the pygame library. Be sure to use the tools in your workspace to help you out.
Usage(completion_tokens=174, prompt_tokens=3728, total_tokens=3902)
==== RAW OUTPUT ====
Thoughts:
From the previous actions, it seems that the basic structure for Conway's Game of Life using pygame has been set up, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to ensure that the game loop properly updates the grid and refreshes the display to show the new state of the grid in each iteration. This involves calling `pygame.display.update()` to update the entire screen and using `pygame.time.Clock()` to control the frame rate. I will start by adding these necessary updates to the game loop.
Action:
"""
edit 95 95
pygame.display.update()
clock = pygame.time.Clock()
clock.tick(10) # Control the frame rate (10 frames per second)
"""
==== END RAW ====
ACTION:
FileWriteAction(path='game_of_life.py', content='pygame.display.update()\n clock = pygame.time.Clock()\n clock.tick(10) # Control the frame rate (10 frames per second)', start=95, end=95, thoughts="Thoughts:\nFrom the previous actions, it seems that the basic structure for Conway's Game of Life using pygame has been set up, including initializing pygame, setting up the display window, defining the main game loop, initializing the grid with random states, and creating functions to count alive neighbors and update the grid. The next step is to ensure that the game loop properly updates the grid and refreshes the display to show the new state of the grid in each iteration. This involves calling `pygame.display.update()` to update the entire screen and using `pygame.time.Clock()` to control the frame rate. I will start by adding these necessary updates to the game loop.\n\nAction:", action=<ActionType.WRITE: 'write'>)
OBSERVATION:
WRITE OPERATION:
You have written to "game_of_life.py" on these lines: 95:95.
16:43:55 - opendevin:INFO: agent_controller.py:125 - Exited before finishing the task.
```
## Result:
```
import pygame
# Setup the display window
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
# Main game loop
grid = update_grid(grid)
running = True
# Define grid size and initialize grid
grid_size = (50, 50) # Grid size as a tuple (rows, columns)
def count_alive_neighbors(grid, x, y):
directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
def update_grid(grid):
new_grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])]
for y in range(grid_size[0]):
for x in range(grid_size[1]):
alive_neighbors = count_alive_neighbors(grid, x, y)
if grid[y][x] == 1 and (alive_neighbors == 2 or alive_neighbors == 3):
new_grid[y][x] = 1
elif grid[y][x] == 0 and alive_neighbors == 3:
new_grid[y][x] = 1
return new_grid
count = 0
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < grid_size[1] and 0 <= ny < grid_size[0]:
count += grid[ny][nx]
return count
grid = [[0 for x in range(grid_size[1])] for y in range(grid_size[0])] # Initialize grid with zeros
import random
for y in range(grid_size[0]):
for x in range(grid_size[1]):
grid[y][x] = random.randint(0, 1) # Randomly assign cells as alive (1) or dead (0)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.set_caption("Conway's Game of Life")
pygame.init()
# This is a placeholder for the game of life implementation using pygame.pygame.display.update()
clock = pygame.time.Clock()
clock.tick(10) # Control the frame rate (10 frames per second)
```

View File

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

108
agenthub/SWE_agent/agent.py Normal file
View File

@@ -0,0 +1,108 @@
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import (
Action,
FileReadAction,
FileWriteAction,
MessageAction,
)
from opendevin.events.observation import Observation
from opendevin.events.serialization.event import event_to_memory
from opendevin.llm.llm import LLM
from .parser import parse_command
from .prompts import (
CONTEXT_PROMPT,
MEMORY_FORMAT,
NO_ACTION,
STEP_PROMPT,
SYSTEM_MESSAGE,
)
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 'goto', 'search_for', 'edit', 'scroll', 'run'
"""
def __init__(self, llm: LLM):
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 _remember(self, action: Action, observation: Observation) -> None:
"""Agent has a limited memory of the few steps implemented as a queue"""
memory = MEMORY_FORMAT(event_to_memory(action), event_to_memory(observation))
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',
)
return parse_command(action_resp, self.cur_file, self.cur_line)
def _update(self, action: Action) -> None:
if isinstance(action, (FileReadAction, FileWriteAction)):
self.cur_file = action.path
self.cur_line = action.start
def step(self, 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)
"""
for prev_action, obs in state.updated_info:
self._remember(prev_action, obs)
goal = state.get_current_user_intent()
prompt = STEP_PROMPT(goal, self.cur_file, self.cur_line)
msgs = [
{'content': SYSTEM_MESSAGE, 'role': 'system'},
{'content': prompt, 'role': 'user'},
]
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)]))
action, thought = self._think_act(messages=msgs)
start_msg_len = len(msgs)
while not action and len(msgs) < self.max_retries + start_msg_len:
error = NO_ACTION(thought)
error_msg = {'content': error, 'role': 'user'}
msgs.append(error_msg)
action, thought = self._think_act(messages=msgs)
if not action:
action = MessageAction(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]
def reset(self) -> None:
"""Used to reset the agent"""
self.running_memory = []
super().reset()

View File

@@ -0,0 +1,167 @@
import re
from opendevin.events.action import (
Action,
AgentFinishAction,
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.'
)
def invalid_error(cmd, docs):
return f"""ERROR:
Invalid command structure for
```
{cmd}
```
You may have caused this error by having multiple commands within your command block.
If so, try again by running only one of the commands:
Try again using this format:
{COMMAND_USAGE[docs]}
"""
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
"""
vars = command_string.split(' ')
cmd = vars[0]
args = [] if len(vars) == 1 else vars[1:]
if 'exit' == cmd:
return AgentFinishAction()
elif 'think' == cmd:
return MessageAction(' '.join(args))
elif 'scroll_up' == cmd:
if not path:
return no_open_file_error
return FileReadAction(path, line + 100, line + 200, thoughts)
elif 'scroll_down' == cmd:
if not path:
return no_open_file_error
return FileReadAction(path, line - 100, line, thoughts)
elif 'goto' == cmd:
if not path:
return no_open_file_error
rex = r'^goto\s+(\d+)$'
valid = re.match(rex, command_string)
if valid:
start = int(valid.group(1))
end = start + 100
return FileReadAction(path, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'goto'))
elif 'edit' == cmd:
if not path:
return no_open_file_error
rex = r'^edit\s+(\d+)\s+(-?\d+)\s+(\S.*)$'
valid = re.match(rex, command_string, re.DOTALL)
if valid:
start = int(valid.group(1))
end = int(valid.group(2))
change = valid.group(3)
if '"' == change[-1] and '"' == change[0]:
change = change[1:-1]
return FileWriteAction(path, change, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'edit'))
elif 'read' == cmd:
rex = r'^read\s+(\S+)(?:\s+(\d+))?(?:\s+(-?\d+))?$'
valid = re.match(rex, command_string, re.DOTALL)
if valid:
file = valid.group(1)
start_str = valid.group(2)
end_str = valid.group(3)
start = 0 if not start_str else int(start_str)
end = -1 if not end_str else int(end_str)
return FileReadAction(file, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'read'))
elif 'write' == cmd:
rex = r'^write\s+(\S+)\s+(.*?)\s*(\d+)?\s*(-?\d+)?$'
valid = re.match(rex, command_string, re.DOTALL)
if valid:
file = valid.group(1)
content = valid.group(2)
start_str = valid.group(3)
end_str = valid.group(4)
start = 0 if not start_str else int(start_str)
end = -1 if not end_str else int(end_str)
if '"' == content[-1] and '"' == content[0]:
content = content[1:-1]
return FileWriteAction(file, content, start, end, thoughts)
else:
return MessageAction(invalid_error(command_string, 'write'))
elif 'browse' == cmd:
return BrowseURLAction(args[0].strip())
elif cmd in ['search_file', 'search_dir', 'find_file']:
rex = r'^(search_file|search_dir|find_file)\s+(\S+)(?:\s+(\S+))?$'
valid = re.match(rex, command_string, re.DOTALL)
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}'
)
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)
else:
# run valid command
return CmdRunAction(command_string)
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.
Args:
input_str (str): The input string to be parsed.
Returns:
tuple: A tuple containing the command and the accompanying text (if any).
"""
input_str = input_str.strip()
if '```' in input_str:
parts = input_str.split('```')
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)
if action:
return action, accompanying_text
return None, input_str # used for retry

View File

@@ -0,0 +1,207 @@
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.",
'write <file> <changes> [<start_line>] [<end_line>]': 'Modifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>. Default start_line = 0 and end_line = -1. Calling this with no line args will replace the whole file.',
'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',
'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',
}
COMMAND_USAGE = {
'exit': 'Usage:\n```\nexit\n```\nExecuted when task is complete',
'read': "Args:\n<file_name> [<start_line>] [<end_line>]\nUsage:\n```\nread file.py\n```\nor\n```\nread example.py <start_line> <end_line>\n```\nShows 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.",
'write': 'Args:\n<file> <changes> [<start_line>] [<end_line>]\nUsage:\n```\nwrite "def main():\n print("This is line one")" 0 2\n```\nModifies a <file> by replacing the current lines between <start_line> and <end_line> with <changes>. Default start_line = 0 and end_line = -1. Calling this with no line args will replace the whole file.',
'edit': 'Args:\n<start_line> <end_line> <changes>\nUsage:\n```\nedit 0 1 import pandas as pd\n```\nThis will modify the current file you are in with the changes you make between the line numbers you designate',
'goto': 'Args:\n<line_num>\nUsage:\n```\ngoto <line_num>\n```\nThis will show you the 100 lines below and including the line you specify within your current file.',
'scroll_up': 'Usage:\n```\nscroll_up\n```\nThis will return the 100 lines above where you are currently at',
'scroll_down': 'Usage:\n```\nscroll_down\n```\nThis will return the 100 line below where you are currently at',
'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()])
# from opendevin.parse_commands import parse_command_file
# USE parse_command_file(filepath) to get the custom commands
CUSTOM_DOCS = None
CUSTOM_COMMANDS = f"""Custom bash commands:
{CUSTOM_DOCS}
"""
DOCUMENTATION = f"""DOCUMENTATION:
It is recommend that you use the commands provided for interacting with files and your directory because they have been specially built for you.
They will make it much easier for you to look at files and make changes. Using these commands will help you be better at your task.
You can open an file by using either the read or write operations.
- If a file already exists you should read it before making any changes. Use the `edit` command to make changes once you have read it.
- If you are creating a new file use the write command. Use the `edit` command to make changes once you have created the new file.
Commands:
{DEFAULT_COMMANDS}
{CUSTOM_COMMANDS}
The following commands require an open file to be used: edit, scroll_up, scroll_down, goto
To modify the current file use 'edit'. To move through the current file use 'goto' or 'scroll_up'/'scroll_down'
when using write and edit do not surround the code with any "" just write the code.
"""
GENERAL_GUIDELINES = """INSTRUCTIONS:
Now, you're going to solve this issue on your own. You can use any bash commands or custom commands you wish to complete your task. Edit all the files you need to and run any checks or tests that you want.
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
When you're satisfied with all of the changes you've made, you can indicate that you are done by running the exit command.
Note however that you cannot use any interactive session commands (e.g. python, vim, node) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
NOTE ABOUT THE write COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
IMPORTANT TIPS:
1. Reproduce the bug: Always start by trying to replicate the bug that the issue discusses. If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment and run it to ensure you can reproduce the bug. Then, start trying to fix it. When you think you've fixed the bug, re-run the bug reproduction script to make sure that the issue has indeed been resolved.
If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file, so that you can be sure the script ran fine all the way through.
2. Try different commands: If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it.
3. Navigate large files: If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, you would use the 'read' command like this: 'read <file> 583'. This is a much faster way to read through the file.
4. Handle input files: If the bug reproduction script requires inputting/reading a specific file, such as 'buggy-input.png', and you'd like to understand how to input that file, conduct a search in the existing repository code to see whether someone else has already done that. Do this by running the command: 'search_dir "buggy-input.png"'. If that doesn't work, use the Linux 'find' command.
5. Understand your context: Always make sure to look at the currently open file and the current working directory. The currently open file might be in a different directory than the working directory.
6. Verify your edits: When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
7. Thoroughly test your solution: After making any changes to fix a bug, be sure to thoroughly test your solution to ensure the bug has been resolved. Re-run the bug reproduction script and verify that the issue has been addressed.
"""
RESPONSE_FORMAT = """RESPONSE FORMAT:
This is the format of the response you will make in order to solve the current issue.
You will be given multiple iterations to complete this task so break it into steps and solve them one by one.
Your output must contain the following:
- First, thoughts about what your next action should be and plan it out.
- You will have a memory of your thoughts so you can use this to remember things for the next step.
- Use your thoughts to think about what you are currently doing, what you have done on prior steps and how that relates to solving the problem.
- Second, create a piece of code that will execute your next action based on the thoughts you have.
- Remember that you can only have one action for each thought, do not include multiple actions.
Your code MUST be surrounded in triple back ticks EXACTLY like this:
```
<code>
```
Notes:
- Adhere to the format so that the program loop continues smoothly, it is very important to only give one command per output.
- DO NOT give more than one command within the triple backticks. This will just throw an error and nothing will happen as a result.
- Do not give multiple code blocks, if you do only the second one will be captured and run, this might give an error if the first one was necessary.
- 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.
- 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.
This is a template using the format described above
Items in <> are suggestions for you, fill them out based on the context of the problem you are solving.
[ FORMAT ]
Thoughts:
<Provide clear and concise thoughts on the next step to take, highlighting any important details or context that should be remembered.>
<You can use multiple lines to express your thoughts>
Action:
```
<command> <params>
```
[ END FORMAT ]
Do not provide anything extra just your thought and action.
"""
SYSTEM_MESSAGE = f"""SYSTEM INFO:
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}
{DOCUMENTATION}
""".strip()
def NO_ACTION(latest):
return f"""
You did not include any action to take in your most recent output:
===== Output ======
{latest}
==== End Output ===
Remember these are the custom commands you can use:
{DOCUMENTATION}
Lets try that again, it is very important that you adhere to the output format
This time, be sure to use the exact format below, replacing anything in <> with the appropriate value(s):
{RESPONSE_FORMAT}
It is crucial you use the format provided as the output will be parsed automatically.
"""
def file_info(file: str, line: int):
if file:
return f"""CURRENT WORKSPACE:
Open File: {file} on line {line}
You can use these commands with the current file:
Navigation: `scroll_up`, `scroll_down`, and `goto <line>`
Modification: `edit <start_line> <end_line> <changes>`
"""
def STEP_PROMPT(task, file, line_num):
return f"""
{RESPONSE_FORMAT}
You are currently trying to complete this task:
{task}
{file_info(file, line_num)}
Keep all of the guidelines above in mind when you are thinking and making code.
Please come up with a thought and action based on your current task and latest steps.
Make sure that you do not repeat the same actions, there will not be any changes in result if you do not changes anything.
Be very strict about the formatting that you use and make sure you follow the guidelines.
NEVER output multiple commands. ONLY take ONE STEP at a time.
When you have completed your task run the "exit" command.
Begin with your thought about the next step and then come up with an action to perform your thought.
""".strip()
def unpack_dict(data: dict, restrict: list[str] = []):
lines = []
for key, value in data.items():
if key in restrict:
continue
elif isinstance(value, dict):
nested_str = unpack_dict(value, restrict).replace('\n', '\n ')
val = f'{key}:' + '\n ' + f'{nested_str}'
lines.append(val)
else:
lines.append(f'{key}: {value}')
return '\n'.join(lines)
def MEMORY_FORMAT(act, obs):
return f"""
Previous Action:
{unpack_dict(act, ["content"])}
Output from Action:
{unpack_dict(obs)}
""".strip()
def CONTEXT_PROMPT(memory, window):
res = f'These are your past {window} actions:\n'
window_size = window if len(memory) > window else len(memory)
cur_mems = memory[-window_size:]
res += '===== Previous Actions =====\n'
for idx, mem in enumerate(cur_mems):
res += f'\nMemory {idx}:\n{mem}\n'
res += '======= End Actions =======\n'
res += 'Use these memories to provide additional context to the problem you are solving.\nRemember that you have already completed these steps so you do not need to perform them again.'
return res

View File

@@ -9,12 +9,10 @@ load_dotenv()
from . import ( # noqa: E402
browsing_agent,
SWE_agent,
codeact_agent,
codeact_swe_agent,
delegator_agent,
dummy_agent,
gptswarm_agent,
monologue_agent,
planner_agent,
)
@@ -22,12 +20,10 @@ from . import ( # noqa: E402
__all__ = [
'monologue_agent',
'codeact_agent',
'gptswarm_agent',
'codeact_swe_agent',
'planner_agent',
'SWE_agent',
'delegator_agent',
'dummy_agent',
'browsing_agent',
]
for agent in all_microagents.values():

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

View File

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

View File

@@ -1,215 +0,0 @@
import os
from browsergym.core.action.highlevel import HighLevelActionSet
from browsergym.utils.obs import flatten_axtree_to_str
from agenthub.browsing_agent.response_parser import BrowsingResponseParser
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.event import EventSource
from opendevin.events.observation import BrowserOutputObservation
from opendevin.events.observation.observation import Observation
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins import (
PluginRequirement,
)
from opendevin.runtime.tools import RuntimeTool
USE_NAV = (
os.environ.get('USE_NAV', 'true') == 'true'
) # only disable NAV actions when running webarena and miniwob benchmarks
USE_CONCISE_ANSWER = (
os.environ.get('USE_CONCISE_ANSWER', 'false') == 'true'
) # only return concise answer when running webarena and miniwob benchmarks
if not USE_NAV and USE_CONCISE_ANSWER:
EVAL_MODE = True # disabled NAV actions and only return concise answer, for webarena and miniwob benchmarks\
else:
EVAL_MODE = False
def get_error_prefix(last_browser_action: str) -> str:
return f'IMPORTANT! Last action is incorrect:\n{last_browser_action}\nThink again with the current observation of the page.\n'
def get_system_message(goal: str, action_space: str) -> str:
return 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
{action_space}
"""
CONCISE_INSTRUCTION = """\
Here is another example with chain of thought of a valid action when providing a concise answer to user:
"
In order to accomplish my goal I need to send the information asked back to the user. This page list the information of HP Inkjet Fax Machine, which is the product identified in the objective. Its price is $279.49. I will send a message back to user with the answer.
```send_msg_to_user("$279.49")```
"
"""
def get_prompt(error_prefix: str, cur_axtree_txt: str, prev_action_str: str) -> str:
prompt = f"""\
{error_prefix}
# Current Accessibility Tree:
{cur_axtree_txt}
# Previous Actions
{prev_action_str}
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()
if USE_CONCISE_ANSWER:
prompt += CONCISE_INSTRUCTION
return prompt
class BrowsingAgent(Agent):
VERSION = '1.0'
"""
An agent that interacts with the browser.
"""
sandbox_plugins: list[PluginRequirement] = []
runtime_tools: list[RuntimeTool] = [RuntimeTool.BROWSER]
response_parser = BrowsingResponseParser()
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)
# define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
# see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
action_subsets = ['chat', 'bid']
if USE_NAV:
action_subsets.append('nav')
self.action_space = HighLevelActionSet(
subsets=action_subsets,
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
self.error_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
"""
messages = []
prev_actions = []
cur_axtree_txt = ''
error_prefix = ''
last_obs = None
last_action = None
if EVAL_MODE and len(state.history.get_events_as_list()) == 1:
# for webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
# initialize and retrieve the first observation by issuing an noop OP
# For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
return BrowseInteractiveAction(browser_actions='noop()')
for event in state.history.get_events():
if isinstance(event, BrowseInteractiveAction):
prev_actions.append(event.browser_actions)
last_action = event
elif isinstance(event, MessageAction) and event.source == EventSource.AGENT:
# agent has responded, task finished.
return AgentFinishAction(outputs={'content': event.content})
elif isinstance(event, Observation):
last_obs = event
if EVAL_MODE:
prev_actions = prev_actions[1:] # remove the first noop action
prev_action_str = '\n'.join(prev_actions)
# if the final BrowserInteractiveAction exec BrowserGym's send_msg_to_user,
# we should also send a message back to the user in OpenDevin and call it a day
if (
isinstance(last_action, BrowseInteractiveAction)
and last_action.browsergym_send_msg_to_user
):
return MessageAction(last_action.browsergym_send_msg_to_user)
if isinstance(last_obs, BrowserOutputObservation):
if last_obs.error:
# add error recovery prompt prefix
error_prefix = get_error_prefix(last_obs.last_browser_action)
self.error_accumulator += 1
if self.error_accumulator > 5:
return MessageAction('Too many errors encountered. Task failed.')
try:
cur_axtree_txt = flatten_axtree_to_str(
last_obs.axtree_object,
extra_properties=last_obs.extra_element_properties,
with_clickable=True,
filter_visible_only=True,
)
except Exception as e:
logger.error(
'Error when trying to process the accessibility tree: %s', e
)
return MessageAction('Error encountered when browsing.')
if (goal := state.get_current_user_intent()) is None:
goal = state.inputs['task']
system_msg = get_system_message(
goal,
self.action_space.describe(with_long_description=False, with_examples=True),
)
messages.append({'role': 'system', 'content': system_msg})
prompt = get_prompt(error_prefix, cur_axtree_txt, prev_action_str)
messages.append({'role': 'user', 'content': prompt})
logger.debug(prompt)
response = self.llm.completion(
messages=messages,
temperature=0.0,
stop=[')```', ')\n```'],
)
return self.response_parser.parse(response)

View File

@@ -1,787 +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 shrinking strategy 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 shrunk 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)}.',
stacklevel=2,
)
if flags.demo_mode != 'off':
warn(
f'Flag action_space={repr(flags.action_space)} incompatible with demo_mode={repr(flags.demo_mode)}.',
stacklevel=2,
)
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
)

View File

@@ -1,90 +0,0 @@
import ast
from opendevin.controller.action_parser import ActionParser, ResponseParser
from opendevin.core.logger import opendevin_logger as logger
from opendevin.events.action import (
Action,
BrowseInteractiveAction,
)
class BrowsingResponseParser(ResponseParser):
def __init__(self):
# Need to pay attention to the item order in self.action_parsers
super().__init__()
self.action_parsers = [BrowsingActionParserMessage()]
self.default_parser = BrowsingActionParserBrowseInteractive()
def parse(self, response: str) -> Action:
action_str = self.parse_response(response)
return self.parse_action(action_str)
def parse_response(self, response) -> str:
action_str = response['choices'][0]['message']['content']
if action_str is None:
return ''
action_str = action_str.strip()
if not action_str.endswith('```'):
action_str = action_str + ')```'
logger.info(action_str)
return action_str
def parse_action(self, action_str: str) -> Action:
for action_parser in self.action_parsers:
if action_parser.check_condition(action_str):
return action_parser.parse(action_str)
return self.default_parser.parse(action_str)
class BrowsingActionParserMessage(ActionParser):
"""
Parser action:
- BrowseInteractiveAction(browser_actions) - unexpected response format, message back to user
"""
def __init__(
self,
):
pass
def check_condition(self, action_str: str) -> bool:
return '```' not in action_str
def parse(self, action_str: str) -> Action:
msg = f'send_msg_to_user("""{action_str}""")'
return BrowseInteractiveAction(
browser_actions=msg,
thought=action_str,
browsergym_send_msg_to_user=action_str,
)
class BrowsingActionParserBrowseInteractive(ActionParser):
"""
Parser action:
- BrowseInteractiveAction(browser_actions) - handle send message to user function call in BrowserGym
"""
def __init__(
self,
):
pass
def check_condition(self, action_str: str) -> bool:
return True
def parse(self, action_str: str) -> Action:
thought = action_str.split('```')[0].strip()
action_str = action_str.split('```')[1].strip()
msg_content = ''
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
msg_content = args[0].value
return BrowseInteractiveAction(
browser_actions=action_str,
thought=thought,
browsergym_send_msg_to_user=msg_content,
)

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), stacklevel=2)
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

View File

@@ -1,6 +1,6 @@
# CodeAct 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 ([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:

View File

@@ -1,183 +0,0 @@
import re
from opendevin.controller.action_parser import ActionParser, ResponseParser
from opendevin.events.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
class CodeActResponseParser(ResponseParser):
"""
Parser action:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
def __init__(self):
# Need pay attention to the item order in self.action_parsers
super().__init__()
self.action_parsers = [
CodeActActionParserFinish(),
CodeActActionParserCmdRun(),
CodeActActionParserIPythonRunCell(),
CodeActActionParserAgentDelegate(),
]
self.default_parser = CodeActActionParserMessage()
def parse(self, response) -> Action:
action_str = self.parse_response(response)
return self.parse_action(action_str)
def parse_response(self, response) -> str:
action = response.choices[0].message.content
if action is None:
return ''
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 parse_action(self, action_str: str) -> Action:
for action_parser in self.action_parsers:
if action_parser.check_condition(action_str):
return action_parser.parse(action_str)
return self.default_parser.parse(action_str)
class CodeActActionParserFinish(ActionParser):
"""
Parser action:
- AgentFinishAction() - end the interaction
"""
def __init__(
self,
):
self.finish_command = None
def check_condition(self, action_str: str) -> bool:
self.finish_command = re.search(r'<finish>.*</finish>', action_str, re.DOTALL)
return self.finish_command is not None
def parse(self, action_str: str) -> Action:
assert (
self.finish_command is not None
), 'self.finish_command should not be None when parse is called'
thought = action_str.replace(self.finish_command.group(0), '').strip()
return AgentFinishAction(thought=thought)
class CodeActActionParserCmdRun(ActionParser):
"""
Parser action:
- CmdRunAction(command) - bash command to run
- AgentFinishAction() - end the interaction
"""
def __init__(
self,
):
self.bash_command = None
def check_condition(self, action_str: str) -> bool:
self.bash_command = re.search(
r'<execute_bash>(.*?)</execute_bash>', action_str, re.DOTALL
)
return self.bash_command is not None
def parse(self, action_str: str) -> Action:
assert (
self.bash_command is not None
), 'self.bash_command should not be None when parse is called'
thought = action_str.replace(self.bash_command.group(0), '').strip()
# a command was found
command_group = self.bash_command.group(1).strip()
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command=command_group, thought=thought)
class CodeActActionParserIPythonRunCell(ActionParser):
"""
Parser action:
- IPythonRunCellAction(code) - IPython code to run
"""
def __init__(
self,
):
self.python_code = None
self.jupyter_kernel_init_code: str = 'from agentskills import *'
def check_condition(self, action_str: str) -> bool:
self.python_code = re.search(
r'<execute_ipython>(.*?)</execute_ipython>', action_str, re.DOTALL
)
return self.python_code is not None
def parse(self, action_str: str) -> Action:
assert (
self.python_code is not None
), 'self.python_code should not be None when parse is called'
code_group = self.python_code.group(1).strip()
thought = action_str.replace(self.python_code.group(0), '').strip()
return IPythonRunCellAction(
code=code_group,
thought=thought,
kernel_init_code=self.jupyter_kernel_init_code,
)
class CodeActActionParserAgentDelegate(ActionParser):
"""
Parser action:
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
"""
def __init__(
self,
):
self.agent_delegate = None
def check_condition(self, action_str: str) -> bool:
self.agent_delegate = re.search(
r'<execute_browse>(.*)</execute_browse>', action_str, re.DOTALL
)
return self.agent_delegate is not None
def parse(self, action_str: str) -> Action:
assert (
self.agent_delegate is not None
), 'self.agent_delegate should not be None when parse is called'
thought = action_str.replace(self.agent_delegate.group(0), '').strip()
browse_actions = self.agent_delegate.group(1).strip()
task = f'{thought}. I should start with: {browse_actions}'
return AgentDelegateAction(agent='BrowsingAgent', inputs={'task': task})
class CodeActActionParserMessage(ActionParser):
"""
Parser action:
- MessageAction(content) - Message action to run (e.g. ask for clarification)
"""
def __init__(
self,
):
pass
def check_condition(self, action_str: str) -> bool:
# We assume the LLM is GOOD enough that when it returns pure natural language
# it wants to talk to the user
return True
def parse(self, action_str: str) -> Action:
return MessageAction(content=action_str, wait_for_response=True)

View File

@@ -1,4 +1,5 @@
from agenthub.codeact_agent.action_parser import CodeActResponseParser
import re
from agenthub.codeact_agent.prompt import (
COMMAND_DOCS,
EXAMPLES,
@@ -8,102 +9,85 @@ from agenthub.codeact_agent.prompt import (
)
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.events.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
BrowseInteractiveAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
from opendevin.events.observation import (
AgentDelegateObservation,
BrowserOutputObservation,
CmdOutputObservation,
IPythonRunCellObservation,
)
from opendevin.events.serialization.event import truncate_content
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins import (
AgentSkillsRequirement,
JupyterRequirement,
PluginRequirement,
SWEAgentCommandsRequirement,
)
from opendevin.runtime.tools import RuntimeTool
ENABLE_GITHUB = True
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, AgentDelegateAction):
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
elif isinstance(action, MessageAction):
return action.content
return ''
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 get_action_message(action: Action) -> dict[str, str] | None:
if (
isinstance(action, AgentDelegateAction)
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 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_observation_message(obs) -> dict[str, str] | None:
max_message_chars = config.get_llm_config_from_agent(
'CodeActAgent'
).max_message_chars
if isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
content += (
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
def swe_agent_edit_hack(bash_command: str) -> str:
"""
Hack to handle the SWE-agent edit command. The vanilla edit command will hang the SSHBox.
REPLACE THIS:
edit 683:693
try:
return list(urlsplit(url))
except ValueError:
raise ValidationError(self.error_messages['invalid'], code='invalid')
end_of_edit
WITH THIS:
edit 683:693 <<EOF
try:
return list(urlsplit(url))
except ValueError:
raise ValidationError(self.error_messages['invalid'], code='invalid')
EOF
"""
if 'edit' in bash_command:
# edit\s(\d+):(\d+)([\s\S]*)end_of_edit
# replace
bash_command = re.sub(
r'edit\s(\d+):(\d+)([\s\S]*?)end_of_edit',
r'edit \1:\2 <<EOF\3EOF',
bash_command,
)
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_content(content, max_message_chars)
return {'role': 'user', 'content': content}
elif isinstance(obs, AgentDelegateObservation):
content = 'OBSERVATION:\n' + truncate_content(
str(obs.outputs), max_message_chars
)
return {'role': 'user', 'content': content}
return None
# 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
return bash_command
class CodeActAgent(Agent):
VERSION = '1.8'
VERSION = '1.3'
"""
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.
@@ -141,18 +125,15 @@ class CodeActAgent(Agent):
"""
sandbox_plugins: list[PluginRequirement] = [
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
# AgentSkillsRequirement provides a lot of Python functions,
# and it needs to be initialized before Jupyter for Jupyter to use those functions.
AgentSkillsRequirement(),
JupyterRequirement(),
SWEAgentCommandsRequirement(),
]
runtime_tools: list[RuntimeTool] = [RuntimeTool.BROWSER]
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!"
action_parser = CodeActResponseParser()
system_message: str = (
f'{SYSTEM_PREFIX}\n{GITHUB_MESSAGE}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
if ENABLE_GITHUB
else f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
)
def __init__(
self,
@@ -172,6 +153,14 @@ class CodeActAgent(Agent):
Resets the CodeAct Agent.
"""
super().reset()
self.messages: list[dict[str, str]] = [
{'role': 'system', 'content': self.system_message},
{
'role': 'user',
'content': f"Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\nNOW, LET'S START!",
},
]
self.cost_accumulator = 0
def step(self, state: State) -> Action:
"""
@@ -179,26 +168,58 @@ class CodeActAgent(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
- state (State): used to get updated info and background commands
Returns:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
# if we're done, go back
latest_user_message = state.history.get_last_user_message()
if latest_user_message and latest_user_message.strip() == '/exit':
return AgentFinishAction()
updated_info = state.updated_info
if updated_info:
for prev_action, obs in updated_info:
if (
isinstance(prev_action, MessageAction)
and prev_action.source == 'user'
):
self.messages.append(
{'role': 'user', 'content': prev_action.content}
)
if prev_action.content.strip() == '/exit':
# User wants to exit
return AgentFinishAction()
# prepare what we want to send to the LLM
messages: list[dict[str, str]] = self._get_messages(state)
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}]]'
self.messages.append({'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)
self.messages.append({'role': 'user', 'content': content})
elif isinstance(obs, BrowserOutputObservation):
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
self.messages.append({'role': 'user', 'content': content})
latest_user_message = [m for m in self.messages if m['role'] == 'user'][-1]
if latest_user_message:
latest_user_message['content'] += (
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
)
response = self.llm.completion(
messages=messages,
messages=self.messages,
stop=[
'</execute_ipython>',
'</execute_bash>',
@@ -206,36 +227,62 @@ class CodeActAgent(Agent):
],
temperature=0.0,
)
return self.action_parser.parse(response)
def _get_messages(self, state: State) -> list[dict[str, str]]:
messages = [
{'role': 'system', 'content': self.system_message},
{'role': 'user', 'content': self.in_context_example},
]
self.log_cost(response)
for event in state.history.get_events():
# create a regular message from an event
message = (
get_action_message(event)
if isinstance(event, Action)
else get_observation_message(event)
action_str: str = parse_response(response)
state.num_of_chars += sum(
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
):
# 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()
command_group = swe_agent_edit_hack(command_group)
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)
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)
# add regular message
if message:
messages.append(message)
def search_memory(self, query: str) -> list[str]:
raise NotImplementedError('Implement this abstract method')
# the latest user message is important:
# we want to remind the agent of the environment constraints
latest_user_message = next(
(m for m in reversed(messages) if m['role'] == 'user'), None
def log_cost(self, response):
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,
)
# add a reminder to the prompt
if latest_user_message:
latest_user_message['content'] += (
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with <finish></finish>'
)
return messages

View File

@@ -1,74 +1,70 @@
from opendevin.runtime.plugins import AgentSkillsRequirement
from opendevin.runtime.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. Remember, the file must be open before editing.
"""
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_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE 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."
'\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.
The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with <execute_ipython>.
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>.
Important, however: do not run interactive commands. You do not have access to stdin.
Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
"""
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.
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.
If the assistant encounters an import error in IPython for a newly installed package, they should try to restart the kernel and import the package again. IPython kernel can be re-started by:
<execute_ipython>
import IPython
IPython.Application.instance().kernel.do_shutdown(True) # Restart the kernel
</execute_ipython>"""
BROWSING_PREFIX = """The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
"""
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."""
SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX
GITHUB_MESSAGE = """To interact with GitHub, use the $GITHUB_TOKEN environment variable.
For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
GITHUB_MESSAGE = """To do any activities on GitHub, you 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`, you can use the following four commands:
<execute_bash> git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch </execute_bash>
If $GITHUB_TOKEN is not set, ask the user to set it."""
If you require access to GitHub but $GITHUB_TOKEN is not set, ask the user to set it for you."""
SYSTEM_SUFFIX = """Responses should be concise.
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
If the assistant is finished with the task you MUST include <finish></finish> in your response.
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
When handling files, try to use full paths and pwd to avoid errors.
SYSTEM_SUFFIX = """The assistant's response should be concise.
You should include <execute_ipython> or <execute_bash> or <execute_browse> in every one of your responses, unless you are 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.
"""
# ======= EXAMPLE MESSAGE =======
EXAMPLES = """
--- START OF EXAMPLE ---
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
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 Python file `app.py`:
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)]
(this is the beginning of the file)
1|
(this is the end of the file)
[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('/')
@@ -77,30 +73,15 @@ def index():
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)\"\"\"
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
app.run(port=5000)
'''
with open('app.py', 'w') as f:
f.write(CODE)
</execute_ipython>
USER:
OBSERVATION:
(this is the beginning of the file)
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)
(this is the end of the file)
[File updated (edited at line 1). 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:
@@ -109,7 +90,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:
@@ -118,7 +99,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
@@ -131,7 +112,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)
@@ -155,7 +136,7 @@ python3 app.py > server.log 2>&1 &
</execute_bash>
USER:
OBSERVATION:
Observation:
[1] 124
ASSISTANT:
@@ -165,7 +146,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.
@@ -180,11 +161,11 @@ 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>
Get the content on "http://127.0.0.1:5000"
goto("http://127.0.0.1:5000")
</execute_browse>
USER:
OBSERVATION:
Observation:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ASSISTANT:
@@ -199,59 +180,55 @@ 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)]
(this is the beginning of the file)
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)
(this is the end of the file)
[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_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
<execute_bash>
edit 8:8
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
end_of_edit
</execute_bash>
USER:
Observation:
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
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)
(this is the end of the file)
[File updated (edited at line 7). 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:
Running the updated file:
The file has been updated. Let me run the Python file again with the new changes:
<execute_bash>
python3 app.py > server.log 2>&1 &
</execute_bash>
@@ -261,15 +238,14 @@ Observation:
[1] 126
ASSISTANT:
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
<finish></finish>
The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Free free to let me know if you have any further requests!
--- END OF EXAMPLE ---
"""
INVALID_INPUT_MESSAGE = (
"I don't understand your input. \n"
'For bash commands, use <execute_bash> YOUR_COMMAND </execute_bash>.\n'
'For Python code, use <execute_ipython> YOUR_CODE </execute_ipython>.\n'
'For browsing, use <execute_browse> YOUR_COMMAND </execute_browse>.\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'
)

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.

View File

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

View File

@@ -1,114 +0,0 @@
import re
from opendevin.controller.action_parser import ActionParser
from opendevin.events.action import (
Action,
AgentFinishAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
class CodeActSWEActionParserFinish(ActionParser):
"""
Parser action:
- AgentFinishAction() - end the interaction
"""
def __init__(
self,
):
self.finish_command = None
def check_condition(self, action_str: str) -> bool:
self.finish_command = re.search(r'<finish>.*</finish>', action_str, re.DOTALL)
return self.finish_command is not None
def parse(self, action_str: str) -> Action:
assert (
self.finish_command is not None
), 'self.finish_command should not be None when parse is called'
thought = action_str.replace(self.finish_command.group(0), '').strip()
return AgentFinishAction(thought=thought)
class CodeActSWEActionParserCmdRun(ActionParser):
"""
Parser action:
- CmdRunAction(command) - bash command to run
- AgentFinishAction() - end the interaction
"""
def __init__(
self,
):
self.bash_command = None
def check_condition(self, action_str: str) -> bool:
self.bash_command = re.search(
r'<execute_bash>(.*?)</execute_bash>', action_str, re.DOTALL
)
return self.bash_command is not None
def parse(self, action_str: str) -> Action:
assert (
self.bash_command is not None
), 'self.bash_command should not be None when parse is called'
thought = action_str.replace(self.bash_command.group(0), '').strip()
# a command was found
command_group = self.bash_command.group(1).strip()
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command=command_group, thought=thought)
class CodeActSWEActionParserIPythonRunCell(ActionParser):
"""
Parser action:
- IPythonRunCellAction(code) - IPython code to run
"""
def __init__(
self,
):
self.python_code = None
self.jupyter_kernel_init_code: str = 'from agentskills import *'
def check_condition(self, action_str: str) -> bool:
self.python_code = re.search(
r'<execute_ipython>(.*?)</execute_ipython>', action_str, re.DOTALL
)
return self.python_code is not None
def parse(self, action_str: str) -> Action:
assert (
self.python_code is not None
), 'self.python_code should not be None when parse is called'
code_group = self.python_code.group(1).strip()
thought = action_str.replace(self.python_code.group(0), '').strip()
return IPythonRunCellAction(
code=code_group,
thought=thought,
kernel_init_code=self.jupyter_kernel_init_code,
)
class CodeActSWEActionParserMessage(ActionParser):
"""
Parser action:
- MessageAction(content) - Message action to run (e.g. ask for clarification)
"""
def __init__(
self,
):
pass
def check_condition(self, action_str: str) -> bool:
# We assume the LLM is GOOD enough that when it returns pure natural language
# it wants to talk to the user
return True
def parse(self, action_str: str) -> Action:
return MessageAction(content=action_str, wait_for_response=True)

View File

@@ -1,195 +0,0 @@
from agenthub.codeact_swe_agent.prompt import (
COMMAND_DOCS,
SWE_EXAMPLE,
SYSTEM_PREFIX,
SYSTEM_SUFFIX,
)
from agenthub.codeact_swe_agent.response_parser import CodeActSWEResponseParser
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import config
from opendevin.events.action import (
Action,
AgentFinishAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
from opendevin.events.observation import (
CmdOutputObservation,
IPythonRunCellObservation,
)
from opendevin.events.serialization.event import truncate_content
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins import (
AgentSkillsRequirement,
JupyterRequirement,
PluginRequirement,
)
from opendevin.runtime.tools import RuntimeTool
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, MessageAction):
return action.content
return ''
def get_action_message(action: Action) -> dict[str, str] | None:
if (
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:
max_message_chars = config.get_llm_config_from_agent(
'CodeActSWEAgent'
).max_message_chars
if isinstance(obs, CmdOutputObservation):
content = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
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_content(content, max_message_chars)
return {'role': 'user', 'content': content}
return None
def get_system_message() -> str:
return f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
def get_in_context_example() -> str:
return SWE_EXAMPLE
class CodeActSWEAgent(Agent):
VERSION = '1.6'
"""
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 needs to be initialized before Jupyter for Jupyter to use those functions.
AgentSkillsRequirement(),
JupyterRequirement(),
]
runtime_tools: list[RuntimeTool] = []
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!"
response_parser = CodeActSWEResponseParser()
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
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
# if we're done, go back
latest_user_message = state.history.get_last_user_message()
if latest_user_message and latest_user_message.strip() == '/exit':
return AgentFinishAction()
# prepare what we want to send to the LLM
messages: list[dict[str, str]] = self._get_messages(state)
response = self.llm.completion(
messages=messages,
stop=[
'</execute_ipython>',
'</execute_bash>',
],
temperature=0.0,
)
return self.response_parser.parse(response)
def _get_messages(self, state: State) -> list[dict[str, str]]:
messages = [
{'role': 'system', 'content': self.system_message},
{'role': 'user', 'content': self.in_context_example},
]
for event in state.history.get_events():
# create a regular message from an event
message = (
get_action_message(event)
if isinstance(event, Action)
else get_observation_message(event)
)
# add regular message
if message:
messages.append(message)
# the latest user message is important:
# we want to remind the agent of the environment constraints
latest_user_message = next(
(m for m in reversed(messages) if m['role'] == 'user'), None
)
# add a reminder to the prompt
if latest_user_message:
latest_user_message['content'] += (
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
)
return messages

View File

@@ -1,455 +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>.
"""
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."""
SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + PIP_INSTALL_PREFIX
SYSTEM_SUFFIX = """The assistant's response should be concise.
The assistant should include ONLY ONE <execute_ipython> or <execute_bash> 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> 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 ---
"""

View File

@@ -1,47 +0,0 @@
from agenthub.codeact_swe_agent.action_parser import (
CodeActSWEActionParserCmdRun,
CodeActSWEActionParserFinish,
CodeActSWEActionParserIPythonRunCell,
CodeActSWEActionParserMessage,
)
from opendevin.controller.action_parser import ResponseParser
from opendevin.events.action import Action
class CodeActSWEResponseParser(ResponseParser):
"""
Parser action:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
def __init__(self):
# Need pay attention to the item order in self.action_parsers
super().__init__()
self.action_parsers = [
CodeActSWEActionParserFinish(),
CodeActSWEActionParserCmdRun(),
CodeActSWEActionParserIPythonRunCell(),
]
self.default_parser = CodeActSWEActionParserMessage()
def parse(self, response: str) -> Action:
action_str = self.parse_response(response)
return self.parse_action(action_str)
def parse_response(self, response) -> str:
action = response.choices[0].message.content
if action is None:
return ''
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 parse_action(self, action_str: str) -> Action:
for action_parser in self.action_parsers:
if action_parser.check_condition(action_str):
return action_parser.parse(action_str)
return self.default_parser.parse(action_str)

View File

@@ -6,9 +6,9 @@ from opendevin.llm.llm import LLM
class DelegatorAgent(Agent):
VERSION = '1.0'
"""
The Delegator Agent is responsible for delegating tasks to other agents based on the current task.
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 = ''
@@ -25,14 +25,14 @@ class DelegatorAgent(Agent):
def step(self, state: State) -> Action:
"""
Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, delegates the task to the next agent in the pipeline.
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
Parameters:
- state (State): The current state given the previous actions and observations
Returns:
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
- AgentDelegateAction: The next agent to delegate the task to
- Action: The next action to take based on llm response
"""
if self.current_delegate == '':
self.current_delegate = 'study'
@@ -41,9 +41,7 @@ class DelegatorAgent(Agent):
agent='StudyRepoForTaskAgent', inputs={'task': task}
)
# last observation in history should be from the delegate
last_observation = state.history.get_last_observation()
last_observation = state.history[-1][1]
if not isinstance(last_observation, AgentDelegateObservation):
raise Exception('Last observation is not an AgentDelegateObservation')
@@ -82,3 +80,6 @@ class DelegatorAgent(Agent):
)
else:
raise Exception('Invalid delegate state')
def search_memory(self, query: str) -> list[str]:
return []

View File

@@ -7,8 +7,8 @@ from opendevin.events.action import (
Action,
AddTaskAction,
AgentFinishAction,
AgentRecallAction,
AgentRejectAction,
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
@@ -17,6 +17,7 @@ from opendevin.events.action import (
ModifyTaskAction,
)
from opendevin.events.observation import (
AgentRecallObservation,
CmdOutputObservation,
FileReadObservation,
FileWriteObservation,
@@ -29,6 +30,8 @@ from opendevin.llm.llm import LLM
"""
FIXME: There are a few problems this surfaced
* FileWrites seem to add an unintended newline at the end of the file
* command_id is sometimes a number, sometimes a string
* Why isn't the output of the background command split between two steps?
* Browser not working
"""
@@ -36,9 +39,10 @@ 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.
@@ -90,15 +94,29 @@ class DummyAgent(Agent):
],
},
{
'action': BrowseURLAction(url='https://google.com'),
'action': CmdRunAction(command=BACKGROUND_CMD, background=True),
'observations': [
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
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': BrowseInteractiveAction(
browser_actions='goto("https://google.com")'
),
'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=""),
],
@@ -117,16 +135,11 @@ class DummyAgent(Agent):
time.sleep(0.1)
if state.iteration > 0:
prev_step = self.steps[state.iteration - 1]
# a step is (action, observations list)
if 'observations' in prev_step:
# one obs, at most
expected_observations = prev_step['observations']
# check if the history matches the expected observations
hist_events = state.history.get_last_events(len(expected_observations))
hist_start = len(state.history) - len(expected_observations)
for i in range(len(expected_observations)):
hist_obs = event_to_dict(hist_events[i])
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']
@@ -140,7 +153,13 @@ class DummyAgent(Agent):
):
del expected_obs['extras']['command_id']
expected_obs['content'] = ''
if hist_obs != expected_obs:
print('\nactual', hist_obs)
print('\nexpect', expected_obs)
assert (
hist_obs == expected_obs
), f'Expected observation {expected_obs}, got {hist_obs}'
return self.steps[state.iteration]['action']
def search_memory(self, query: str) -> list[str]:
return ['I am a computer.']

View File

@@ -1,16 +0,0 @@
# GPTSwarm Framework
## Introduction
This folder implements the GPTSwarm ([paper](https://arxiv.org/abs/2402.01030), [Original Repo](https://github.com/metauto-ai/GPTSwarm)). For more details, please see paper.
## Reference
```
@article{zhuge2024language,
title={Language Agents as Optimizable Graphs},
author={Zhuge, Mingchen and Wang, Wenyi and Kirsch, Louis and Faccio, Francesco and Khizbullin, Dmitrii and Schmidhuber, Jurgen},
journal={arXiv preprint arXiv:2402.16823},
year={2024}
}
```

View File

@@ -1,5 +0,0 @@
from opendevin.controller.agent import Agent
from .gptswarm_agent import GPTSwarm
Agent.register('GPTSwarmAgent', GPTSwarm)

View File

@@ -1,196 +0,0 @@
import asyncio
import dataclasses
from copy import deepcopy
from typing import Any, Dict, List, Literal
from agenthub.gptswarm_agent.gptswarm_graph import AssistantGraph
from agenthub.gptswarm_agent.prompt import GPTSwarmPromptSet
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
from opendevin.llm.llm import LLM
ENABLE_GITHUB = True
OPENAI_API_KEY = 'sk-proj-****' # TODO: get from environment or config
MessageRole = Literal['system', 'user', 'assistant']
@dataclasses.dataclass()
class Message:
role: MessageRole
content: str
class GPTSwarm(Agent):
VERSION = '1.0'
"""
This is simple revision of GPTSwarm which serve as an assistant agent.
GPTSwarm Paper: https://arxiv.org/abs/2402.16823 (ICML 2024, Oral Presentation)
GPTSwarm Code: https://github.com/metauto-ai/GPTSwarm
"""
def __init__(
self,
llm: LLM,
model_name: str,
) -> None:
"""
Initializes a new instance of the GPTSwarm class.
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm)
self.api_key = OPENAI_API_KEY
self.llm = LLM(model=model_name, api_key=self.api_key)
self.graph = AssistantGraph(domain='gaia', model_name=model_name)
self.prompt_set = GPTSwarmPromptSet()
def reset(self) -> None:
"""
Resets the GPTSwarm Agent.
"""
super().reset()
def step(self, state: State) -> Action:
"""
# TODO: It is stateless now. Find a way to make it stateful.
# NOTE: For the AI assistant, state-based design may introduce more uncertainties.
"""
raise NotImplementedError
async def swarm_run(self, inputs: List[Dict[str, Any]], num_agents=3) -> List[str]:
"""
Run the `run` method of this agent concurrently for `num_agents` times.
# NOTE: This is just a simple self-consistency.
# TODO: should follow original GPTSwarm's graph design to revise.
"""
async def run_single_agent(index):
try:
result = await asyncio.wait_for(self.run(inputs=inputs), timeout=200)
print('-----------------------------------')
print(f'No. {index} Agent complete task..')
logger.info(result[0])
print('-----------------------------------')
return result[0]
except asyncio.TimeoutError:
print(f'No. {index} Agent timed out.')
return None
except Exception as e:
print(f'No. {index} Agent resulted in an error: {e}')
return None
# Create a list of tasks to run concurrently
tasks = [run_single_agent(i) for i in range(num_agents)]
# Run all tasks concurrently and gather the results
agent_answers = await asyncio.gather(*tasks)
# Filter out None results (from timeouts or errors)
agent_answers = [answer for answer in agent_answers if answer is not None]
task = inputs[0]['task']
prompt = self.prompt_set.get_self_consistency(
question=task,
answers=agent_answers,
constraint=self.prompt_set.get_constraint(),
)
messages = [
Message(role='system', content=f'You are a {self.prompt_set.get_role()}.'),
Message(role='user', content=prompt),
]
swarm_ans = self.llm.completion(
messages=[{'role': msg.role, 'content': msg.content} for msg in messages]
)
swarm_ans = swarm_ans.choices[0].message.content
return [swarm_ans]
async def run(
self,
inputs: List[Dict[str, Any]],
max_tries: int = 3,
max_time: int = 600,
return_all_outputs: bool = False,
) -> List[Any]:
def is_node_useful(node):
if node in self.graph.output_nodes:
return True
for successor in node.successors:
if is_node_useful(successor):
return True
return False
useful_node_ids = [
node_id
for node_id, node in self.graph.nodes.items()
if is_node_useful(node)
]
in_degree = {
node_id: len(self.graph.nodes[node_id].predecessors)
for node_id in useful_node_ids
}
zero_in_degree_queue = [
node_id
for node_id, deg in in_degree.items()
if deg == 0 and node_id in useful_node_ids
]
for i, input_node in enumerate(self.graph.input_nodes):
node_input = deepcopy(inputs)
input_node.inputs = [node_input]
while zero_in_degree_queue:
current_node_id = zero_in_degree_queue.pop(0)
current_node = self.graph.nodes[current_node_id]
tries = 0
while tries < max_tries:
try:
await asyncio.wait_for(
self.graph.nodes[current_node_id].execute(), timeout=max_time
)
# TODO: make GPTSwarm stateful in OpenDevin.
# State.inputs = self.graph.nodes[current_node_id].inputs
# State.outputs = self.graph.nodes[current_node_id].outputs
# self.step(State)
except asyncio.TimeoutError:
print(
f'Node {current_node_id} execution timed out, retrying {tries + 1} out of {max_tries}...'
)
except Exception as e:
print(f'Error during execution of node {current_node_id}: {e}')
break
tries += 1
for successor in current_node.successors:
if successor.id in useful_node_ids:
in_degree[successor.id] -= 1
if in_degree[successor.id] == 0:
zero_in_degree_queue.append(successor.id)
final_answers = []
for output_node in self.graph.output_nodes:
output_messages = output_node.outputs
if len(output_messages) > 0 and not return_all_outputs:
final_answer = output_messages[-1].get('output', output_messages[-1])
final_answers.append(final_answer)
else:
for output_message in output_messages:
final_answer = output_message.get('output', output_message)
final_answers.append(final_answer)
if len(final_answers) == 0:
final_answers.append('No answer since there are no inputs provided')
return final_answers
def search_memory(self, query: str) -> list[str]:
raise NotImplementedError('Implement this abstract method')

View File

@@ -1,520 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import ast
import asyncio
import dataclasses
import os
import re
from collections import defaultdict
from pathlib import Path
from typing import Any, List, Literal, Optional
import requests
from pytube import YouTube
from swarm.graph import Graph, Node
from agenthub.gptswarm_agent.prompt import GPTSwarmPromptSet
from opendevin.core.logger import opendevin_logger as logger
from opendevin.llm.llm import LLM
from opendevin.runtime.plugins.agent_skills.agentskills import (
parse_audio,
parse_docx,
parse_image,
parse_latex,
parse_pdf,
parse_pptx,
parse_txt,
parse_video,
)
OPENAI_API_KEY = 'sk-proj-****' # TODO: get from environment or config
SEARCHAPI_API_KEY = '****' # TODO: get from environment or config
MessageRole = Literal['system', 'user', 'assistant']
@dataclasses.dataclass()
class Message:
role: MessageRole
content: str
READER_MAP = {
'.png': parse_image,
'.jpg': parse_image,
'.jpeg': parse_image,
'.gif': parse_image,
'.bmp': parse_image,
'.tiff': parse_image,
'.tif': parse_image,
'.webp': parse_image,
'.mp3': parse_audio,
'.m4a': parse_audio,
'.wav': parse_audio,
'.MOV': parse_video,
'.mp4': parse_video,
'.mov': parse_video,
'.avi': parse_video,
'.mpg': parse_video,
'.mpeg': parse_video,
'.wmv': parse_video,
'.flv': parse_video,
'.webm': parse_video,
'.pptx': parse_pptx,
'.pdf': parse_pdf,
'.docx': parse_docx,
'.tex': parse_latex,
'.txt': parse_txt,
}
class FileReader:
def __init__(self):
self.reader = None # Initial type is None
def set_reader(self, suffix: str):
reader = READER_MAP.get(suffix)
if reader is not None:
self.reader = reader
logger.info(f'Setting Reader to {self.reader.__name__}')
else:
logger.error(f'No reader found for suffix {suffix}')
self.reader = None
def read_file(self, file_path: Path, task: str = 'describe the file') -> str:
suffix = file_path.suffix
self.set_reader(suffix)
if not self.reader:
raise ValueError(f'No reader set for suffix {suffix}')
if self.reader in [parse_image, parse_video]:
file_content = self.reader(file_path, task)
else:
file_content = self.reader(file_path)
logger.info(f'Reading file {file_path} using {self.reader.__name__}')
return file_content
class GenerateQuery(Node):
def __init__(
self,
domain: str = 'gaia',
model_name: Optional[str] = 'gpt-4o-2024-05-13',
operation_description: str = 'Given a question, return what information is needed to answer the question.',
id=None,
):
super().__init__(operation_description, id, True)
self.domain = domain
self.api_key = OPENAI_API_KEY
self.llm = LLM(model=model_name, api_key=self.api_key)
self.prompt_set = GPTSwarmPromptSet()
@property
def node_name(self) -> str:
return self.__class__.__name__
def extract_urls(self, text: str) -> List[str]:
url_pattern = r'https?://[^\s]+'
urls = re.findall(url_pattern, text)
return urls
def is_youtube_url(self, url: str) -> bool:
youtube_regex = (
r'(https?://)?(www\.)?'
r'(youtube|youtu|youtube-nocookie)\.(com|be)/'
r'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})'
)
return bool(re.match(youtube_regex, url))
def _youtube_download(self, url: str) -> str:
try:
video_id = url.split('v=')[-1].split('&')[0]
video_id = video_id.strip()
youtube = YouTube(url)
video_stream = (
youtube.streams.filter(progressive=True, file_extension='mp4')
.order_by('resolution')
.desc()
.first()
)
if not video_stream:
raise ValueError('No suitable video stream found.')
output_dir = 'workspace/tmp'
os.makedirs(output_dir, exist_ok=True)
output_path = f'{output_dir}/{video_id}.mp4'
video_stream.download(output_path=output_dir, filename=f'{video_id}.mp4')
return output_path
except Exception as e:
logger.error(
f'Error downloading video from {url}: {e}'
) # Use logger for error messages
return ''
async def _execute(
self, inputs: Optional[List[dict]] = None, **kwargs
) -> List[dict]:
if inputs is None:
inputs = []
node_inputs = inputs
outputs = []
for input in node_inputs:
urls = self.extract_urls(input['task'])
download_paths = []
for url in urls:
if self.is_youtube_url(url):
download_path = self._youtube_download(url)
if download_path:
download_paths.append(download_path)
if urls:
logger.info(urls)
if download_paths:
logger.info(download_paths)
files = input.get('files', [])
if not isinstance(files, list):
files = []
files.extend(download_paths)
role = self.prompt_set.get_role()
# constraint = self.prompt_set.get_constraint()
prompt = self.prompt_set.get_query_prompt(question=input['task'])
messages = [
Message(role='system', content=f'You are a {role}.'),
Message(role='user', content=prompt),
]
response = self.llm.completion(
messages=[
{'role': msg.role, 'content': msg.content} for msg in messages
]
)
response = response.choices[0].message.content
executions = {
'operation': self.node_name,
'task': input['task'],
'files': files,
'input': input.get('task', None),
'subtask': prompt,
'output': response,
'format': 'natural language',
}
outputs.append(executions)
return outputs
class FileAnalyse(Node):
def __init__(
self,
domain: str = 'gaia',
model_name: Optional[str] = 'gpt-4o-2024-05-13',
operation_description: str = 'Given a question, extract information from a file.',
id=None,
):
super().__init__(operation_description, id, True)
self.domain = domain
self.api_key = OPENAI_API_KEY
self.llm = LLM(model=model_name, api_key=self.api_key)
self.prompt_set = GPTSwarmPromptSet()
self.reader = FileReader()
@property
def node_name(self) -> str:
return self.__class__.__name__
async def _execute(
self, inputs: Optional[List[dict]] = None, **kwargs
) -> List[dict]:
if inputs is None:
inputs = []
node_inputs = inputs
outputs = []
for input in node_inputs:
query = input.get('output', 'Please organize the information of this file.')
files = input.get('files', [])
response = await self.file_analyse(query, files, self.llm)
executions = {
'operation': self.node_name,
'task': input['task'],
'files': files,
'input': query,
'subtask': f'Read the content of ###{files}, use query ###{query}',
'output': response,
'format': 'natural language',
}
outputs.append(executions)
return outputs
async def file_analyse(self, query: str, files: List[str], llm: LLM) -> str:
answer = ''
for file in files:
file_path = Path(file)
if self.reader not in [parse_image, parse_video]:
file_content = self.reader.read_file(file_path)
prompt = self.prompt_set.get_file_analysis_prompt(
query=query, file=file_content
)
messages = [
Message(
role='system',
content=f'You are a {self.prompt_set.get_role()}.',
),
Message(role='user', content=prompt),
]
response = llm.completion(
messages=[
{'role': msg.role, 'content': msg.content} for msg in messages
]
)
answer += response.choices[0].message.content + '\n'
return answer
class WebSearch(Node):
def __init__(
self,
domain: str = 'gaia',
model_name: Optional[str] = 'gpt-4o-2024-05-13',
operation_description: str = 'Given a question, search the web for infomation.',
id=None,
):
super().__init__(operation_description, id, True)
self.domain = domain
self.api_key = OPENAI_API_KEY
self.llm = LLM(model=model_name, api_key=self.api_key)
self.prompt_set = GPTSwarmPromptSet()
@property
def node_name(self) -> str:
return self.__class__.__name__
async def _execute(
self, inputs: Optional[List[dict]] = None, max_keywords: int = 4, **kwargs
) -> List[dict]:
if inputs is None:
inputs = []
node_inputs = inputs
outputs = []
for input in node_inputs:
task = input['task']
query = input['output']
prompt = self.prompt_set.get_websearch_prompt(question=task, query=query)
messages = [
Message(
role='system', content=f'You are a {self.prompt_set.get_role()}.'
),
Message(role='user', content=prompt),
]
generated_quires = self.llm.completion(
messages=[
{'role': msg.role, 'content': msg.content} for msg in messages
]
)
generated_quires = generated_quires.choices[0].message.content
generated_quires = generated_quires.split(',')[:max_keywords]
logger.info(f'The search keywords include: {generated_quires}')
search_results = [self.web_search(query) for query in generated_quires]
logger.info(f'The search results: {str(search_results)[:100]}...')
distill_prompt = self.prompt_set.get_distill_websearch_prompt(
question=input['task'], query=query, results='.\n'.join(search_results)
)
messages = [
Message(
role='system', content=f'You are a {self.prompt_set.get_role()}.'
),
Message(role='user', content=distill_prompt),
]
response = self.llm.completion(
messages=[
{'role': msg.role, 'content': msg.content} for msg in messages
]
)
response = response.choices[0].message.content
executions = {
'operation': self.node_name,
'task': task,
'files': input.get('files', []),
'input': query,
'subtask': distill_prompt,
'output': response,
'format': 'natural language',
}
outputs.append(executions)
return outputs
def web_search(self, query: str, item_num: int = 3) -> str:
url = 'https://www.searchapi.io/api/v1/search'
params = {
'engine': 'google',
'q': query,
'api_key': SEARCHAPI_API_KEY, # os.getenv("SEARCHAPI_API_KEY")
}
response = ast.literal_eval(requests.get(url, params=params).text)
if (
'knowledge_graph' in response.keys()
and 'description' in response['knowledge_graph'].keys()
):
return response['knowledge_graph']['description']
if (
'organic_results' in response.keys()
and len(response['organic_results']) > 0
):
snippets = []
for res in response['organic_results'][:item_num]:
if 'snippet' in res:
snippets.append(res['snippet'])
return '\n'.join(snippets)
return ' '
class CombineAnswer(Node):
def __init__(
self,
domain: str = 'gaia',
model_name: Optional[str] = 'gpt-4o-2024-05-13',
operation_description: str = 'Combine multiple inputs into one.',
max_token: int = 500,
id=None,
):
super().__init__(operation_description, id, True)
self.domain = domain
self.max_token = max_token
self.api_key = OPENAI_API_KEY
self.llm = LLM(model=model_name, api_key=self.api_key)
self.prompt_set = GPTSwarmPromptSet()
self.materials: defaultdict[str, str] = defaultdict(str)
@property
def node_name(self) -> str:
return self.__class__.__name__
async def _execute(
self, inputs: Optional[List[Any]] = None, **kwargs
) -> List[dict]:
if inputs is None:
inputs = []
node_inputs = inputs
role = self.prompt_set.get_role()
constraint = self.prompt_set.get_constraint()
self.materials = defaultdict(str)
for input in node_inputs:
operation = input.get('operation')
if operation:
self.materials[operation] += f'{input.get("output", "")}\n'
self.materials['task'] = input.get('task')
question = self.prompt_set.get_combine_materials(self.materials)
prompt = self.prompt_set.get_answer_prompt(question=question)
messages = [
Message(role='system', content=f'You are a {role}. {constraint}'),
Message(role='user', content=prompt),
]
response = self.llm.completion(
messages=[{'role': msg.role, 'content': msg.content} for msg in messages]
)
response = response.choices[0].message.content
executions = {
'operation': self.node_name,
'task': self.materials['task'],
'files': self.materials['files']
if isinstance(self.materials['files'], str)
else ', '.join(self.materials['files']),
'input': node_inputs,
'subtask': prompt,
'output': response,
'format': 'natural language',
}
return [executions]
class AssistantGraph(Graph):
def build_graph(self):
query = GenerateQuery(self.domain, self.model_name)
file_analysis = FileAnalyse(self.domain, self.model_name)
web_search = WebSearch(self.domain, self.model_name)
query.add_successor(file_analysis)
query.add_successor(web_search)
combine = CombineAnswer(self.domain, self.model_name)
file_analysis.add_successor(combine)
web_search.add_successor(combine)
self.input_nodes = [query]
self.output_nodes = [combine]
self.add_node(query)
self.add_node(file_analysis)
self.add_node(web_search)
self.add_node(combine)
if __name__ == '__main__':
# # test node
# task = 'What is the text representation of the last digit of twelve squared?'
# inputs = [{'task': task}]
# query_instance = GenerateQuery()
# query = asyncio.run(query_instance._execute(inputs))
# print(query)
# task = 'What is the text representation of the last digit of twelve squared?'
# inputs = [
# {
# 'task': 'How can researchers ensure AGI development is both safe and ethical while avoiding societal biases and inequalities?',
# 'files': ['agi.txt'],
# }
# ]
# file_instance = FileAnalyse()
# file_info = asyncio.run(file_instance._execute(inputs))
# print(file_info)
# task = 'What is the text representation of the last digit of twelve squared?'
# inputs = [
# {
# 'task': 'How can researchers ensure AGI development is both safe and ethical while avoiding societal biases and inequalities?'
# }
# ]
# search_instance = WebSearch()
# search_info = asyncio.run(search_instance._execute(inputs))
# print(search_info)
assistant_graph = AssistantGraph(domain='gaia', model_name='gpt-4o-2024-05-13')
# test graph
assistant_graph.build_graph()
inputs = [
{
'task': 'How can researchers ensure AGI development is both safe and ethical while avoiding societal biases and inequalities?',
'files': ['agi.txt'],
}
]
outputs = asyncio.run(assistant_graph.run(inputs))
print(outputs)

View File

@@ -1,129 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Any, Dict
class GPTSwarmPromptSet:
"""
GPTSwarmPromptSet provides a collection of static methods to generate prompts
for a general AI assistant. These prompts cover various tasks like answering questions,
performing web searches, analyzing files, and reflecting on tasks.
"""
@staticmethod
def get_role():
return 'a general AI assistant'
@staticmethod
def get_constraint():
return (
'I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER]. '
'YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. '
"If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. "
"If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. "
'If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string. '
)
@staticmethod
def get_format():
return 'natural language'
@staticmethod
def get_answer_prompt(question):
return f'{question}'
@staticmethod
def get_query_prompt(question):
return (
'# Information Gathering for Question Resolution\n\n'
'Evaluate if additional information is needed to answer the question. '
'If a web search or file analysis is necessary, outline specific clues or details to be searched for.\n\n'
f'## ❓ Target Question:\n{question}\n\n'
'## 🔍 Clues for Investigation:\n'
'Identify critical clues and concepts within the question that are essential for finding the answer.\n'
)
@staticmethod
def get_file_analysis_prompt(query, file):
return (
'# File Analysis Task\n\n'
f'## 🔍 Information Extraction Objective:\n---\n{query}\n---\n\n'
f'## 📄 File Under Analysis:\n---\n{file}\n---\n\n'
'## 📝 Instructions:\n'
'1. Identify the key sections in the file relevant to the query.\n'
'2. Extract and summarize the necessary information from these sections.\n'
'3. Ensure the response is focused and directly addresses the query.\n'
"Example: 'Identify the main theme in the text.'"
)
@staticmethod
def get_websearch_prompt(question, query):
return (
'# Web Search Task\n\n'
f'## Original Question: \n---\n{question}\n---\n\n'
f'## 🔍 Targeted Search Objective:\n---\n{query}\n---\n\n'
'## 🌐 Simplified Search Instructions:\n'
'Generate three specific search queries directly related to the original question. Each query should focus on key terms from the question. Format the output as a comma-separated list.\n'
"For example, if the question is 'Who will be the next US president?', your queries could be: 'US presidential candidates, current US president, next US president'.\n"
"Remember to format the queries as 'query1, query2, query3'."
)
@staticmethod
def get_distill_websearch_prompt(question, query, results):
return (
'# Summarization of Search Results\n\n'
f'## Original question: \n---\n{question}\n---\n\n'
f'## 🔍 Required Information for Summary:\n---\n{query}\n---\n\n'
f'## 🌐 Analyzed Search Results:\n---\n{results}\n---\n\n'
'## 📝 Instructions for Summarization:\n'
'1. Review the provided search results and identify the most relevant information related to the question and query.\n'
'2. Extract and highlight the key findings, facts, or data points from these results.\n'
'3. Organize the summarized information in a coherent and logical manner.\n'
'4. Ensure the summary is concise and directly addresses the query, avoiding extraneous details.\n'
'5. If the information from web search is useless, directly answer: "No useful information from WebSearch".\n'
)
@staticmethod
def get_combine_materials(materials: Dict[str, Any], avoid_vague=True) -> str:
question = materials.get('task', 'No problem provided')
for key, value in materials.items():
if 'No useful information from WebSearch' in value:
continue
value = value.strip('\n').strip()
if key != 'task' and value:
question += (
f'\n\nReference information for {key}:'
+ '\n----------------------------------------------\n'
+ f'{value}'
+ '\n----------------------------------------------\n\n'
)
if avoid_vague:
question += (
'\nProvide a specific answer. For questions with known answers, ensure to provide accurate and factual responses. '
+ "Avoid vague responses or statements like 'unable to...' that don't contribute to a definitive answer. "
+ "For example: if a question asks 'who will be the president of America', and the answer is currently unknown, you could suggest possibilities like 'Donald Trump', or 'Biden'. However, if the answer is known, provide the correct information."
)
return question
@staticmethod
def get_self_consistency(question: str, answers: list, constraint: str) -> str:
formatted_answers = '\n'.join(
[f'Answer {index + 1}: {answer}' for index, answer in enumerate(answers)]
)
return (
'# Self-Consistency Evaluation Task\n\n'
f'## 🤔 Question for Review:\n---\n{question}\n---\n\n'
f'## 💡 Reviewable Answers:\n---\n{formatted_answers}\n---\n\n'
'## 📋 Instructions for Selection:\n'
'1. Read each answer and assess how it addresses the question.\n'
"2. Compare the answers for their adherence to the given question's criteria and logical coherence.\n"
"3. Identify the answer that best aligns with the question's requirements and is the most logically consistent.\n"
"4. Ignore the candidate answers if they do not give a direct answer, for example, using 'unable to ...', 'as an AI ...'.\n"
'5. Copy the most suitable answer as it is, without modification, to maintain its original form.\n'
f'6. Adhere to the constraints: {constraint}.\n'
'Note: If no answer fully meets the criteria, choose and copy the one that is closest to the requirements.'
)

View File

@@ -12,6 +12,3 @@ in the following structure:
Note that `prompt.md` could use jinja2 template syntax. During runtime, `prompt.md`
is loaded and rendered, and used together with `agent.yaml` to initialize a
micro-agent.
Micro-agents can be used independently. You can also use `ManagerAgent` which knows
how to coordinate the agents and collaboratively finish a task.

View File

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

View File

@@ -0,0 +1,2 @@
* `kill` - kills a background command
* `command_id` - the ID of the background command to kill

View File

@@ -1,2 +1,2 @@
* `reject` - reject the task. Arguments:
* `outputs` - a dictionary with only a `reason` attribute
* `outputs` - a dictionary representing the outputs of your task, if any

View File

@@ -1,2 +1,3 @@
* `run` - runs a command on the command line in a Linux shell. Arguments:
* `command` - the command to run
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.

View File

@@ -2,13 +2,11 @@ from jinja2 import BaseLoader, Environment
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import config
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.llm.llm import LLM
from opendevin.memory.history import ShortTermHistory
from .instructions import instructions
from .registry import all_microagents
@@ -29,31 +27,19 @@ def to_json(obj, **kwargs):
return json.dumps(obj, **kwargs)
def history_to_json(history: ShortTermHistory, max_events=20, **kwargs):
def history_to_json(obj, **kwargs):
"""
Serialize and simplify history to str format
"""
# TODO: get agent specific llm config
llm_config = config.get_llm_config()
max_message_chars = llm_config.max_message_chars
processed_history = []
event_count = 0
for event in history.get_events(reverse=True):
if event_count >= max_events:
break
processed_history.append(event_to_memory(event, max_message_chars))
event_count += 1
# history is in reverse order, let's fix it
processed_history.reverse()
return json.dumps(processed_history, **kwargs)
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)
class MicroAgent(Agent):
VERSION = '1.0'
prompt = ''
agent_definition: dict = {}
@@ -66,16 +52,21 @@ class MicroAgent(Agent):
del self.delegates[self.agent_definition['name']]
def step(self, state: State) -> Action:
latest_user_message = state.get_current_user_intent()
prompt = self.prompt_template.render(
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(),
latest_user_message=latest_user_message,
)
messages = [{'content': prompt, 'role': 'user'}]
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]:
return []

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

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 }}
{{ latest_user_message }}
{% if state.inputs.summary %}
Here's a summary of the codebase, as it relates to this task:
@@ -21,7 +21,7 @@ Do NOT finish until you have completed the tasks.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
{{ history_to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}

View File

@@ -3,7 +3,7 @@
CommitWriterAgent can help write git commit message. Example:
```bash
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_BOX_TYPE="ssh" \
WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_TYPE="exec" \
poetry run python opendevin/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
```

View File

@@ -3,4 +3,3 @@ description: "Write a git commit message for files in the git staging area"
inputs: {}
outputs:
answer: string
reason: string

View File

@@ -11,16 +11,14 @@ 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 first use `git status` to check whether it's a valid git repo and there
is diff in the staging area. If not, please call the `reject` action.
If it is a valid git repo and there is diff in the staging area, 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.
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, max_events=20) }}
{{ history_to_json(state.history[-10:]) }}
If the last item in the history is an error, you should try to fix it.

View File

@@ -3,6 +3,4 @@ description: Delegates tasks to microagents based on their area of expertise
generates: Action
inputs:
task: string
outputs:
summary: string # if finished
reason: string # if rejected
outputs: {}

View File

@@ -7,16 +7,6 @@ can do the actual work. A description of each agent is provided below. You MUST
select one of the delegates below to move towards accomplishing the task, and you MUST
provide the correct inputs for the delegate you select.
Note: the delegated agent either returns "finish" or "reject".
- If the action is "finish", but the full task is not done yet, you should
continue to delegate to one of the agents below to until the full task is finished.
- If the action is "reject", it means the delegated agent is not capable of the
task you send to. You should revisit the input you send to the delegate, and consider
whether any other delegate would be able to solve the task. If you cannot find
a proper delegate agent, or the delegate attempts keep failing, call the `reject`
action. In `reason` attribute, make sure you include your attempts (e.g. what agent
you have delegated to, and why they failed).
## Agents
{% for name, details in delegates.items() %}
### {{ name }}
@@ -27,15 +17,11 @@ you have delegated to, and why they failed).
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
If the last item in the history is an error, you should try to fix it. If you
cannot fix it, call the `reject` action.
{{ history_to_json(state.history[-10:]) }}
## Available Actions
{{ instructions.actions.delegate }}
{{ instructions.actions.finish }}
{{ instructions.actions.reject }}
## Format
{{ instructions.format.action }}

View File

@@ -1,7 +1,7 @@
# Task
You are a brilliant mathematician and programmer. You've been given the following problem to solve:
`{{ state.inputs.task }}`
{{ latest_user_message }}
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, max_events=20) }}
{{ history_to_json(state.history[-10:]) }}
If the last item in the history is an error, you should try to fix it.

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 }}
{{ latest_user_message }}
You must:
* Investigate the existing migrations to understand the current schema
@@ -18,7 +18,7 @@ You may take any of the following actions:
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
{{ history_to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}

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:

View File

@@ -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, max_events=20) }}
{{ history_to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}

View File

@@ -1,11 +1,9 @@
# 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.
{{ latest_user_message }}
## Available Actions
{{ instructions.actions.run }}
@@ -13,50 +11,15 @@ might be helpful for your software engineering team.
{{ instructions.actions.message }}
{{ 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, 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 should call the `finish` action.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
{{ history_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"
}
}
USER:
OBSERVATION:
[]
ASSISTANT:
{
"action": "finish",
"args": {
"outputs": {
"summary": "The codebase appears to be empty. Engineers should start everything from scratch."
}
}
}
--- END OF EXAMPLE ---

View File

@@ -1,6 +1,5 @@
name: TypoFixerAgent
description: Fixes typos in files in the current working directory
inputs:
task: string
inputs: {}
outputs:
summary: string

View File

@@ -1,13 +1,5 @@
# 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:
You are a proofreader tasked with fixing typos in the files in your current working directory. Your goal is to:
1. Scan the files for typos
2. Overwrite the files with the typos fixed
3. Provide a summary of the typos fixed
@@ -21,17 +13,17 @@ To achieve this goal, you should:
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.
2. Use the `think` 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.
4. Use the `think` 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, max_events=10) }}
{{ history_to_json(state.history[-5:]) }}
## Format
{{ instructions.format.action }}

View File

@@ -2,10 +2,9 @@
You are a quality assurance engineer. Another engineer has made changes to the
codebase which are supposed to solve this task:
{{ state.inputs.task }}
{{ latest_user_message }}
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 }}
@@ -22,7 +21,7 @@ explaining what the problem is.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
{{ history_to_json(state.history[-10:]) }}
## Format
{{ instructions.format.action }}

View File

@@ -1,6 +1,4 @@
import agenthub.monologue_agent.utils.prompts as prompts
from agenthub.monologue_agent.response_parser import MonologueResponseParser
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
@@ -8,6 +6,7 @@ from opendevin.core.exceptions import AgentNoInstructionError
from opendevin.core.schema import ActionType
from opendevin.events.action import (
Action,
AgentRecallAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
@@ -16,6 +15,7 @@ from opendevin.events.action import (
NullAction,
)
from opendevin.events.observation import (
AgentRecallObservation,
BrowserOutputObservation,
CmdOutputObservation,
FileReadObservation,
@@ -25,14 +25,61 @@ from opendevin.events.observation import (
from opendevin.events.serialization.event import event_to_memory
from opendevin.llm.llm import LLM
from opendevin.memory.condenser import MemoryCondenser
from opendevin.runtime.tools import RuntimeTool
from opendevin.memory.history import ShortTermHistory
if config.get_agent_config('MonologueAgent').memory_enabled:
if config.agent.memory_enabled:
from opendevin.memory.memory import LongTermMemory
MAX_TOKEN_COUNT_PADDING = 512
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!',
'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.
@@ -40,21 +87,57 @@ class MonologueAgent(Agent):
"""
_initialized = False
initial_thoughts: list[dict[str, str]]
monologue: ShortTermHistory
memory: 'LongTermMemory | None'
memory_condenser: MemoryCondenser
runtime_tools: list[RuntimeTool] = [RuntimeTool.BROWSER]
response_parser = MonologueResponseParser()
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: 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 (
'args' in event_dict
and 'output' in event_dict['args']
and len(event_dict['args']['output']) > MAX_OUTPUT_LENGTH
):
event_dict['args']['output'] = (
event_dict['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
)
self.monologue.add_event(event_dict)
if self.memory is not None:
self.memory.add_event(event_dict)
# Test monologue token length
prompt = prompts.get_request_action_prompt(
'',
self.monologue.get_events(),
[],
)
messages = [{'content': prompt, 'role': 'user'}]
token_count = self.llm.get_token_count(messages)
if token_count + MAX_TOKEN_COUNT_PADDING > self.llm.max_input_tokens:
prompt = prompts.get_summarize_monologue_prompt(self.monologue.events)
summary_response = self.memory_condenser.condense(
summarize_prompt=prompt, llm=self.llm
)
self.monologue.events = prompts.parse_summary_response(summary_response)
def _initialize(self, task: str):
"""
Utilizes the INITIAL_THOUGHTS list to give the agent a context for its capabilities
@@ -63,7 +146,7 @@ class MonologueAgent(Agent):
Will execute again when called after reset.
Parameters:
- task: The initial goal statement provided by the user
- task (str): The initial goal statement provided by the user
Raises:
- AgentNoInstructionError: If task is not provided
@@ -75,8 +158,8 @@ class MonologueAgent(Agent):
if task is None or task == '':
raise AgentNoInstructionError()
self.initial_thoughts = []
if config.get_agent_config('MonologueAgent').memory_enabled:
self.monologue = ShortTermHistory()
if config.agent.memory_enabled:
self.memory = LongTermMemory()
else:
self.memory = None
@@ -87,9 +170,6 @@ class MonologueAgent(Agent):
self._initialized = True
def _add_initial_thoughts(self, task):
max_message_chars = config.get_llm_config_from_agent(
'MonologueAgent'
).max_message_chars
previous_action = ''
for thought in INITIAL_THOUGHTS:
thought = thought.replace('$TASK', task)
@@ -101,13 +181,13 @@ class MonologueAgent(Agent):
)
elif previous_action == ActionType.READ:
observation = FileReadObservation(content=thought, path='')
elif previous_action == ActionType.RECALL:
observation = AgentRecallObservation(content=thought, memories=[])
elif previous_action == ActionType.BROWSE:
observation = BrowserOutputObservation(
content=thought, url='', screenshot=''
)
self.initial_thoughts.append(
event_to_memory(observation, max_message_chars)
)
self._add_event(event_to_memory(observation))
previous_action = ''
else:
action: Action = NullAction()
@@ -124,13 +204,17 @@ class MonologueAgent(Agent):
path = thought.split('READ ')[1]
action = FileReadAction(path=path)
previous_action = ActionType.READ
elif thought.startswith('RECALL'):
query = thought.split('RECALL ')[1]
action = AgentRecallAction(query=query)
previous_action = ActionType.RECALL
elif thought.startswith('BROWSE'):
url = thought.split('BROWSE ')[1]
action = BrowseURLAction(url=url)
previous_action = ActionType.BROWSE
else:
action = MessageAction(thought)
self.initial_thoughts.append(event_to_memory(action, max_message_chars))
self._add_event(event_to_memory(action))
def step(self, state: State) -> Action:
"""
@@ -142,48 +226,43 @@ class MonologueAgent(Agent):
Returns:
- Action: The next action to take based on LLM response
"""
max_message_chars = config.get_llm_config_from_agent(
'MonologueAgent'
).max_message_chars
goal = state.get_current_user_intent()
self._initialize(goal)
for prev_action, obs in state.updated_info:
self._add_event(event_to_memory(prev_action))
self._add_event(event_to_memory(obs))
recent_events: list[dict[str, str]] = []
state.updated_info = []
# add the events from state.history
for event in state.history.get_events():
recent_events.append(event_to_memory(event, max_message_chars))
# add the last messages to long term memory
if self.memory is not None:
last_action = state.history.get_last_action()
last_observation = state.history.get_last_observation()
# this should still work
# we will need to do this differently: find out if there really is an action or an observation in this step
if last_action:
self.memory.add_event(event_to_memory(last_action, max_message_chars))
if last_observation:
self.memory.add_event(
event_to_memory(last_observation, max_message_chars)
)
# the action prompt with initial thoughts and recent events
prompt = prompts.get_request_action_prompt(
goal, self.initial_thoughts, recent_events
goal,
self.monologue.get_events(),
state.background_commands_obs,
)
messages: list[dict[str, str]] = [
{'role': 'user', 'content': prompt},
]
# format all as a single message, a monologue
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
action = self.response_parser.parse(resp)
action_resp = resp['choices'][0]['message']['content']
state.num_of_chars += len(prompt) + len(action_resp)
action = prompts.parse_action_response(action_resp)
self.latest_action = action
return action
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.
Parameters:
- 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
"""
if self.memory is None:
return []
return self.memory.search(query)
def reset(self) -> None:
super().reset()

View File

@@ -1,38 +0,0 @@
from opendevin.controller.action_parser import ResponseParser
from opendevin.core.utils import json
from opendevin.events.action import (
Action,
)
from opendevin.events.serialization.action import action_from_dict
class MonologueResponseParser(ResponseParser):
def __init__(self):
super().__init__()
def parse(self, response: str) -> Action:
action_str = self.parse_response(response)
return self.parse_action(action_str)
def parse_response(self, response) -> str:
# get the next action from the response
return response['choices'][0]['message']['content']
def parse_action(self, action_str: str) -> Action:
"""
Parses a string to find an action within it
Parameters:
- response (str): The string to be parsed
Returns:
- Action: The action that was found in the response string
"""
# attempt to load the JSON dict from the response
action_dict = json.loads(action_str)
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)

View File

@@ -3,6 +3,9 @@ from opendevin.core.utils import json
from opendevin.events.action import (
Action,
)
from opendevin.events.observation import (
CmdOutputObservation,
)
from opendevin.events.serialization.action import action_from_dict
ACTION_PROMPT = """
@@ -15,6 +18,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:
@@ -29,18 +33,25 @@ Here are the possible actions:
* `content` - the content to write to the file
* `run` - runs a command. Arguments:
* `command` - the command to run
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
* `kill` - kills a background command
* `command_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:
* `owner` - the owner of the repo to push to
* `repo` - the name of the repo to push to
* `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
* `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
You MUST take time to think in between read, write, run, browse, and push actions--do this with the `message` action.
%(background_commands)s
You MUST take time to think in between read, write, run, browse, push, and recall actions--do this with the `message` action.
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.
@@ -81,43 +92,6 @@ 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.',
"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]):
"""
@@ -134,7 +108,7 @@ 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] = [],
):
"""
Gets the action prompt formatted with appropriate values.
@@ -142,38 +116,42 @@ def get_request_action_prompt(
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
Returns:
- str: Formatted prompt string with hint, task, monologue, and background commands included
- str: Formatted prompt string with hint, task, monologue, and background included
"""
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'] == 'message':
if latest_thought['args']['content'].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:
bg_commands_message = 'The following commands are running in the background:'
for command_obs in background_commands_obs:
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.'
user = 'opendevin' if config.run_as_devin else 'root'
monologue = thoughts + recent_events
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,
'timeout': config.sandbox_timeout,
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
}

View File

@@ -1,21 +1,16 @@
from agenthub.monologue_agent.response_parser import MonologueResponseParser
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.events.action import Action, AgentFinishAction
from opendevin.llm.llm import LLM
from opendevin.runtime.tools import RuntimeTool
from .prompt import get_prompt
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.
"""
runtime_tools: list[RuntimeTool] = [RuntimeTool.BROWSER]
response_parser = MonologueResponseParser()
def __init__(self, llm: LLM):
"""
@@ -48,4 +43,10 @@ class PlannerAgent(Agent):
prompt = get_prompt(state)
messages = [{'content': prompt, 'role': 'user'}]
resp = self.llm.completion(messages=messages)
return self.response_parser.parse(resp)
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]:
return []

View File

@@ -1,5 +1,4 @@
from opendevin.controller.state.state import State
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import ActionType
from opendevin.core.utils import json
@@ -7,10 +6,13 @@ from opendevin.events.action import (
Action,
NullAction,
)
from opendevin.events.observation import (
NullObservation,
)
from opendevin.events.serialization.action import action_from_dict
from opendevin.events.serialization.event import event_to_memory
HISTORY_SIZE = 20
HISTORY_SIZE = 10
prompt = """
# Task
@@ -75,6 +77,9 @@ It must be an object, and it must contain two fields:
* `content` - the content to write to the file
* `run` - runs a command on the command line in a Linux shell. Arguments:
* `command` - the command to run
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
* `kill` - kills a background command
* `command_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:
@@ -89,7 +94,7 @@ It must be an object, and it must contain two fields:
* `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, and browse actions--do this with the `message` action.
You MUST take time to think in between read, write, run, browse, and recall actions--do this with the `message` action.
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.
@@ -109,6 +114,7 @@ def get_hint(latest_action_id: str) -> str:
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.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.',
ActionType.SUMMARIZE: '',
@@ -128,32 +134,19 @@ def get_prompt(state: State) -> str:
Returns:
- str: The formatted string prompt with historical values
"""
max_message_chars = config.get_llm_config_from_agent(
'PlannerAgent'
).max_message_chars
# the plan
plan_str = json.dumps(state.root_task.to_dict(), indent=2)
# the history
sub_history = state.history[-HISTORY_SIZE:]
history_dicts = []
latest_action: Action = NullAction()
# retrieve the latest HISTORY_SIZE events
for event_count, event in enumerate(state.history.get_events(reverse=True)):
if event_count >= HISTORY_SIZE:
break
if latest_action == NullAction() and isinstance(event, Action):
latest_action = event
history_dicts.append(event_to_memory(event, max_message_chars))
# history_dicts is in reverse order, lets fix it
history_dicts.reverse()
# and get it as a JSON string
for action, observation in sub_history:
if not isinstance(action, NullAction):
history_dicts.append(event_to_memory(action))
latest_action = action
if not isinstance(observation, NullObservation):
observation_dict = event_to_memory(observation)
history_dicts.append(observation_dict)
history_str = json.dumps(history_dicts, indent=2)
# the plan status
current_task = state.root_task.get_current_task()
if current_task is not None:
plan_status = f"You're currently working on this task:\n{current_task.goal}."
@@ -161,15 +154,9 @@ def get_prompt(state: State) -> str:
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."
# the hint, based on the last action
hint = get_hint(event_to_memory(latest_action, max_message_chars).get('action', ''))
logger.info('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
# the last relevant user message (the task)
hint = get_hint(event_to_memory(latest_action).get('action', ''))
logger.info('HINT:\n' + hint, extra={'msg_type': 'INFO'})
task = state.get_current_user_intent()
# finally, fill in the prompt
return prompt % {
'task': task,
'plan': plan_str,
@@ -182,8 +169,10 @@ def get_prompt(state: State) -> str:
def parse_response(response: str) -> Action:
"""
Parses the model output to find a valid action to take
Parameters:
- response (str): A response from the model that potentially contains an Action.
Returns:
- Action: A valid next action to perform from model output
"""

View File

@@ -1,203 +0,0 @@
###################### OpenDevin Configuration Example ######################
#
# All settings have default values, so you only need to uncomment and
# modify what you want to change
# The fields within each section are sorted in alphabetical order.
#
##############################################################################
#################################### Core ####################################
# General core configurations
##############################################################################
[core]
# API key for E2B
#e2b_api_key = ""
# Base path for the workspace
workspace_base = "./workspace"
# Cache directory path
#cache_dir = "/tmp/cache"
# Debugging enabled
#debug = false
# Disable color in terminal output
#disable_color = false
# Enable auto linting after editing
#enable_auto_lint = false
# Enable saving and restoring the session when run from CLI
#enable_cli_session = false
# File store path
#file_store_path = "/tmp/file_store"
# File store type
#file_store = "memory"
# List of allowed file extensions for uploads
#file_uploads_allowed_extensions = [".*"]
# Maximum file size for uploads, in megabytes
#file_uploads_max_file_size_mb = 0
# Maximum budget per task, 0.0 means no limit
#max_budget_per_task = 0.0
# Maximum number of iterations
#max_iterations = 100
# Path to mount the workspace in the sandbox
#workspace_mount_path_in_sandbox = "/workspace"
# Path to mount the workspace
#workspace_mount_path = ""
# Path to rewrite the workspace mount path to
#workspace_mount_rewrite = ""
# Persist the sandbox
persist_sandbox = false
# Run as devin
#run_as_devin = true
# Runtime environment
#runtime = "server"
# SSH hostname for the sandbox
#ssh_hostname = "localhost"
# SSH password for the sandbox
#ssh_password = ""
# SSH port for the sandbox
#ssh_port = 63710
# Use host network
#use_host_network = false
# Name of the default agent
#default_agent = "CodeActAgent"
#################################### LLM #####################################
# Configuration for LLM models (group name starts with 'llm')
# use 'llm' for the default LLM config
##############################################################################
[llm]
# AWS access key ID
#aws_access_key_id = ""
# AWS region name
#aws_region_name = ""
# AWS secret access key
#aws_secret_access_key = ""
# API key to use
api_key = "your-api-key"
# API base URL
#base_url = ""
# API version
#api_version = ""
# Cost per input token
#input_cost_per_token = 0.0
# Cost per output token
#output_cost_per_token = 0.0
# Custom LLM provider
#custom_llm_provider = ""
# Embedding API base URL
#embedding_base_url = ""
# Embedding deployment name
#embedding_deployment_name = ""
# Embedding model to use
embedding_model = ""
# Maximum number of characters in an observation's content
#max_message_chars = 10000
# Maximum number of input tokens
#max_input_tokens = 0
# Maximum number of output tokens
#max_output_tokens = 0
# Model to use
model = "gpt-4o"
# Number of retries to attempt
#num_retries = 5
# Retry maximum wait time
#retry_max_wait = 60
# Retry minimum wait time
#retry_min_wait = 3
# Temperature for the API
#temperature = 0.0
# Timeout for the API
#timeout = 0
# Top p for the API
#top_p = 0.5
[llm.gpt3]
# API key to use
api_key = "your-api-key"
# Model to use
model = "gpt-3.5"
#################################### Agent ###################################
# Configuration for agents (group name starts with 'agent')
# Use 'agent' for the default agent config
# otherwise, group name must be `agent.<agent_name>` (case-sensitive), e.g.
# agent.CodeActAgent
##############################################################################
[agent]
# Memory enabled
#memory_enabled = false
# Memory maximum threads
#memory_max_threads = 2
# LLM config group to use
#llm_config = 'llm'
[agent.RepoExplorerAgent]
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
# useful when an agent doesn't demand high quality but uses a lot of tokens
llm_config = 'gpt3'
#################################### Sandbox ###################################
# Configuration for the sandbox
##############################################################################
[sandbox]
# Sandbox timeout in seconds
#timeout = 120
# Sandbox type (ssh, e2b, local)
#box_type = "ssh"
# Sandbox user ID
#user_id = 1000
# Container image to use for the sandbox
#container_image = "ghcr.io/opendevin/sandbox:main"
#################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation
# plugin for the available options
##############################################################################

View File

@@ -1,12 +1,11 @@
# Docker Containers
Each folder here contains a Dockerfile, and a config.sh describing how to build
the images and where to push them. These images are built and pushed in GitHub Actions
the image and where to push it. These are images are built and pushed in GitHub Actions
by the `ghcr.yml` workflow.
## Building Manually
```bash
```
docker build -f containers/app/Dockerfile -t opendevin .
docker build -f containers/sandbox/Dockerfile -t sandbox .
```

View File

@@ -10,7 +10,7 @@ RUN npm ci
COPY ./frontend ./
RUN npm run make-i18n && npm run build
FROM python:3.12.3-slim as backend-builder
FROM python:3.12-slim as backend-builder
WORKDIR /app
ENV PYTHONPATH '/app'
@@ -28,7 +28,7 @@ COPY ./pyproject.toml ./poetry.lock ./
RUN touch README.md
RUN poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR
FROM python:3.12.3-slim as runtime
FROM python:3.12-slim as runtime
WORKDIR /app
@@ -53,7 +53,6 @@ RUN useradd -l -m -u $OPENDEVIN_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
USER opendevin
ENV VIRTUAL_ENV=/app/.venv \

View File

@@ -26,17 +26,13 @@ if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
"$@"
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. 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
echo "Failed to create user enduser with id $SANDBOX_USER_ID for a second time. Exiting."
exit 1
fi
fi
usermod -aG app enduser
@@ -50,7 +46,6 @@ else
groupadd -g $DOCKER_SOCKET_GID docker
fi
mkdir -p /home/enduser/.cache/huggingface/hub/
mkdir -p /home/enduser/.cache/ms-playwright/
mv /home/opendevin/.cache/ms-playwright/ /home/enduser/.cache/

View File

@@ -3,68 +3,65 @@ set -eo pipefail
image_name=$1
org_name=$2
platform=$3
push=0
if [[ $3 == "--push" ]]; then
push=1
fi
echo "Building: $image_name for platform: $platform"
echo -e "Building: $image_name"
tags=()
OPEN_DEVIN_BUILD_VERSION="dev"
cache_tag_base="buildcache"
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
major_version=$(echo "$GITHUB_REF_NAME" | cut -d. -f1)
minor_version=$(echo "$GITHUB_REF_NAME" | cut -d. -f1,2)
tags+=("$major_version" "$minor_version")
tags+=("latest")
major_version=$(echo $GITHUB_REF_NAME | cut -d. -f1)
minor_version=$(echo $GITHUB_REF_NAME | cut -d. -f1,2)
tags+=($major_version $minor_version)
fi
sanitized=$(echo "$GITHUB_REF_NAME" | sed 's/[^a-zA-Z0-9.-]\+/-/g')
sanitized=$(echo $GITHUB_REF_NAME | sed 's/[^a-zA-Z0-9.-]\+/-/g')
OPEN_DEVIN_BUILD_VERSION=$sanitized
tag=$(echo "$sanitized" | tr '[:upper:]' '[:lower:]') # lower case is required in tagging
tags+=("$tag")
cache_tag+="-${sanitized}"
tags+=($sanitized)
fi
echo "Tags: ${tags[@]}"
if [[ "$image_name" == "opendevin" ]]; then
dir="./containers/app"
else
dir="./containers/$image_name"
fi
if [[ ! -f "$dir/Dockerfile" ]]; then
dir=./containers/$image_name
if [ ! -f $dir/Dockerfile ]; then
echo "No Dockerfile found"
exit 1
fi
if [[ ! -f "$dir/config.sh" ]]; then
if [ ! -f $dir/config.sh ]; then
echo "No config.sh found for Dockerfile"
exit 1
fi
source "$dir/config.sh"
source $dir/config.sh
if [[ -n "$org_name" ]]; then
DOCKER_ORG="$org_name"
fi
DOCKER_REPOSITORY="$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE"
DOCKER_REPOSITORY=$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE
DOCKER_REPOSITORY=${DOCKER_REPOSITORY,,} # lowercase
echo "Repo: $DOCKER_REPOSITORY"
echo "Base dir: $DOCKER_BASE_DIR"
args=""
for tag in "${tags[@]}"; do
for tag in ${tags[@]}; do
args+=" -t $DOCKER_REPOSITORY:$tag"
done
output_image="/tmp/${image_name}_image_${platform}.tar"
if [[ $push -eq 1 ]]; then
args+=" --push"
args+=" --cache-to=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag,mode=max"
fi
docker buildx build \
$args \
--build-arg OPEN_DEVIN_BUILD_VERSION="$OPEN_DEVIN_BUILD_VERSION" \
--platform linux/$platform \
--build-arg OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION \
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag \
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag_base-main \
--platform linux/amd64,linux/arm64 \
--provenance=false \
-f "$dir/Dockerfile" \
--output type=docker,dest="$output_image" \
"$DOCKER_BASE_DIR"
echo "${tags[*]}" > tags.txt
-f $dir/Dockerfile $DOCKER_BASE_DIR

View File

@@ -16,11 +16,11 @@ RUN apt-get update && apt-get install -y \
build-essential \
openssh-server \
sudo \
bash \
gcc \
jq \
g++ \
make \
iproute2 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m0755 /var/run/sshd
@@ -28,17 +28,6 @@ RUN mkdir -p -m0755 /var/run/sshd
# symlink python3 to python
RUN ln -s /usr/bin/python3 /usr/bin/python
# ==== OpenDevin Runtime Client ====
RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs
RUN wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
RUN bash Miniforge3.sh -b -p /opendevin/miniforge3
RUN chmod -R g+w /opendevin/miniforge3
RUN bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"
RUN echo "" > /opendevin/bash.bashrc
RUN rm -f Miniforge3.sh
# - agentskills dependencies
RUN /opendevin/miniforge3/bin/pip install --upgrade pip
RUN /opendevin/miniforge3/bin/pip install jupyterlab notebook jupyter_kernel_gateway flake8
RUN /opendevin/miniforge3/bin/pip install python-docx PyPDF2 python-pptx pylatexenc openai
RUN /opendevin/miniforge3/bin/pip install python-dotenv toml termcolor pydantic python-docx pyyaml docker pexpect tenacity e2b browsergym minio
# install basic dependencies for CodeActAgent
RUN pip3 install --upgrade pip
RUN pip3 install jupyterlab notebook jupyter_kernel_gateway flake8

View File

@@ -9,17 +9,10 @@ select = [
"F",
"I",
"Q",
"B",
]
ignore = [
"E501",
"B003",
"B007",
"B009",
"B010",
"B904",
"B018",
]
[lint.flake8-quotes]

View File

@@ -8,8 +8,8 @@ const config: Config = {
favicon: "img/logo.png",
// Set the production url of your site here
url: "https://docs.all-hands.dev",
baseUrl: "/",
url: "https://OpenDevin.github.io",
baseUrl: "/OpenDevin/",
// GitHub pages deployment config.
organizationName: "OpenDevin",
@@ -23,13 +23,8 @@ const config: Config = {
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr', 'zh-Hans'],
localeConfigs: {
en: {
htmlLang: 'en-GB',
},
},
defaultLocale: "en",
locales: ["en"],
},
presets: [
@@ -56,6 +51,7 @@ const config: Config = {
} satisfies Preset.Options,
],
],
themeConfig: {
image: "img/docusaurus.png",
navbar: {
@@ -83,11 +79,44 @@ const config: Config = {
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
type: 'localeDropdown',
position: 'left',
title: "OpenDevin",
items: [
{
label: "Docs",
to: "/modules/usage/intro",
},
],
},
{
title: "Community",
items: [
{
label: "Slack",
href: "https://join.slack.com/t/opendevin/shared_invite/zt-2ggtwn3k5-PvAA2LUmqGHVZ~XzGq~ILw"
},
{
label: "Discord",
href: "https://discord.gg/ESHStjSjD4",
},
],
},
{
title: "More",
items: [
{
label: "GitHub",
href: "https://github.com/OpenDevin/OpenDevin",
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} OpenDevin`,
},
prism: {
theme: prismThemes.oneLight,

View File

@@ -1,406 +0,0 @@
{
"footer.title": {
"message": "OpenDevin"
},
"footer.docs": {
"message": "Documents"
},
"footer.community": {
"message": "Communauté"
},
"footer.copyright": {
"message": "© {year} OpenDevin"
},
"faq.title": {
"message": "Questions Fréquemment Posées",
"description": "FAQ Title"
},
"faq.description": {
"message": "Questions Fréquemment Posées"
},
"faq.section.title.1": {
"message": "Qu'est-ce qu'OpenDevin ?",
"description": "First Section Title"
},
"faq.section.highlight": {
"message": "OpenDevin",
"description": "Highlight Text"
},
"faq.section.description.1": {
"message": "est un ingénieur logiciel autonome qui peut résoudre des tâches d'ingénierie logicielle et de navigation web à tout moment. Il peut exécuter des requêtes en sciences des données, telles que \"Trouver le nombre de demandes de pull à l'repository OpenDevin dans les derniers mois\", et des tâches d'ingénierie logicielle, comme \"Veuillez ajouter des tests à ce fichier et vérifier si tous les tests passent. Si ce n'est pas le cas, réparez le fichier.\"",
"description": "Description for OpenDevin"
},
"faq.section.description.2": {
"message": "De plus, OpenDevin est une plateforme et communauté pour les développeurs d'agents qui souhaitent tester et évaluer de nouveaux agents.",
"description": "Further Description for OpenDevin"
},
"faq.section.title.2": {
"message": "Support",
"description": "Support Section Title"
},
"faq.section.support.answer": {
"message": "Si vous rencontrez un problème que d'autres utilisateurs peuvent également avoir, merci de le signaler sur {githubLink}. Si vous avez des difficultés à l'installation ou des questions générales, rejoignez-vous sur {discordLink} ou {slackLink}.",
"description": "Support Answer"
},
"faq.section.title.3": {
"message": "Comment résoudre un problème sur GitHub avec OpenDevin ?",
"description": "GitHub Issue Section Title"
},
"faq.section.github.steps.intro": {
"message": "Pour résoudre un problème sur GitHub en utilisant OpenDevin, envoyez une commande à OpenDevin demandant qu'il suit des étapes comme les suivantes :",
"description": "GitHub Steps Introduction"
},
"faq.section.github.step1": {
"message": "Lisez l'issue https://github.com/OpenDevin/OpenDevin/issues/1611",
"description": "GitHub Step 1"
},
"faq.section.github.step2": {
"message": "Cloner le dépôt et vérifier une nouvelle branche",
"description": "GitHub Step 2"
},
"faq.section.github.step3": {
"message": "Sur la base des instructions dans la description de l'issue, modifiez les fichiers pour résoudre le problème",
"description": "GitHub Step 3"
},
"faq.section.github.step4": {
"message": "Pousser le résultat à GitHub en utilisant la variable d'environnement GITHUB_TOKEN",
"description": "GitHub Step 4"
},
"faq.section.github.step5": {
"message": "Dites-moi le lien que je dois utiliser pour envoyer une demande de pull",
"description": "GitHub Step 5"
},
"faq.section.github.steps.preRun": {
"message": "Avant de lancer OpenDevin, vous pouvez faire :",
"description": "GitHub Steps Pre-Run"
},
"faq.section.github.steps.tokenInfo": {
"message": "où XXX est un jeton GitHub que vous avez créé et qui a les autorisations pour pousser dans le dépôt OpenDevin. Si vous n'avez pas d'autorisations de modification du dépôt OpenDevin, vous devrez peut-être changer cela en :",
"description": "GitHub Steps Token Info"
},
"faq.section.github.steps.usernameInfo": {
"message": "où USERNAME est votre nom GitHub.",
"description": "GitHub Steps Username Info"
},
"faq.section.title.4": {
"message": "Comment OpenDevin est-il différent de Devin ?",
"description": "Devin Section Title"
},
"faq.section.devin.linkText": {
"message": "Devin",
"description": "Devin Link Text"
},
"faq.section.devin.description": {
"message": "est un produit commercial par Cognition Inc., qui a servi d'inspiration initiale pour OpenDevin. Les deux visent à bien faire le travail d'ingénierie logicielle, mais vous pouvez télécharger, utiliser et modifier OpenDevin, tandis que Devin peut être utilisé uniquement via le site de Cognition. De plus, OpenDevin a évolué au-delà de l'inspiration initiale, et est maintenant un écosystème communautaire pour le développement d'agents en général, et nous serions ravis de vous voir rejoindre et",
"description": "Devin Description"
},
"faq.section.devin.contribute": {
"message": "contribuer",
"description": "Contribute Link"
},
"faq.section.title.5": {
"message": "Comment OpenDevin est-il différent de ChatGPT ?",
"description": "ChatGPT Section Title"
},
"faq.section.chatgpt.description": {
"message": "ChatGPT vous pouvez accéder en ligne, il ne se connecte pas aux fichiers locaux et ses capacités d'exécution du code sont limitées. Alors qu'il peut écrire du code, mais c'est difficile à tester ou à exécuter.",
"description": "ChatGPT Description"
},
"homepage.description": {
"message": "Génération d'code AI pour l'ingénierie logicielle.",
"description": "The homepage description"
},
"homepage.getStarted": {
"message": "Commencer"
},
"welcome.message": {
"message": "Bienvenue à OpenDevin, un système d'IA autonome ingénieur logiciel capable d'exécuter des tâches d'ingénierie complexes et de collaborer activement avec les utilisateurs sur les projets de développement logiciel."
},
"theme.ErrorPageContent.title": {
"message": "Cette page a planté.",
"description": "The title of the fallback page when the page crashed"
},
"theme.BackToTopButton.buttonAriaLabel": {
"message": "Retourner en haut de la page",
"description": "The ARIA label for the back to top button"
},
"theme.blog.archive.title": {
"message": "Archives",
"description": "The page & hero title of the blog archive page"
},
"theme.blog.archive.description": {
"message": "Archives",
"description": "The page & hero description of the blog archive page"
},
"theme.blog.paginator.navAriaLabel": {
"message": "Pagination des listes d'articles du blog",
"description": "The ARIA label for the blog pagination"
},
"theme.blog.paginator.newerEntries": {
"message": "Nouvelles entrées",
"description": "The label used to navigate to the newer blog posts page (previous page)"
},
"theme.blog.paginator.olderEntries": {
"message": "Anciennes entrées",
"description": "The label used to navigate to the older blog posts page (next page)"
},
"theme.blog.post.paginator.navAriaLabel": {
"message": "Pagination des articles du blog",
"description": "The ARIA label for the blog posts pagination"
},
"theme.blog.post.paginator.newerPost": {
"message": "Article plus récent",
"description": "The blog post button label to navigate to the newer/previous post"
},
"theme.blog.post.paginator.olderPost": {
"message": "Article plus ancien",
"description": "The blog post button label to navigate to the older/next post"
},
"theme.blog.post.plurals": {
"message": "Un article|{count} articles",
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.blog.tagTitle": {
"message": "{nPosts} tags avec « {tagName} »",
"description": "The title of the page for a blog tag"
},
"theme.tags.tagsPageLink": {
"message": "Voir tous les tags",
"description": "The label of the link targeting the tag list page"
},
"theme.colorToggle.ariaLabel": {
"message": "Basculer entre le mode sombre et clair (actuellement {mode})",
"description": "The ARIA label for the navbar color mode toggle"
},
"theme.colorToggle.ariaLabel.mode.dark": {
"message": "mode sombre",
"description": "The name for the dark color mode"
},
"theme.colorToggle.ariaLabel.mode.light": {
"message": "mode clair",
"description": "The name for the light color mode"
},
"theme.docs.breadcrumbs.navAriaLabel": {
"message": "Bouton de navigation des liens de la page",
"description": "The ARIA label for the breadcrumbs"
},
"theme.docs.DocCard.categoryDescription.plurals": {
"message": "1 élément|{count} éléments",
"description": "The default description for a category card in the generated index about how many items this category includes"
},
"theme.docs.paginator.navAriaLabel": {
"message": "Pages de documentation",
"description": "The ARIA label for the docs pagination"
},
"theme.docs.paginator.previous": {
"message": "Précédent",
"description": "The label used to navigate to the previous doc"
},
"theme.docs.paginator.next": {
"message": "Suivant",
"description": "The label used to navigate to the next doc"
},
"theme.docs.tagDocListPageTitle.nDocsTagged": {
"message": "Un document tagué|{count} documents tagués",
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.docs.tagDocListPageTitle": {
"message": "{nDocsTagged} avec \"{tagName}\"",
"description": "The title of the page for a docs tag"
},
"theme.docs.versionBadge.label": {
"message": "Version: {versionLabel}"
},
"theme.docs.versions.unreleasedVersionLabel": {
"message": "Ceci est la documentation de la prochaine version {versionLabel} de {siteTitle}.",
"description": "The label used to tell the user that he's browsing an unreleased doc version"
},
"theme.docs.versions.unmaintainedVersionLabel": {
"message": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
},
"theme.docs.versions.latestVersionSuggestionLabel": {
"message": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
"description": "The label used to tell the user to check the latest version"
},
"theme.docs.versions.latestVersionLinkLabel": {
"message": "dernière version",
"description": "The label used for the latest version suggestion link label"
},
"theme.common.editThisPage": {
"message": "Éditer cette page",
"description": "The link label to edit the current page"
},
"theme.common.headingLinkTitle": {
"message": "Lien direct vers {heading}",
"description": "Title for link to heading"
},
"theme.lastUpdated.atDate": {
"message": " le {date}",
"description": "The words used to describe on which date a page has been last updated"
},
"theme.lastUpdated.byUser": {
"message": " par {user}",
"description": "The words used to describe by who the page has been last updated"
},
"theme.lastUpdated.lastUpdatedAtBy": {
"message": "Dernière mise à jour{atDate}{byUser}",
"description": "The sentence used to display when a page has been last updated, and by who"
},
"theme.navbar.mobileVersionsDropdown.label": {
"message": "Versions",
"description": "The label for the navbar versions dropdown on mobile view"
},
"theme.NotFound.title": {
"message": "Page introuvable",
"description": "The title of the 404 page"
},
"theme.tags.tagsListLabel": {
"message": "Tags :",
"description": "The label alongside a tag list"
},
"theme.admonition.caution": {
"message": "prudence",
"description": "The default label used for the Caution admonition (:::caution)"
},
"theme.admonition.danger": {
"message": "danger",
"description": "The default label used for the Danger admonition (:::danger)"
},
"theme.admonition.info": {
"message": "information",
"description": "The default label used for the Info admonition (:::info)"
},
"theme.admonition.note": {
"message": "remarque",
"description": "The default label used for the Note admonition (:::note)"
},
"theme.admonition.tip": {
"message": "astuce",
"description": "The default label used for the Tip admonition (:::tip)"
},
"theme.admonition.warning": {
"message": "prudence",
"description": "The default label used for the Warning admonition (:::warning)"
},
"theme.AnnouncementBar.closeButtonAriaLabel": {
"message": "Fermer",
"description": "The ARIA label for close button of announcement bar"
},
"theme.blog.sidebar.navAriaLabel": {
"message": "Navigation vers les articles récents du blog",
"description": "The ARIA label for recent posts in the blog sidebar"
},
"theme.CodeBlock.copied": {
"message": "Copié",
"description": "The copied button label on code blocks"
},
"theme.CodeBlock.copyButtonAriaLabel": {
"message": "Copier le code",
"description": "The ARIA label for copy code blocks button"
},
"theme.CodeBlock.copy": {
"message": "Copier",
"description": "The copy button label on code blocks"
},
"theme.CodeBlock.wordWrapToggle": {
"message": "Activer/désactiver le retour à la ligne",
"description": "The title attribute for toggle word wrapping button of code block lines"
},
"theme.DocSidebarItem.expandCategoryAriaLabel": {
"message": "Développer la catégorie '{label}' de la barre latérale",
"description": "The ARIA label to expand the sidebar category"
},
"theme.DocSidebarItem.collapseCategoryAriaLabel": {
"message": "Réduire la catégorie '{label}' de la barre latérale",
"description": "The ARIA label to collapse the sidebar category"
},
"theme.NavBar.navAriaLabel": {
"message": "Main",
"description": "The ARIA label for the main navigation"
},
"theme.navbar.mobileLanguageDropdown.label": {
"message": "Langues",
"description": "The label for the mobile language switcher dropdown"
},
"theme.NotFound.p1": {
"message": "Nous n'avons pas trouvé ce que vous recherchez.",
"description": "The first paragraph of the 404 page"
},
"theme.NotFound.p2": {
"message": "Veuillez contacter le propriétaire du site qui vous a lié à l'URL d'origine et leur faire savoir que leur lien est cassé.",
"description": "The 2nd paragraph of the 404 page"
},
"theme.TOCCollapsible.toggleButtonLabel": {
"message": "Sur cette page",
"description": "The label used by the button on the collapsible TOC component"
},
"theme.blog.post.readMore": {
"message": "Lire plus",
"description": "The label used in blog post item excerpts to link to full blog posts"
},
"theme.blog.post.readMoreLabel": {
"message": "En savoir plus sur {title}",
"description": "The ARIA label for the link to full blog posts from excerpts"
},
"theme.blog.post.readingTime.plurals": {
"message": "Une minute de lecture|{readingTime} minutes de lecture",
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.docs.breadcrumbs.home": {
"message": "Page d'accueil",
"description": "The ARIA label for the home page in the breadcrumbs"
},
"theme.docs.sidebar.collapseButtonTitle": {
"message": "Réduire le menu latéral",
"description": "The title attribute for collapse button of doc sidebar"
},
"theme.docs.sidebar.collapseButtonAriaLabel": {
"message": "Réduire le menu latérale",
"description": "The title attribute for collapse button of doc sidebar"
},
"theme.docs.sidebar.navAriaLabel": {
"message": "Barre de navigation latérale des docs",
"description": "The ARIA label for the sidebar navigation"
},
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
"message": "Fermer la barre de navigation",
"description": "The ARIA label for close button of mobile sidebar"
},
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
"message": "← Retour au menu principal",
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
},
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
"message": "Ouvrir/fermer la barre de navigation",
"description": "The ARIA label for hamburger menu button of mobile navigation"
},
"theme.docs.sidebar.expandButtonTitle": {
"message": "Déplier le menu latéral",
"description": "The ARIA label and title attribute for expand button of doc sidebar"
},
"theme.docs.sidebar.expandButtonAriaLabel": {
"message": "Déployer le menu latérale",
"description": "The ARIA label and title attribute for expand button of doc sidebar"
},
"theme.ErrorPageContent.tryAgain": {
"message": "Réessayer",
"description": "The label of the button to try again rendering when the React error boundary captures an error"
},
"theme.common.skipToMainContent": {
"message": "Aller directement au contenu principal",
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
},
"theme.tags.tagsPageTitle": {
"message": "Tags",
"description": "The title of the tag list page"
},
"theme.unlistedContent.title": {
"message": "Page non répertoriée",
"description": "The unlisted content banner title"
},
"theme.unlistedContent.message": {
"message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder.",
"description": "The unlisted content banner message"
}
}

View File

@@ -1,14 +0,0 @@
{
"title": {
"message": "Blog",
"description": "The title for the blog used in SEO"
},
"description": {
"message": "Blog",
"description": "The description for the blog used in SEO"
},
"sidebar.title": {
"message": "Articles récents",
"description": "The label for the left sidebar"
}
}

View File

@@ -1,18 +0,0 @@
{
"version.label": {
"message": "Next",
"description": "The label for version current"
},
"sidebar.docsSidebar.category.🤖 Backends LLM": {
"message": "🤖 Backends LLM",
"description": "The label for category 🤖 Backends LLM in sidebar docsSidebar"
},
"sidebar.docsSidebar.category.🚧 Dépannage": {
"message": "🚧 Dépannage",
"description": "The label for category 🚧 Dépannage in sidebar docsSidebar"
},
"sidebar.apiSidebar.category.Backend": {
"message": "Backend",
"description": "The label for category Backend in sidebar apiSidebar"
}
}

View File

@@ -1,3 +0,0 @@
# Documentation Python
Les documents apparaîtront ici après le déploiement.

View File

@@ -1,5 +0,0 @@
{
"items": ["python/python"],
"label": "Backend",
"type": "categorie"
}

View File

@@ -1,53 +0,0 @@
---
sidebar_position: 7
---
# 📚 Divers {#misc}
## ⭐️ Stratégie de Recherche {#research-strategy}
La réalisation d'une réplication complète des applications de production avec les LLM est une entreprise complexe. Notre stratégie implique :
1. **Recherche Technique de Base :** Se concentrer sur la recherche fondamentale pour comprendre et améliorer les aspects techniques de la génération et de la gestion de code.
2. **Compétences Spécialisées :** Améliorer l'efficacité des composants de base grâce à la curation des données, aux méthodes de formation, et plus encore.
3. **Planification des Tâches :** Développer des capacités pour la détection de bogues, la gestion du code source et l'optimisation.
4. **Évaluation :** Établir des métriques d'évaluation complètes pour mieux comprendre et améliorer nos modèles.
## 🚧 Agent Par Défaut {#default-agent}
- Notre agent par défaut est actuellement le CodeActAgent, capable de générer du code et de gérer des fichiers. Nous travaillons sur d'autres implémentations d'agents, y compris [SWE Agent](https://swe-agent.com/). Vous pouvez [lire à propos de notre ensemble actuel d'agents ici](./agents).
## 🤝 Comment Contribuer {#how-to-contribute}
OpenDevin est un projet communautaire, et nous accueillons les contributions de tout le monde. Que vous soyez développeur, chercheur, ou simplement enthousiaste à l'idée de faire progresser le domaine de l'ingénierie logicielle avec l'IA, il existe de nombreuses façons de vous impliquer :
- **Contributions de Code :** Aidez-nous à développer les fonctionnalités de base, l'interface frontend ou les solutions de sandboxing.
- **Recherche et Évaluation :** Contribuez à notre compréhension des LLM en ingénierie logicielle, participez à l'évaluation des modèles ou suggérez des améliorations.
- **Retour d'Information et Tests :** Utilisez l'ensemble d'outils OpenDevin, signalez des bogues, suggérez des fonctionnalités ou fournissez des retours sur l'ergonomie.
Pour plus de détails, veuillez consulter [ce document](https://github.com/OpenDevin/OpenDevin/blob/main/CONTRIBUTING.md).
## 🤖 Rejoignez Notre Communauté {#join-our-community}
Nous avons maintenant à la fois un espace de travail Slack pour la collaboration sur la construction d'OpenDevin et un serveur Discord pour discuter de tout ce qui est lié, par exemple, à ce projet, aux LLM, aux agents, etc.
- [Espace de travail Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA)
- [Serveur Discord](https://discord.gg/ESHStjSjD4)
Si vous souhaitez contribuer, n'hésitez pas à rejoindre notre communauté. Simplifions l'ingénierie logicielle ensemble !
🐚 **Codez moins, créez plus avec OpenDevin.**
[![Star History Chart](https://api.star-history.com/svg?repos=OpenDevin/OpenDevin&type=Date)](https://star-history.com/#OpenDevin/OpenDevin&Date)
## 🛠️ Construit Avec {#built-with}
OpenDevin est construit en utilisant une combinaison de cadres et de bibliothèques puissants, offrant une base robuste pour son développement. Voici les technologies clés utilisées dans le projet :
![FastAPI](https://img.shields.io/badge/FastAPI-black?style=for-the-badge) ![uvicorn](https://img.shields.io/badge/uvicorn-black?style=for-the-badge) ![LiteLLM](https://img.shields.io/badge/LiteLLM-black?style=for-the-badge) ![Docker](https://img.shields.io/badge/Docker-black?style=for-the-badge) ![Ruff](https://img.shields.io/badge/Ruff-black?style=for-the-badge) ![MyPy](https://img.shields.io/badge/MyPy-black?style=for-the-badge) ![LlamaIndex](https://img.shields.io/badge/LlamaIndex-black?style=for-the-badge) ![React](https://img.shields.io/badge/React-black?style=for-the-badge)
Veuillez noter que la sélection de ces technologies est en cours, et que des technologies supplémentaires peuvent être ajoutées ou des existantes supprimées au fur et à mesure de l'évolution du projet. Nous nous efforçons d'adopter les outils les plus adaptés et efficaces pour améliorer les capacités d'OpenDevin.
## 📜 Licence {#license}
Distribué sous la licence MIT. Voir [notre licence](https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE) pour plus d'informations.

View File

@@ -1,134 +0,0 @@
---
sidebar_position: 3
---
# 🧠 Agents et Capacités
## Agent CodeAct
### Description
Cet agent implémente l'idée CodeAct ([article](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) qui consolide les **act**ions des agents LLM en un espace d'action **code** unifié pour à la fois la _simplicité_ et la _performance_ (voir article pour plus de détails).
L'idée conceptuelle est illustrée ci-dessous. À chaque tour, l'agent peut :
1. **Converse** : Communiquer avec les humains en langage naturel pour demander des clarifications, des confirmations, etc.
2. **CodeAct** : Choisir d'accomplir la tâche en exécutant du code
- Exécuter toute commande `bash` Linux valide
- Exécuter tout code `Python` valide avec [un interpréteur Python interactif](https://ipython.org/). Cela est simulé à travers la commande `bash`, voir le système de plugin ci-dessous pour plus de détails.
![image](https://github.com/OpenDevin/OpenDevin/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3)
### Système de Plugin
Pour rendre l'agent CodeAct plus puissant avec seulement l'accès à l'espace d'action `bash`, l'agent CodeAct exploite le système de plugins d'OpenDevin:
- [Plugin Jupyter](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter) : pour l'exécution d'IPython via la commande bash
- [Plugin outil agent SWE](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands) : Outils de ligne de commande bash puissants pour les tâches de développement logiciel introduits par [swe-agent](https://github.com/princeton-nlp/swe-agent).
### Démonstration
https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
_Exemple de CodeActAgent avec `gpt-4-turbo-2024-04-09` effectuant une tâche de science des données (régression linéaire)_
### Actions
`Action`,
`CmdRunAction`,
`IPythonRunCellAction`,
`AgentEchoAction`,
`AgentFinishAction`,
`AgentTalkAction`
### Observations
`CmdOutputObservation`,
`IPythonRunCellObservation`,
`AgentMessageObservation`,
`UserMessageObservation`
### Méthodes
| Méthode | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `__init__` | Initialise un agent avec `llm` et une liste de messages `list[Mapping[str, str]]` |
| `step` | Effectue une étape en utilisant l'agent CodeAct. Cela inclut la collecte d'informations sur les étapes précédentes et invite le modèle à exécuter une commande. |
### En cours de réalisation & prochaine étape
[] Support de la navigation sur le web
[] Compléter le workflow pour l'agent CodeAct afin de soumettre des PRs Github
## Agent Monologue
### Description
L'agent Monologue utilise la mémoire à long terme et à court terme pour accomplir des tâches.
La mémoire à long terme est stockée en tant qu'objet LongTermMemory et le modèle l'utilise pour rechercher des exemples du passé.
La mémoire à court terme est stockée en tant qu'objet Monologue et le modèle peut la condenser si nécessaire.
### Actions
`Action`,
`NullAction`,
`CmdRunAction`,
`FileWriteAction`,
`FileReadAction`,
`BrowseURLAction`,
`GithubPushAction`,
`AgentThinkAction`
### Observations
`Observation`,
`NullObservation`,
`CmdOutputObservation`,
`FileReadObservation`,
`BrowserOutputObservation`
### Méthodes
| Méthode | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `__init__` | Initialise l'agent avec une mémoire à long terme et un monologue interne |
| `_add_event` | Ajoute des événements au monologue de l'agent et condense avec un résumé automatiquement si le monologue est trop long |
| `_initialize` | Utilise la liste `INITIAL_THOUGHTS` pour donner à l'agent un contexte pour ses capacités et comment naviguer dans le `/workspace` |
| `step` | Modifie l'état actuel en ajoutant les actions et observations les plus récentes, puis invite le modèle à réfléchir à la prochaine action à entreprendre. |
## Agent Planificateur
### Description
L'agent planificateur utilise une stratégie d'incitation spéciale pour créer des plans à long terme pour résoudre les problèmes.
L'agent reçoit ses paires action-observation précédentes, la tâche actuelle, et un indice basé sur la dernière action effectuée à chaque étape.
### Actions
`NullAction`,
`CmdRunAction`,
`BrowseURLAction`,
`GithubPushAction`,
`FileReadAction`,
`FileWriteAction`,
`AgentThinkAction`,
`AgentFinishAction`,
`AgentSummarizeAction`,
`AddTaskAction`,
`ModifyTaskAction`,
### Observations
`Observation`,
`NullObservation`,
`CmdOutputObservation`,
`FileReadObservation`,
`BrowserOutputObservation`
### Méthodes
| Méthode | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `__init__` | Initialise un agent avec `llm` |
| `step` | Vérifie si l'étape actuelle est terminée, retourne `AgentFinishAction` si oui. Sinon, crée une incitation de planification et l'envoie au modèle pour inférence, en ajoutant le résultat comme prochaine action. |

View File

@@ -1,50 +0,0 @@
---
sidebar_position: 4
---
# 🏛️ Aperçu de l'Architecture Système
Voici un aperçu de haut niveau de l'architecture du système. Le système est divisé en deux composants principaux : le frontend et le backend. Le frontend est responsable de la gestion des interactions avec l'utilisateur et de l'affichage des résultats. Le backend est responsable de la gestion de la logique métier et de l'exécution des agents.
![system_architecture.svg](/img/system_architecture.svg)
Cet aperçu est simplifié pour montrer les principaux composants et leurs interactions. Pour une vue plus détaillée de l'architecture du backend, consultez la section [Architecture du Backend](#backend-architecture-fr).
# Architecture du Backend {#backend-architecture-fr}
_**Avertissement**: L'architecture du backend est en cours de développement et est sujette à modifications. Le schéma suivant montre l'architecture actuelle du backend basée sur le commit indiqué dans le pied de page du schéma._
![backend_architecture.svg](/img/backend_architecture.svg)
<details>
<summary>Mise à jour de ce Schéma</summary>
<div>
La génération du schéma d'architecture du backend est partiellement automatisée.
Le schéma est généré à partir des annotations de type dans le code en utilisant l'outil py2puml.
Le schéma est ensuite revu manuellement, ajusté et exporté en PNG et SVG.
## Prérequis
- Un environnement Python dans lequel opendevin est exécutable
(selon les instructions du fichier README.md à la racine du dépôt)
- [py2puml](https://github.com/lucsorel/py2puml) installé
## Étapes
1. Générez automatiquement le schéma en exécutant la commande suivante depuis la racine du dépôt :
`py2puml opendevin opendevin > docs/architecture/backend_architecture.puml`
2. Ouvrez le fichier généré dans un éditeur PlantUML, par exemple Visual Studio Code avec l'extension PlantUML ou [PlantText](https://www.planttext.com/)
3. Révisez le PUML généré et apportez toutes les modifications nécessaires au schéma (ajoutez les parties manquantes, corrigez les erreurs, améliorez l'agencement).
_py2puml crée le schéma à partir des annotations de type dans le code, donc les annotations de type manquantes ou incorrectes peuvent entraîner un schéma incomplet ou incorrect._
4. Examinez la différence entre le nouveau schéma et le précédent et vérifiez manuellement si les modifications sont correctes.
_Assurez-vous de ne pas supprimer les parties ajoutées manuellement au schéma par le passé et qui sont toujours pertinentes._
5. Ajoutez le hash du commit qui a été utilisé pour générer le schéma dans le pied de page du schéma.
6. Exporte le schéma sous forme de fichiers PNG et SVG et remplacez les schémas existants dans le répertoire `docs/architecture`. Cela peut être fait avec (par exemple [PlantText](https://www.planttext.com/))
</div>
</details>

View File

@@ -1,107 +0,0 @@
# 💿 Comment Créer un Soutien Docker sur Mesure
Le sandbox par défaut OpenDevin est équipé d'une configuration ubuntu minimaliste. Votre cas d'utilisation pourrait nécessiter des logiciels installés par défaut. Cet article vous enseignera comment réaliser cela en utilisant une image docker personnalisée.
## Configuration
Assurez-vous de pouvoir utiliser OpenDevin en suivant la documentation [Development.md](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md).
## Créer Votre Image Docker
Ensuite, vous devez créer votre image docker personnalisée qui doit être basée sur debian/ubuntu. Par exemple, si nous souhaitons que OpenDevin ait accès au "node" binaire, nous utiliserions ce Dockerfile:
```bash
# Commencez avec l'image ubuntu la plus récente
FROM ubuntu:latest
# Effectuez les mises à jour nécessaires
RUN apt-get update && apt-get install
# Installez nodejs
RUN apt-get install -y nodejs
```
Ensuite, construisez votre image docker avec le nom de votre choix. Par exemple "image_personnalisée". Pour cela, créez un répertoire et placez le fichier à l'intérieur avec le nom "Dockerfile", puis dans le répertoire exécutez cette commande:
```bash
docker build -t image_personnalisée .
```
Cela produira une nouvelle image appelée ```image_personnalisée``` qui sera disponible dans Docker Engine.
> Remarque: Dans la configuration décrite ici, OpenDevin va fonctionner en tant que utilisateur "opendevin" à l'intérieur du sandbox et donc tous les packages installés via le Dockerfile seront disponibles pour tous les utilisateurs sur le système, pas seulement root.
>
> L'installation avec apt-get ci-dessus installe nodejs pour tous les utilisateurs.
## Spécifiez votre image personnalisée dans le fichier config.toml
La configuration OpenDevin se fait via le fichier de niveau supérieur ```config.toml``` .
Créez un fichier ```config.toml``` dans le répertoire OpenDevin et entrez ces contenus:
```toml
[core]
workspace_base="./workspace"
persist_sandbox=false
run_as_devin=true
sandbox_container_image="image_personnalisée"
```
> Assurez-vous que ```sandbox_container_image``` est défini sur le nom de votre image personnalisée précédente.
## Exécution
Exécutez OpenDevin en exécutant ```make run``` dans le répertoire racine.
Naviguez vers ```localhost:3001``` et vérifiez si vos dépendances souhaitées sont disponibles.
Dans le cas de l'exemple ci-dessus, la commande ```node -v``` dans la console produit ```v18.19.1```
Félicitations !
## Explication technique
Le code pertinent est défini dans [ssh_box.py](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/runtime/docker/ssh_box.py) et [image_agnostic_util.py](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/runtime/docker/image_agnostic_util.py).
En particulier, ssh_box.py vérifie l'objet config pour ```config.sandbox_container_image``` et ensuite tente de récupérer l'image à l'aide de [get_od_sandbox_image](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/runtime/docker/image_agnostic_util.py#L72), qui est défini dans image_agnostic_util.py.
Lorsqu'une image personnalisée est utilisée pour la première fois, elle ne sera pas trouvée et donc elle sera construite (à l'exécution ultérieure, l'image construite sera trouvée et renvoyée).
L'image personnalisée est construite avec [_build_sandbox_image()](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/runtime/docker/image_agnostic_util.py#L29), qui crée un fichier docker en utilisant votre image personnalisée comme base et configure ensuite l'environnement pour OpenDevin, comme ceci:
```python
dockerfile_content = (
f'FROM {base_image}\n'
'RUN apt update && apt install -y openssh-server wget sudo\n'
'RUN mkdir -p -m0755 /var/run/sshd\n'
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
'RUN wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"\n'
'RUN bash Miniforge3-$(uname)-$(uname -m).sh -b -p /opendevin/miniforge3\n'
'RUN bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"\n'
'RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> ~/.bashrc\n'
'RUN echo "export PATH=/opendevin/miniforge3/bin:$PATH" >> /opendevin/bash.bashrc\n'
).strip()
```
> Remarque: Le nom de l'image est modifié via [_get_new_image_name()](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/runtime/docker/image_agnostic_util.py#L63) et c'est ce nom modifié qui sera recherché lors des exécutions ultérieures.
## Dépannage / Erreurs
### Erreur: ```useradd: UID 1000 est non unique```
Si vous voyez cette erreur dans la sortie de la console, il s'agit du fait que OpenDevin essaie de créer le utilisateur opendevin dans le sandbox avec un ID d'utilisateur de 1000, cependant cet ID d'utilisateur est déjà utilisé dans l'image (pour une raison inconnue). Pour résoudre ce problème, changez la valeur du champ sandbox_user_id dans le fichier config.toml en une valeur différente:
```toml
[core]
workspace_base="./workspace"
persist_sandbox=false
run_as_devin=true
sandbox_container_image="image_personnalisée"
sandbox_user_id="1001"
```
### Erreurs de port d'utilisation
Si vous voyez un message d'erreur indiquant que le port est utilisé ou indisponible, essayez de supprimer toutes les containers docker en cours d'exécution (exécutez `docker ps` et `docker rm` des containers concernés) puis ré-exécutez ```make run```
## Discuter
Pour d'autres problèmes ou questions rejoignez le [Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA) ou le [Discord](https://discord.gg/ESHStjSjD4) et demandez!

View File

@@ -1,18 +0,0 @@
---
sidebar_position: 6
---
# ✅ Fournir des Commentaires
Lorsque vous utilisez OpenDevin, vous rencontrerez sans aucun doute des cas où les choses fonctionnent bien et d'autres où elles ne fonctionnent pas. Nous vous encourageons à fournir des commentaires lorsque vous utilisez OpenDevin pour aider l'équipe de développement et, peut-être plus important encore, créer un corpus ouvert d'exemples de formation pour les agents de codage -- Partagez-OpenDevin !
## 📝 Comment Fournir des Commentaires
Fournir des commentaires est simple ! Lorsque vous utilisez OpenDevin, vous pouvez appuyer sur le bouton de pouce vers le haut ou vers le bas à n'importe quel moment de votre interaction. Vous serez invité à fournir votre adresse email (par exemple, afin que nous puissions vous contacter si nous voulons poser des questions de suivi), et vous pouvez choisir si vous souhaitez fournir des commentaires publiquement ou en privé.
<iframe width="560" height="315" src="https://www.youtube.com/embed/5rFx-StMVV0?si=svo7xzp6LhGK_GXr" title="Lecteur vidéo YouTube" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## 📜 Licence de Données et Confidentialité
* Les données **publiques** seront distribuées sous la licence MIT, comme OpenDevin lui-même, et pourront être utilisées par la communauté pour former et tester des modèles. Évidemment, les commentaires que vous pouvez rendre publics seront plus précieux pour la communauté dans son ensemble, donc lorsque vous ne traitez pas d'informations sensibles, nous vous encourageons à choisir cette option !
* Les données **privées** ne seront partagées qu'avec l'équipe OpenDevin dans le but d'améliorer OpenDevin.

View File

@@ -1,111 +0,0 @@
---
sidebar_position: 1
---
# 💻 OpenDevin
OpenDevin est un **ingénieur logiciel IA autonome** capable d'exécuter des tâches d'ingénierie complexes et de collaborer activement avec les utilisateurs sur des projets de développement logiciel.
Ce projet est entièrement open-source, vous pouvez donc l'utiliser et le modifier comme bon vous semble.
:::tip
Explorez le code source d'OpenDevin sur [GitHub](https://github.com/OpenDevin/OpenDevin) ou rejoignez l'une de nos communautés !
<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>
<br></br>
<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></br>
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA">
<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>
:::
## 🛠️ Pour commencer
La manière la plus simple d'exécuter OpenDevin est à l'intérieur d'un conteneur Docker. Il fonctionne mieux avec la version la plus récente de Docker, `26.0.0`.
Vous devez utiliser Linux, Mac OS ou WSL sur Windows.
Pour démarrer OpenDevin dans un conteneur docker, exécutez les commandes suivantes dans votre terminal :
:::warning
Lorsque vous exécutez la commande suivante, les fichiers dans `./workspace` peuvent être modifiés ou supprimés.
:::
```bash
WORKSPACE_BASE=$(pwd)/workspace
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 \
--name opendevin-app-$(date +%Y%m%d%H%M%S) \
ghcr.io/opendevin/opendevin:0.6
```
Vous trouverez OpenDevin fonctionnant à l'adresse [http://localhost:3000](http://localhost:3000) avec accès à `./workspace`. Pour qu'OpenDevin fonctionne sur votre code, placez-le dans `./workspace`.
OpenDevin n'aura accès qu'à ce dossier de workspace. Le reste de votre système ne sera pas affecté car il s'exécute dans un bac à sable sécurisé de docker.
:::tip
Si vous souhaitez utiliser la version **(instable !)** la plus récente, vous pouvez utiliser `ghcr.io/opendevin/opendevin:main` comme image (dernière ligne).
:::
Pour le workflow de développement, consultez [Development.md](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md).
Avez-vous des problèmes ? Consultez notre [Guide de dépannage](https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting).
:::warning
OpenDevin est actuellement en cours de développement, mais vous pouvez déjà exécuter la version alpha pour voir le système de bout en bout en action.
:::
[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
[forks-url]: https://github.com/OpenDevin/OpenDevin/network/members
[stars-shield]: https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge
[stars-url]: https://github.com/OpenDevin/OpenDevin/stargazers
[issues-shield]: https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge
[issues-url]: https://github.com/OpenDevin/OpenDevin/issues
[license-shield]: https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge
[license-url]: https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE

View File

@@ -1,37 +0,0 @@
# Azure OpenAI LLM
## Complétion
OpenDevin utilise LiteLLM pour les appels de complétion. Vous pouvez trouver leur documentation sur Azure [ici](https://docs.litellm.ai/docs/providers/azure)
### Configurations openai Azure
Lors de l'exécution de l'image Docker OpenDevin, vous devrez définir les variables d'environnement suivantes en utilisant `-e` :
```
LLM_BASE_URL="<azure-api-base-url>" # e.g. "https://openai-gpt-4-test-v-1.openai.azure.com/"
LLM_API_KEY="<azure-api-key>"
LLM_MODEL="azure/<your-gpt-deployment-name>"
LLM_API_VERSION = "<api-version>" # e.g. "2024-02-15-preview"
```
:::note
Vous pouvez trouver le nom de votre déploiement ChatGPT sur la page des déploiements sur Azure. Par défaut ou initialement, il pourrait être le même que le nom du modèle de chat (par exemple 'GPT4-1106-preview'), mais il n'est pas obligé de l'être. Exécutez OpenDevin, et une fois chargé dans le navigateur, allez dans Paramètres et définissez le modèle comme suit : "azure/&lt;your-actual-gpt-deployment-name&gt;". Si ce n'est pas dans la liste, entrez votre propre texte et enregistrez-le.
:::
## Embeddings
OpenDevin utilise llama-index pour les embeddings. Vous pouvez trouver leur documentation sur Azure [ici](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/)
### Configurations openai Azure
Le modèle utilisé pour les embeddings Azure OpenAI est "text-embedding-ada-002".
Vous avez besoin du nom de déploiement correct pour ce modèle dans votre compte Azure.
Lors de l'exécution d'OpenDevin dans Docker, définissez les variables d'environnement suivantes en utilisant `-e` :
```
LLM_EMBEDDING_MODEL="azureopenai"
LLM_EMBEDDING_DEPLOYMENT_NAME = "<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
LLM_API_VERSION = "<api-version>" # e.g. "2024-02-15-preview"
```

View File

@@ -1,28 +0,0 @@
# Google Gemini/Vertex LLM
## Complétion
OpenDevin utilise LiteLLM pour les appels de complétion. Les ressources suivantes sont pertinentes pour utiliser OpenDevin avec les LLMs de Google :
- [Gemini - Google AI Studio](https://docs.litellm.ai/docs/providers/gemini)
- [VertexAI - Google Cloud Platform](https://docs.litellm.ai/docs/providers/vertex)
### Configurations de Gemini - Google AI Studio
Pour utiliser Gemini via Google AI Studio lors de l'exécution de l'image Docker d'OpenDevin, vous devez définir les variables d'environnement suivantes en utilisant `-e` :
```
GEMINI_API_KEY="<votre-cle-api-google>"
LLM_MODEL="gemini/gemini-1.5-pro"
```
### Configurations de Vertex AI - Google Cloud Platform
Pour utiliser Vertex AI via Google Cloud Platform lors de l'exécution de l'image Docker d'OpenDevin, vous devez définir les variables d'environnement suivantes en utilisant `-e` :
```
GOOGLE_APPLICATION_CREDENTIALS="<dump-json-du-compte-de-service-gcp-json>"
VERTEXAI_PROJECT="<votre-id-de-projet-gcp>"
VERTEXAI_LOCATION="<votre-localisation-gcp>"
LLM_MODEL="vertex_ai/<modele-llm-desire>"
```

View File

@@ -1,44 +0,0 @@
---
sidebar_position: 2
---
# 🤖 Backends LLM
OpenDevin peut fonctionner avec n'importe quel backend LLM.
Pour une liste complète des fournisseurs et des modèles LM disponibles, veuillez consulter la
[documentation litellm](https://docs.litellm.ai/docs/providers).
:::warning
OpenDevin émettra de nombreuses invitations au LLM que vous configurez. La plupart de ces LLM coûtent de l'argent -- assurez-vous de définir des limites de dépenses et de surveiller l'utilisation.
:::
La variable d'environnement `LLM_MODEL` contrôle le modèle utilisé dans les interactions programmatiques.
Mais en utilisant l'interface utilisateur OpenDevin, vous devrez choisir votre modèle dans la fenêtre des paramètres (la roue dentée en bas à gauche).
Les variables d'environnement suivantes peuvent être nécessaires pour certains LLM :
- `LLM_API_KEY`
- `LLM_BASE_URL`
- `LLM_EMBEDDING_MODEL`
- `LLM_EMBEDDING_DEPLOYMENT_NAME`
- `LLM_API_VERSION`
Nous avons quelques guides pour exécuter OpenDevin avec des fournisseurs de modèles spécifiques :
- [ollama](llms/localLLMs)
- [Azure](llms/azureLLMs)
Si vous utilisez un autre fournisseur, nous vous encourageons à ouvrir une PR pour partager votre configuration !
## Remarque sur les modèles alternatifs
Les meilleurs modèles sont GPT-4 et Claude 3. Les modèles locaux et open source actuels ne sont pas aussi puissants.
Lors de l'utilisation d'un modèle alternatif, vous pouvez constater des temps d'attente prolongés entre les messages,
des réponses de mauvaise qualité ou des erreurs sur des JSON mal formés. OpenDevin
ne peut être aussi puissant que les modèles qui le pilotent -- heureusement, les membres de notre équipe travaillent activement à la construction de meilleurs modèles open source !
## Réessais d'API et limites de taux
Certains LLM ont des limites de taux et peuvent nécessiter des réessais. OpenDevin réessaiera automatiquement les demandes s'il reçoit une erreur 429 ou une erreur de connexion API.
Vous pouvez définir les variables d'environnement `LLM_NUM_RETRIES`, `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` pour contrôler le nombre de réessais et le temps entre les réessais.
Par défaut, `LLM_NUM_RETRIES` est 5 et `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` sont respectivement de 3 secondes et 60 secondes.

View File

@@ -1,141 +0,0 @@
# LLM Local avec Ollama
Assurez-vous que le serveur Ollama est en cours d'exécution.
Pour des instructions détaillées de démarrage, consultez [ici](https://github.com/ollama/ollama)
Ce guide suppose que vous avez démarré ollama avec `ollama serve`. Si vous exécutez ollama différemment (par exemple, à l'intérieur de docker), les instructions pourraient devoir être modifiées. Veuillez noter que si vous utilisez WSL, la configuration par défaut de ollama bloque les requêtes des conteneurs docker. Voir [ici](#configuring-ollama-service-fr).
## Télécharger des modèles
Les noms des modèles Ollama peuvent être trouvés [ici](https://ollama.com/library). Pour un petit exemple, vous pouvez utiliser
le modèle `codellama:7b`. Des modèles plus grands offriront généralement de meilleures performances.
```bash
ollama pull codellama:7b
```
vous pouvez vérifier quels modèles vous avez téléchargés de cette manière :
```bash
~$ ollama list
NAME ID SIZE MODIFIED
codellama:7b 8fdf8f752f6e 3.8 GB 6 weeks ago
mistral:7b-instruct-v0.2-q4_K_M eb14864c7427 4.4 GB 2 weeks ago
starcoder2:latest f67ae0f64584 1.7 GB 19 hours ago
```
## Démarrer OpenDevin
### Docker
Utilisez les instructions [ici](../intro) pour démarrer OpenDevin en utilisant Docker.
Mais lors de l'exécution de `docker run`, vous devrez ajouter quelques arguments supplémentaires :
```bash
--add-host host.docker.internal:host-gateway \
-e LLM_API_KEY="ollama" \
-e LLM_BASE_URL="http://host.docker.internal:11434" \
```
Par exemple :
```bash
# Le répertoire que vous souhaitez qu'OpenDevin modifie. DOIT être un chemin absolu !
export WORKSPACE_BASE=$(pwd)/workspace
docker run \
-it \
--pull=always \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_USER_ID=$(id -u) \
-e LLM_API_KEY="ollama" \
-e LLM_BASE_URL="http://host.docker.internal:11434" \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
ghcr.io/opendevin/opendevin:main
```
Vous devriez maintenant pouvoir vous connecter à `http://localhost:3000/`
### Compiler à partir des sources
Utilisez les instructions dans [Development.md](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) pour compiler OpenDevin.
Assurez-vous que `config.toml` soit présent en exécutant `make setup-config` qui en créera un pour vous. Dans `config.toml`, saisissez les éléments suivants :
```
LLM_MODEL="ollama/codellama:7b"
LLM_API_KEY="ollama"
LLM_EMBEDDING_MODEL="local"
LLM_BASE_URL="http://localhost:11434"
WORKSPACE_BASE="./workspace"
WORKSPACE_DIR="$(pwd)/workspace"
```
Remplacez `LLM_MODEL` par celui de votre choix si nécessaire.
Fini ! Vous pouvez maintenant démarrer Devin avec : `make run` sans Docker. Vous devriez maintenant pouvoir vous connecter à `http://localhost:3000/`
## Sélection de votre modèle
Dans l'interface OpenDevin, cliquez sur l'icône des paramètres en bas à gauche.
Ensuite, dans l'entrée `Model`, saisissez `ollama/codellama:7b`, ou le nom du modèle que vous avez téléchargé précédemment.
S'il n'apparaît pas dans un menu déroulant, ce n'est pas grave, tapez-le simplement. Cliquez sur Enregistrer lorsque vous avez terminé.
Et maintenant, vous êtes prêt à démarrer !
## Configuration du service ollama (WSL){#configuring-ollama-service-fr}
La configuration par défaut pour ollama sous WSL ne sert que localhost. Cela signifie que vous ne pouvez pas l'atteindre depuis un conteneur docker, par exemple, il ne fonctionnera pas avec OpenDevin. Testons d'abord que ollama est en cours d'exécution correctement.
```bash
ollama list # obtenir la liste des modèles installés
curl http://localhost:11434/api/generate -d '{"model":"[NAME]","prompt":"hi"}'
#ex. curl http://localhost:11434/api/generate -d '{"model":"codellama:7b","prompt":"hi"}'
#ex. curl http://localhost:11434/api/generate -d '{"model":"codellama","prompt":"hi"}' #le tag est optionnel s'il n'y en a qu'un seul
```
Une fois cela fait, testez qu'il accepte les requêtes "externes", comme celles provenant d'un conteneur docker.
```bash
docker ps # obtenir la liste des conteneurs docker en cours d'exécution, pour un test le plus précis choisissez le conteneur de sandbox open devin.
docker exec [CONTAINER ID] curl http://host.docker.internal:11434/api/generate -d '{"model":"[NAME]","prompt":"hi"}'
#ex. docker exec cd9cc82f7a11 curl http://host.docker.internal:11434/api/generate -d '{"model":"codellama","prompt":"hi"}'
```
## Correction
Maintenant faisons en sorte que cela fonctionne. Modifiez /etc/systemd/system/ollama.service avec les privilèges sudo. (Le chemin peut varier selon la distribution Linux)
```bash
sudo vi /etc/systemd/system/ollama.service
```
ou
```bash
sudo nano /etc/systemd/system/ollama.service
```
Dans la section [Service], ajoutez ces lignes
```
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_ORIGINS=*"
```
Ensuite, sauvegardez, rechargez la configuration et redémarrez le service.
```bash
sudo systemctl daemon-reload
sudo systemctl restart ollama
```
Enfin, testez que ollama est accessible depuis le conteneur
```bash
ollama list # obtenir la liste des modèles installés
docker ps # obtenir la liste des conteneurs docker en cours d'exécution, pour un test le plus précis choisissez le conteneur de sandbox open devin.
docker exec [CONTAINER ID] curl http://host.docker.internal:11434/api/generate -d '{"model":"[NAME]","prompt":"hi"}'
```

View File

@@ -1,207 +0,0 @@
---
sidebar_position: 5
---
# 🚧 Dépannage
Il existe certains messages d'erreur qui sont souvent signalés par les utilisateurs.
Nous essaierons de rendre le processus d'installation plus facile et ces messages d'erreur
mieux à l'avenir. Mais pour l'instant, vous pouvez rechercher votre message d'erreur ci-dessous et voir s'il existe des solutions de contournement.
Pour chacun de ces messages d'erreur, **il existe un problème existant**. Veuillez ne pas
ouvrir un nouveau problème - commentez simplement dessus.
Si vous trouvez plus d'informations ou une solution de contournement pour l'un de ces problèmes, veuillez ouvrir un *PR* pour ajouter des détails à ce fichier.
:::tip
Si vous utilisez Windows et que vous rencontrez des problèmes, consultez notre [guide pour les utilisateurs de Windows (WSL)](troubleshooting/windows).
:::
## Impossible de se connecter à Docker
[Problème GitHub](https://github.com/OpenDevin/OpenDevin/issues/1226)
### Symptômes
```bash
Erreur lors de la création du contrôleur. Veuillez vérifier que Docker est en cours d'exécution et visitez `https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting` pour plus d'informations sur le débogage.
```
```bash
docker.errors.DockerException: Erreur lors de la récupération de la version de l'API du serveur : ('Connection aborted.', FileNotFoundError(2, 'Aucun fichier ou répertoire de ce type'))
```
### Détails
OpenDevin utilise un conteneur Docker pour effectuer son travail en toute sécurité, sans risquer de briser votre machine.
### Solutions de contournement
* Exécutez `docker ps` pour vous assurer que docker est en cours d'exécution
* Assurez-vous que vous n'avez pas besoin de `sudo` pour exécuter docker [voir ici](https://www.baeldung.com/linux/docker-run-without-sudo)
* Si vous êtes sur un Mac, vérifiez les [exigences en matière d'autorisations](https://docs.docker.com/desktop/mac/permission-requirements/) et envisagez particulièrement d'activer l'option `Allow the default Docker socket to be used` sous `Settings > Advanced` dans Docker Desktop.
* De plus, mettez à jour Docker vers la dernière version sous `Check for Updates`
## Impossible de se connecter à la boîte SSH
[Problème GitHub](https://github.com/OpenDevin/OpenDevin/issues/1156)
### Symptômes
```python
self.shell = DockerSSHBox(
...
pexpect.pxssh.ExceptionPxssh: Impossible d'établir une connexion avec l'hôte
```
### Détails
Par défaut, OpenDevin se connecte à un conteneur en cours d'exécution via SSH. Sur certaines machines,
en particulier Windows, cela semble échouer.
### Solutions de contournement
* Redémarrez votre ordinateur (parfois cela fonctionne)
* Assurez-vous d'avoir les dernières versions de WSL et Docker
* Vérifiez que votre distribution dans WSL est également à jour
* Essayez [ce guide de réinstallation](https://github.com/OpenDevin/OpenDevin/issues/1156#issuecomment-2064549427)
## Impossible de se connecter à LLM
[Problème GitHub](https://github.com/OpenDevin/OpenDevin/issues/1208)
### Symptômes
```python
File "/app/.venv/lib/python3.12/site-packages/openai/_exceptions.py", line 81, in __init__
super().__init__(message, response.request, body=body)
^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'request'
```
### Détails
[Problèmes GitHub](https://github.com/OpenDevin/OpenDevin/issues?q=is%3Aissue+is%3Aopen+404)
Cela se produit généralement avec les configurations de LLM *locales*, lorsque OpenDevin ne parvient pas à se connecter au serveur LLM.
Consultez notre guide pour [LLMs locaux](llms/localLLMs) pour plus d'informations.
### Solutions de contournement
* Vérifiez votre `base_url` dans votre config.toml (si elle existe) sous la section "llm"
* Vérifiez que ollama (ou tout autre LLM que vous utilisez) fonctionne correctement
* Assurez-vous d'utiliser `--add-host host.docker.internal:host-gateway` lorsque vous utilisez Docker
## `404 Ressource non trouvée`
### Symptômes
```python
Traceback (most recent call last):
File "/app/.venv/lib/python3.12/site-packages/litellm/llms/openai.py", line 414, in completion
raise e
File "/app/.venv/lib/python3.12/site-packages/litellm/llms/openai.py", line 373, in completion
response = openai_client.chat.completions.create(**data, timeout=timeout) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_utils/_utils.py", line 277, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/resources/chat/completions.py", line 579, in create
return self._post(
^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1232, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 921, in request
return self._request(
^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1012, in _request
raise self._make_status_error_from_response(err.response) from None
openai.NotFoundError: Code d'erreur : 404 - {'error': {'code': '404', 'message': 'Ressource non trouvée'}}
```
### Détails
Cela se produit lorsque LiteLLM (notre bibliothèque pour se connecter à différents fournisseurs de LLM) ne parvient pas à trouver
le point de terminaison API avec lequel vous essayez de vous connecter. Cela arrive le plus souvent aux utilisateurs de Azure ou ollama.
### Solutions de contournement
* Vérifiez que vous avez correctement défini `LLM_BASE_URL`
* Vérifiez que le modèle est correctement défini, en fonction des [docs de LiteLLM](https://docs.litellm.ai/docs/providers)
* Si vous êtes en cours d'exécution dans l'interface utilisateur, assurez-vous de définir le `model` dans le modal des paramètres
* Si vous êtes en cours d'exécution sans interface (via main.py), assurez-vous de définir `LLM_MODEL` dans votre env/config
* Assurez-vous de suivre les instructions spéciales de votre fournisseur de LLM
* [ollama](/fr/modules/usage/llms/localLLMs)
* [Azure](/fr/modules/usage/llms/azureLLMs)
* [Google](/fr/modules/usage/llms/googleLLMs)
* Assurez-vous que votre clé API est correcte
* Voyez si vous pouvez vous connecter au LLM en utilisant `curl`
* Essayez de [vous connecter via LiteLLM directement](https://github.com/BerriAI/litellm) pour tester votre configuration
## `make build` bloqué sur les installations de packages
### Symptômes
Installation de package bloquée sur `En attente...` sans aucun message d'erreur :
```bash
Opérations de package : 286 installations, 0 mises à jour, 0 suppressions
- Installation de certifi (2024.2.2) : En attente...
- Installation de h11 (0.14.0) : En attente...
- Installation de idna (3.7) : En attente...
- Installation de sniffio (1.3.1) : En attente...
- Installation de typing-extensions (4.11.0) : En attente...
```
### Détails
Dans de rares cas, `make build` peut sembler bloqué sur les installations de packages
sans aucun message d'erreur.
### Solutions de contournement
* Le gestionnaire de packages Poetry peut manquer d'un paramètre de configuration concernant
l'emplacement où doivent être recherchées les informations d'identification (keyring).
### Solution de contournement
Tout d'abord, vérifiez avec `env` si une valeur pour `PYTHON_KEYRING_BACKEND` existe.
Sinon, exécutez la commande ci-dessous pour la définir à une valeur connue et réessayez la construction :
```bash
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
```
## Les sessions ne sont pas restaurées
### Symptômes
OpenDevin demande généralement s'il faut reprendre ou commencer une nouvelle session lors de l'ouverture de l'interface utilisateur.
Mais cliquer sur "Reprendre" démarre toujours une toute nouvelle discussion.
### Détails
Avec une installation standard à ce jour, les données de session sont stockées en mémoire.
Actuellement, si le service OpenDevin est redémarré, les sessions précédentes deviennent
invalides (un nouveau secret est généré) et donc non récupérables.
### Solutions de contournement
* Modifiez la configuration pour rendre les sessions persistantes en éditant le fichier `config.toml`
(dans le dossier racine d'OpenDevin) en spécifiant un `file_store` et un
`file_store_path` absolu :
```toml
file_store="local"
file_store_path="/absolute/path/to/opendevin/cache/directory"
```
* Ajoutez un secret jwt fixe dans votre .bashrc, comme ci-dessous, afin que les id de session précédents
restent acceptés.
```bash
EXPORT JWT_SECRET=A_CONST_VALUE
```

View File

@@ -1,76 +0,0 @@
# Notes pour les utilisateurs de Windows et WSL
OpenDevin ne supporte Windows que via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
Veuillez vous assurer de lancer toutes les commandes à l'intérieur de votre terminal WSL.
## Dépannage
### Erreur : 'docker' n'a pas pu être trouvé dans cette distribution WSL 2.
Si vous utilisez Docker Desktop, assurez-vous de le démarrer avant d'exécuter toute commande docker depuis l'intérieur de WSL.
Docker doit également avoir l'option d'intégration WSL activée.
### Recommandation : Ne pas exécuter en tant qu'utilisateur root
Pour des raisons de sécurité, il est fortement recommandé de ne pas exécuter OpenDevin en tant qu'utilisateur root, mais en tant qu'utilisateur avec un UID non nul.
De plus, les sandboxes persistants ne seront pas pris en charge lors de l'exécution en tant que root et un message approprié pourrait apparaître lors du démarrage d'OpenDevin.
Références :
* [Pourquoi il est mauvais de se connecter en tant que root](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
* [Définir l'utilisateur par défaut dans WSL](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
Astuce pour la 2e référence : pour les utilisateurs d'Ubuntu, la commande pourrait en fait être "ubuntupreview" au lieu de "ubuntu".
### Échec de la création de l'utilisateur opendevin
Si vous rencontrez l'erreur suivante lors de l'installation :
```sh
Exception: Failed to create opendevin user in sandbox: 'useradd: UID 0 is not unique'
```
Vous pouvez la résoudre en exécutant :
```sh
export SANDBOX_USER_ID=1000
```
### Installation de Poetry
* Si vous rencontrez des problèmes pour exécuter Poetry même après l'avoir installé pendant le processus de construction, il peut être nécessaire d'ajouter son chemin binaire à votre environnement :
```sh
export PATH="$HOME/.local/bin:$PATH"
```
* Si `make build` s'arrête avec une erreur telle que :
```sh
ModuleNotFoundError: no module named <module-name>
```
Cela pourrait être un problème avec le cache de Poetry.
Essayez d'exécuter ces 2 commandes l'une après l'autre :
```sh
rm -r ~/.cache/pypoetry
make build
```
### L'objet NoneType n'a pas d'attribut 'request'
Si vous rencontrez des problèmes liés au réseau, tels que `NoneType object has no attribute 'request'` lors de l'exécution de `make run`, il peut être nécessaire de configurer vos paramètres réseau WSL2. Suivez ces étapes :
* Ouvrez ou créez le fichier `.wslconfig` situé à `C:\Users\%username%\.wslconfig` sur votre machine hôte Windows.
* Ajoutez la configuration suivante au fichier `.wslconfig` :
```sh
[wsl2]
networkingMode=mirrored
localhostForwarding=true
```
* Enregistrez le fichier `.wslconfig`.
* Redémarrez WSL2 complètement en quittant toute instance WSL2 en cours d'exécution et en exécutant la commande `wsl --shutdown` dans votre invite de commande ou terminal.
* Après avoir redémarré WSL, essayez d'exécuter `make run` à nouveau.
Le problème réseau devrait être résolu.

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