mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4af937dc4 | |||
| 95eb048672 | |||
| 832a82867f | |||
| 3eaa6fbcbb | |||
| e699f21f19 | |||
| 368f0b9434 | |||
| a27b0bb748 | |||
| a9dc3ce6f3 | |||
| a98f15ae95 | |||
| fa97e57360 | |||
| cb23bdbf62 | |||
| a36f6f5d33 | |||
| 6e2736f46b | |||
| 851df736b9 | |||
| 604c8d9888 | |||
| c2a284fde2 | |||
| 7783c10f82 | |||
| deef10b43e | |||
| 2a1cc9a089 | |||
| 4f853e79cf | |||
| 4aeb002901 | |||
| 80c0a33c6b | |||
| 1e58a12dbf | |||
| 8ec58d2618 | |||
| e9d788959d | |||
| 5bccaefc5f | |||
| 55fdee31ad | |||
| 3c5c214d87 | |||
| ae8cda1495 | |||
| 3d29ec0418 | |||
| b66a915de1 | |||
| 18d07bda89 | |||
| 783fea62a0 | |||
| 2c0a2dbc61 | |||
| f0271f9f91 | |||
| 5114230e53 | |||
| a6b3ce866d | |||
| 7d6cb69a51 | |||
| 1891fd88d5 | |||
| be1c2ad60d | |||
| 0c829cd067 | |||
| 28ab00946b | |||
| ec68af5b83 | |||
| 221035d39a | |||
| b31f7701eb | |||
| 91f313c914 | |||
| 36ff060c1a | |||
| cfae6821fa | |||
| 752ce8c4ea | |||
| cc6895a65c | |||
| 5538ee9bde | |||
| 9a0bae6d9b | |||
| de0f30f6cc | |||
| 6ae16dbc48 | |||
| b6be108f49 | |||
| ef813af9d7 | |||
| 909d7b45ef | |||
| e731048ccf | |||
| 2d52298a1d | |||
| 2f6167b953 | |||
| 78241d9d43 | |||
| b13a40c05c | |||
| ad2784d534 | |||
| 593b8d468b | |||
| ae105c2faf | |||
| 9207a8da01 | |||
| 53f64ffa06 | |||
| c59bcbbffd | |||
| cbf4c4b4c4 | |||
| 633ece5f9c | |||
| 9ca2007201 | |||
| b492b6293a | |||
| 0a6b26735b | |||
| dff0f1be13 | |||
| e7306b7226 | |||
| b118df606f | |||
| 602ffcdffb | |||
| ea9c785075 | |||
| 6ff50ed369 | |||
| d6327f99ce | |||
| 1c40ea5222 | |||
| 58d45a1a8a | |||
| ef6cdb7532 | |||
| f53a91b17c | |||
| 0eccf31604 | |||
| 3235836b00 | |||
| a605e59b7e | |||
| d1475e6e04 | |||
| acb430eef5 | |||
| b9a5be2569 | |||
| 9a2591d0f6 | |||
| 5bdacf738d | |||
| 37354dbc83 | |||
| 53afdc8496 | |||
| bf6fa8e5f4 | |||
| afc1752a5c | |||
| 40411db7bf | |||
| 46352e890b | |||
| 8e3e51e984 | |||
| 7da647a091 | |||
| 4292998ee2 | |||
| d18e6c85a0 | |||
| 34cccfe9cc | |||
| da8369c4d2 | |||
| 6618941422 | |||
| 1fe290adf9 | |||
| d76c425b76 | |||
| 43c187b949 | |||
| dd4ef67809 | |||
| a2f00584c1 | |||
| 108404f85a | |||
| f74b807342 | |||
| d60aa2b512 | |||
| 772734c884 | |||
| 6ce0eea4bb | |||
| fd3c83d1eb | |||
| 2610965f07 | |||
| 1e51bb9276 | |||
| 4add8a5595 | |||
| 2926c51839 | |||
| 04094e4201 | |||
| 99651e3249 | |||
| 8750d3e68b | |||
| bcdde452da | |||
| 4612e107c9 | |||
| 1bc1d306bd | |||
| c833d0b847 | |||
| 4c90dedcc7 | |||
| a982a2cc85 | |||
| ef828f3b9b | |||
| eab64cfcc2 | |||
| e978c77eb6 | |||
| 6e2e9ab13d | |||
| 75cecf68e0 | |||
| 7817d4c94f | |||
| b845a38169 | |||
| 3e3dcd52a8 | |||
| ec7be6ee51 | |||
| 6ef7e6eb0f | |||
| c5abc81bc9 | |||
| 50c2141d20 | |||
| 3f1b7117b1 | |||
| c5945bc5a8 | |||
| 75e1485cb0 | |||
| 0ecba83e53 | |||
| f85a73a50c | |||
| b2fdb963b6 | |||
| eafed447f8 | |||
| 061dfc0ee3 | |||
| 9cc76550d1 | |||
| 6896ffd654 | |||
| ee005cc2e9 | |||
| d4c136a48f | |||
| 1a045dc935 | |||
| 10933a2066 | |||
| b401be66f4 | |||
| 0abc35cf57 | |||
| f0ce2ffabf | |||
| a57a213c7c | |||
| 94a9ec76b0 | |||
| f950e3b48e | |||
| 5df85dcb57 | |||
| b0b44ed467 | |||
| 171764469d | |||
| 735fbbfe3e | |||
| 7ca560471b | |||
| c320d908e5 | |||
| 110b878dd9 | |||
| 5b6f622dad | |||
| 9856e76c1f | |||
| 49147bf13a | |||
| fff10402e3 | |||
| 35e7157565 | |||
| 0cb707bc9d | |||
| b3a45ed7fe | |||
| be1aef5863 | |||
| f55e5d00ca | |||
| e31f8b8322 | |||
| 70f3a3c80d | |||
| 77ce5f3e9d | |||
| 756b8c4149 | |||
| a0017070b3 | |||
| 7c66b45667 | |||
| 9d9329a43d | |||
| c8d717fb3a | |||
| a4a7ad6c87 | |||
| b6ff201780 | |||
| e89cc8f19b | |||
| 0fdbe1ee93 | |||
| 7313421ae4 | |||
| 6cce9c3c28 | |||
| adea9b3f32 | |||
| 52e21c20e3 | |||
| 15685f9aba | |||
| 9e59937180 | |||
| 2406b901df | |||
| a84d19f03c | |||
| 76abca361c | |||
| bf14b47890 | |||
| 817222061f | |||
| 6714000b2c | |||
| d1fd277ad4 | |||
| c604f8fcd2 | |||
| 135320861c | |||
| 123968f887 | |||
| 3cef8ee187 | |||
| 8d8ed0c3be | |||
| e4460a974d | |||
| 6ed17aad37 | |||
| 64ee5d404d | |||
| 1d8402a14a | |||
| dcb5d1ce0a | |||
| beb74a19f6 | |||
| 82a798990c | |||
| 3d53d363b4 | |||
| b84f25ab35 | |||
| 2771328036 | |||
| b028bd46bb | |||
| e28b3ef9e8 | |||
| ee66a1d5d1 | |||
| b13d4647ab | |||
| a48b02207f | |||
| 755a4072b6 | |||
| 00c0edae5f | |||
| ba8d8634ac | |||
| eba5ef8e67 | |||
| 4db4a84e2e | |||
| 49de262577 | |||
| e5f1dbf5e7 | |||
| f861db6675 | |||
| efd0d61e70 | |||
| d94b575cd4 | |||
| 8bfae8413e | |||
| 1d58917bc8 | |||
| 6a18cafa40 | |||
| 3abdc231c4 | |||
| c997289eb7 | |||
| 316a772849 | |||
| 93fe31a490 | |||
| 98adbf54ec | |||
| 5277c43c49 | |||
| 4477f08e6d | |||
| 24e61ead65 | |||
| 5244a34a1d | |||
| bde12f4a09 | |||
| 1787a7304e | |||
| 33e141e626 | |||
| e1a1c9a00c | |||
| b4cdebec06 | |||
| 1cbb16cfc2 | |||
| 968b4d71bd | |||
| 78cd2e5b47 | |||
| 18e6b0b2d0 | |||
| 0cf94a2718 | |||
| f8d4b1ab0d | |||
| a17308108c | |||
| 10b971c612 | |||
| e63910263c | |||
| 96151d9147 | |||
| 657b177b4e | |||
| 564739d1db | |||
| 26d82841d5 | |||
| 446eaec1e6 | |||
| 73693ba416 | |||
| ae7f208d51 | |||
| a60a6a40d6 | |||
| 06aae67fed | |||
| 4a72e83938 | |||
| 780db1e906 | |||
| 21fe8dc1eb | |||
| 45d1b6969a | |||
| 73e180638e | |||
| 09e8b11451 | |||
| 4cc462cc18 | |||
| af5bdf67aa | |||
| c2868985e4 | |||
| 88ef414e3a | |||
| 04676d17a8 | |||
| 242c4a0df6 | |||
| 4a2a35b6cf | |||
| fda21d2ce3 | |||
| 2ad9c55010 | |||
| dd666cf0a6 | |||
| 4bf4119259 | |||
| 6150ab6a3e | |||
| 356caf0960 |
@@ -0,0 +1,19 @@
|
||||
codecov:
|
||||
notify:
|
||||
wait_for_ci: true
|
||||
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
threshold: 100% # allow patch coverage to be lower than project coverage by any amount
|
||||
project:
|
||||
default:
|
||||
threshold: 5% # allow project coverage to drop at most 5%
|
||||
|
||||
comment: false
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
ignore:
|
||||
- "agenthub/SWE_agent/**" # SWE agent is deprecated
|
||||
@@ -28,8 +28,8 @@ body:
|
||||
- type: textarea
|
||||
id: current-version
|
||||
attributes:
|
||||
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).
|
||||
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).
|
||||
render: bash
|
||||
validations:
|
||||
required: true
|
||||
@@ -52,6 +52,12 @@ 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:
|
||||
@@ -66,4 +72,4 @@ body:
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Logs, Errors, Screenshots, and Additional Context
|
||||
description: 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.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/frontend" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -12,6 +12,7 @@ jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'OpenDevin/OpenDevin'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -42,7 +43,7 @@ jobs:
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.ref == 'refs/heads/main' && github.repository == 'OpenDevin/OpenDevin'
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: ["app", "evaluation", "sandbox"]
|
||||
image: ["app", "sandbox"]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
|
||||
@@ -7,32 +7,23 @@ concurrency:
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'frontend/**'
|
||||
- 'docs/**'
|
||||
- 'evaluation/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
integration-tests-on-linux:
|
||||
name: Integration Tests on Linux
|
||||
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"
|
||||
sandbox: ["ssh", "exec", "local"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -42,7 +33,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
@@ -54,23 +45,57 @@ jobs:
|
||||
- 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
|
||||
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 }}
|
||||
test_matrix_success:
|
||||
name: All Integration Tests Passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [integration-tests]
|
||||
|
||||
integration-tests-on-mac:
|
||||
name: Integration Tests on MacOS
|
||||
runs-on: macos-13
|
||||
if: contains(github.event.pull_request.title, 'mac') || contains(github.event.pull_request.title, 'Mac')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
sandbox: ["ssh"]
|
||||
steps:
|
||||
- run: echo Done!
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install
|
||||
|
||||
- name: Install & Start Docker
|
||||
run: |
|
||||
brew install colima docker
|
||||
colima start
|
||||
|
||||
# For testcontainers to find the Colima socket
|
||||
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
|
||||
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
|
||||
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
|
||||
- name: Run Integration Tests
|
||||
env:
|
||||
SANDBOX_TYPE: ${{ matrix.sandbox }}
|
||||
run: |
|
||||
TEST_IN_CI=true TEST_ONLY=true ./tests/integration/regenerate.sh
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -7,13 +7,20 @@ concurrency:
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'frontend/**'
|
||||
- 'docs/**'
|
||||
- 'evaluation/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-on-macos:
|
||||
name: Test on macOS
|
||||
runs-on: macos-13
|
||||
env:
|
||||
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
@@ -28,12 +35,13 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
cache: "poetry"
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install
|
||||
|
||||
- name: Install & Start Docker
|
||||
if: env.INSTALL_DOCKER == '1'
|
||||
run: |
|
||||
brew install colima docker
|
||||
colima start
|
||||
@@ -46,7 +54,7 @@ jobs:
|
||||
run: make build
|
||||
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit
|
||||
run: poetry run pytest --forked --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
@@ -55,6 +63,8 @@ jobs:
|
||||
test-on-linux:
|
||||
name: Test on Linux
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
@@ -69,7 +79,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
|
||||
@@ -78,15 +88,39 @@ jobs:
|
||||
run: make build
|
||||
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit
|
||||
run: poetry run pytest --forked --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_matrix_success:
|
||||
name: All Mac/Linux Tests Passed
|
||||
|
||||
test-for-sandbox:
|
||||
name: Test for Sandbox
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-on-macos, test-on-linux]
|
||||
steps:
|
||||
- run: echo Done!
|
||||
- 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 }}
|
||||
|
||||
@@ -126,6 +126,7 @@ env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
.env.bak
|
||||
venv.bak/
|
||||
*venv/
|
||||
|
||||
@@ -196,8 +197,15 @@ logs
|
||||
# agent
|
||||
.envrc
|
||||
/workspace
|
||||
/_test_workspace
|
||||
/debug
|
||||
cache
|
||||
|
||||
# configuration
|
||||
config.toml
|
||||
config.toml.bak
|
||||
evaluation/swe_bench/eval_workspace*
|
||||
evaluation/outputs
|
||||
evaluation/evaluation_outputs
|
||||
test_results*
|
||||
/_test_files_tmp/
|
||||
|
||||
@@ -94,3 +94,8 @@ poetry run pytest ./tests/unit/test_sandbox.py
|
||||
#### Integration tests
|
||||
|
||||
Please refer to [this README](./tests/integration/README.md) for details.
|
||||
|
||||
### 9. Add or update dependency
|
||||
|
||||
1. Add your dependency in `pyproject.toml` or use `peotry add xxx`
|
||||
2. Update the poetry.lock file via `poetry lock --no-update`
|
||||
@@ -7,7 +7,7 @@ BACKEND_PORT = 3000
|
||||
BACKEND_HOST = "127.0.0.1:$(BACKEND_PORT)"
|
||||
FRONTEND_PORT = 3001
|
||||
DEFAULT_WORKSPACE_DIR = "./workspace"
|
||||
DEFAULT_MODEL = "gpt-3.5-turbo-1106"
|
||||
DEFAULT_MODEL = "gpt-3.5-turbo"
|
||||
CONFIG_FILE = config.toml
|
||||
PRECOMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
|
||||
|
||||
@@ -22,7 +22,9 @@ RESET=$(shell tput -Txterm sgr0)
|
||||
build:
|
||||
@echo "$(GREEN)Building project...$(RESET)"
|
||||
@$(MAKE) -s check-dependencies
|
||||
ifeq ($(INSTALL_DOCKER),)
|
||||
@$(MAKE) -s pull-docker-image
|
||||
endif
|
||||
@$(MAKE) -s install-python-dependencies
|
||||
@$(MAKE) -s install-frontend-dependencies
|
||||
@$(MAKE) -s install-precommit-hooks
|
||||
@@ -35,7 +37,9 @@ check-dependencies:
|
||||
@$(MAKE) -s check-python
|
||||
@$(MAKE) -s check-npm
|
||||
@$(MAKE) -s check-nodejs
|
||||
ifeq ($(INSTALL_DOCKER),)
|
||||
@$(MAKE) -s check-docker
|
||||
endif
|
||||
@$(MAKE) -s check-poetry
|
||||
@echo "$(GREEN)Dependencies checked successfully.$(RESET)"
|
||||
|
||||
@@ -44,7 +48,11 @@ check-system:
|
||||
@if [ "$(shell uname)" = "Darwin" ]; then \
|
||||
echo "$(BLUE)macOS detected.$(RESET)"; \
|
||||
elif [ "$(shell uname)" = "Linux" ]; then \
|
||||
echo "$(BLUE)Linux detected.$(RESET)"; \
|
||||
if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Manjaro Linux detected.$(RESET)"; \
|
||||
else \
|
||||
echo "$(BLUE)Linux detected.$(RESET)"; \
|
||||
fi; \
|
||||
elif [ "$$(uname -r | grep -i microsoft)" ]; then \
|
||||
echo "$(BLUE)Windows Subsystem for Linux detected.$(RESET)"; \
|
||||
else \
|
||||
@@ -122,13 +130,20 @@ pull-docker-image:
|
||||
|
||||
install-python-dependencies:
|
||||
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
|
||||
poetry env use python3.11
|
||||
@if [ "$(shell uname)" = "Darwin" ]; then \
|
||||
echo "$(BLUE)Installing `chroma-hnswlib`...$(RESET)"; \
|
||||
echo "$(BLUE)Installing chroma-hnswlib...$(RESET)"; \
|
||||
export HNSWLIB_NO_NATIVE=1; \
|
||||
poetry run pip install chroma-hnswlib; \
|
||||
fi
|
||||
@poetry install --without evaluation
|
||||
@poetry run playwright install --with-deps chromium
|
||||
@poetry install
|
||||
@if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
|
||||
poetry run pip install playwright; \
|
||||
poetry run playwright install chromium; \
|
||||
else \
|
||||
poetry run playwright install --with-deps chromium; \
|
||||
fi
|
||||
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
|
||||
|
||||
install-frontend-dependencies:
|
||||
@@ -205,15 +220,24 @@ setup-config:
|
||||
@echo "$(GREEN)Config.toml setup completed.$(RESET)"
|
||||
|
||||
setup-config-prompts:
|
||||
@read -p "Enter your LLM Model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
|
||||
@echo "[core]" > $(CONFIG_FILE).tmp
|
||||
|
||||
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
|
||||
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
|
||||
echo "workspace_base=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@echo "" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@echo "[llm]" >> $(CONFIG_FILE).tmp
|
||||
@read -p "Enter your LLM model name, used for running without UI. Set the model in the UI after you start the app. (see https://docs.litellm.ai/docs/providers for full list) [default: $(DEFAULT_MODEL)]: " llm_model; \
|
||||
llm_model=$${llm_model:-$(DEFAULT_MODEL)}; \
|
||||
echo "LLM_MODEL=\"$$llm_model\"" > $(CONFIG_FILE).tmp
|
||||
echo "model=\"$$llm_model\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@read -p "Enter your LLM API key: " llm_api_key; \
|
||||
echo "LLM_API_KEY=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
|
||||
@read -p "Enter your LLM api key: " llm_api_key; \
|
||||
echo "api_key=\"$$llm_api_key\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
@read -p "Enter your LLM Base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
|
||||
if [[ ! -z "$$llm_base_url" ]]; then echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
|
||||
@read -p "Enter your LLM base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
|
||||
if [[ ! -z "$$llm_base_url" ]]; then echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
|
||||
|
||||
@echo "Enter your LLM Embedding Model"; \
|
||||
echo "Choices are:"; \
|
||||
@@ -227,22 +251,19 @@ setup-config-prompts:
|
||||
echo " - stable-code"; \
|
||||
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
|
||||
read -p "> " llm_embedding_model; \
|
||||
echo "LLM_EMBEDDING_MODEL=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
|
||||
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
|
||||
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
|
||||
read -p "Enter the local model URL for the embedding model (will set LLM_EMBEDDING_BASE_URL): " llm_embedding_base_url; \
|
||||
echo "LLM_EMBEDDING_BASE_URL=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the local model URL for the embedding model (will set llm.embedding_base_url): " llm_embedding_base_url; \
|
||||
echo "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
|
||||
read -p "Enter the Azure endpoint URL (will overwrite LLM_BASE_URL): " llm_base_url; \
|
||||
echo "LLM_BASE_URL=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure endpoint URL (will overwrite llm.base_url): " llm_base_url; \
|
||||
echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
|
||||
echo "LLM_EMBEDDING_DEPLOYMENT_NAME=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
|
||||
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
|
||||
read -p "Enter the Azure API Version: " llm_api_version; \
|
||||
echo "LLM_API_VERSION=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
|
||||
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
|
||||
fi
|
||||
|
||||
@read -p "Enter your workspace directory [default: $(DEFAULT_WORKSPACE_DIR)]: " workspace_dir; \
|
||||
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
|
||||
echo "WORKSPACE_BASE=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
|
||||
|
||||
# Clean up all caches
|
||||
clean:
|
||||
|
||||
@@ -18,46 +18,51 @@
|
||||
-->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/graphs/contributors"><img src="https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge" alt="Contributors"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/network/members"><img src="https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge" alt="Forks"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/stargazers"><img src="https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge" alt="Stargazers"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/issues"><img src="https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge" alt="Issues"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge" alt="MIT License"></a>
|
||||
</br>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/graphs/contributors"><img src="https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/network/members"><img src="https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge&color=blue" alt="Forks"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/stargazers"><img src="https://img.shields.io/github/stars/opendevin/opendevin?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/issues"><img src="https://img.shields.io/github/issues/opendevin/opendevin?style=for-the-badge&color=blue" alt="Issues"></a>
|
||||
<a href="https://github.com/OpenDevin/OpenDevin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/opendevin/opendevin?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2i1iqdag6-bVmvamiPA9EZUu7oCO6KhA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
|
||||
<br/>
|
||||
<a href="https://huggingface.co/spaces/OpenDevin/evaluation"><img src="https://img.shields.io/badge/SWE--bench%20Lite-25.0%25-green?style=for-the-badge" alt="SWE-bench "></a>
|
||||
<a href="https://codecov.io/github/opendevin/opendevin?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/opendevin/opendevin?style=for-the-badge"></a>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200" height="200">
|
||||
<h1 align="center">OpenDevin: Code Less, Make More</h1>
|
||||
<a href="https://opendevin.github.io/OpenDevin/"><img src="https://img.shields.io/badge/Documenation-OpenDevin-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
<a href="https://opendevin.github.io/OpenDevin/"><img src="https://img.shields.io/badge/Documentation-OpenDevin-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
## 🎯 Mission
|
||||
Welcome to OpenDevin, a platform for autonomous software engineers, powered by AI and LLMs.
|
||||
|
||||
Welcome to OpenDevin, an open-source project aiming to replicate Devin, an autonomous AI software engineer who is capable of executing complex engineering tasks and collaborating actively with users on software development projects. This project aspires to replicate, enhance, and innovate upon Devin through the power of the open-source community.
|
||||
OpenDevin agents collaborate with human developers to write code, fix bugs, and ship features.
|
||||
|
||||
## [🚀 Get Started](https://opendevin.github.io/OpenDevin/modules/usage/intro)
|
||||

|
||||
|
||||
To learn more and to use OpenDevin, **check out our [documentation](https://opendevin.github.io/OpenDevin/)**!
|
||||
## ⚡ Getting Started
|
||||
The easiest way to run OpenDevin is inside a Docker container. It works best with the most recent version of Docker, `26.0.0`.
|
||||
You must be using Linux, Mac OS, or WSL on Windows.
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ Back to Top ↑
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## ⚡ Quick Start
|
||||
You can run OpenDevin with Docker. It works best with the most recent
|
||||
version of Docker, `26.0.0`.
|
||||
To start the app, run these commands, replacing `$(pwd)/workspace` with the directory you want OpenDevin to work with.
|
||||
|
||||
```bash
|
||||
#The directory you want OpenDevin to modify. MUST be an absolute path!
|
||||
# The directory you want OpenDevin to work with. MUST be an absolute path!
|
||||
export WORKSPACE_BASE=$(pwd)/workspace;
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> OpenDevin runs bash commands within a Docker sandbox, so it should not affect your machine.
|
||||
> But your workspace directory will be attached to that sandbox, and files in the directory may be modified or deleted.
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-it \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
@@ -68,54 +73,53 @@ docker run \
|
||||
ghcr.io/opendevin/opendevin:0.5
|
||||
```
|
||||
|
||||
For troubleshooting and advanced configuration, see
|
||||
[the full documentation](https://opendevin.github.io/OpenDevin/).
|
||||
You'll find OpenDevin running at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## 🚀 Documentation
|
||||
|
||||
To learn more about the project, and for tips on using OpenDevin,
|
||||
**check out our [documentation](https://opendevin.github.io/OpenDevin/)**.
|
||||
|
||||
There you'll find resources on how to use different LLM providers (like ollama and Anthropic's Claude),
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
## 🤝 How to Contribute
|
||||
|
||||
OpenDevin is a community-driven project, and we welcome contributions from everyone. Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of software engineering with AI, there are many ways to get involved:
|
||||
OpenDevin is a community-driven project, and we welcome contributions from everyone.
|
||||
Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of
|
||||
software engineering with AI, there are many ways to get involved:
|
||||
|
||||
- **Code Contributions:** Help us develop the core functionalities, frontend interface, or sandboxing solutions.
|
||||
- **Code Contributions:** Help us develop new agents, core functionality, the frontend and other interfaces, or sandboxing solutions.
|
||||
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements.
|
||||
- **Feedback and Testing:** Use the OpenDevin toolset, report bugs, suggest features, or provide feedback on usability.
|
||||
|
||||
For details, please check [this document](./CONTRIBUTING.md).
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ Back to Top ↑
|
||||
</a>
|
||||
</p>
|
||||
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
## 🤖 Join Our Community
|
||||
|
||||
Now we have both Slack workspace for the collaboration on building OpenDevin and Discord server for discussion about anything related, e.g., this project, LLM, agent, etc.
|
||||
Whether you're a developer, a researcher, or simply enthusiastic about OpenDevin, we'd love to have you in our community.
|
||||
Let's make software engineering better together!
|
||||
|
||||
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2ggtwn3k5-PvAA2LUmqGHVZ~XzGq~ILw)
|
||||
- [Discord server](https://discord.gg/ESHStjSjD4)
|
||||
- [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.
|
||||
|
||||
If you would love to contribute, feel free to join our community (note that now there is no need to fill in the [form](https://forms.gle/758d5p6Ve8r2nxxq6)). Let's simplify software engineering together!
|
||||
|
||||
🐚 **Code less, make more with OpenDevin.**
|
||||
|
||||
[](https://star-history.com/#OpenDevin/OpenDevin&Date)
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ Back to Top ↑
|
||||
## 📈 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">
|
||||
<img src="https://api.star-history.com/svg?repos=OpenDevin/OpenDevin&type=Date" width="500" alt="Star History Chart">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📜 License
|
||||
|
||||
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ Back to Top ↑
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/opendevin/opendevin?style=for-the-badge
|
||||
[contributors-url]: https://github.com/OpenDevin/OpenDevin/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/opendevin/opendevin?style=for-the-badge
|
||||
|
||||
+21
-7
@@ -16,14 +16,18 @@ Every agent also has a `self.llm` which it can use to interact with the LLM conf
|
||||
See the [LiteLLM docs for `self.llm.completion`](https://docs.litellm.ai/docs/completion).
|
||||
|
||||
## State
|
||||
|
||||
The `state` contains:
|
||||
* A history of actions taken by the agent, as well as any observations (e.g. file content, command output) from those actions
|
||||
* A list of actions/observations that have happened since the most recent step
|
||||
* A [`plan`](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/plan.py), which contains the main goal
|
||||
* The agent can add and modify subtasks through the `AddTaskAction` and `ModifyTaskAction`
|
||||
|
||||
- A history of actions taken by the agent, as well as any observations (e.g. file content, command output) from those actions
|
||||
- A list of actions/observations that have happened since the most recent step
|
||||
- A [`root_task`](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/controller/state/task.py), which contains a plan of action
|
||||
- The agent can add and modify subtasks through the `AddTaskAction` and `ModifyTaskAction`
|
||||
|
||||
## Actions
|
||||
|
||||
Here is a list of available Actions, which can be returned by `agent.step()`:
|
||||
|
||||
- [`CmdRunAction`](../opendevin/action/bash.py) - Runs a command inside a sandboxed terminal
|
||||
- [`CmdKillAction`](../opendevin/action/bash.py) - Kills a background command
|
||||
- [`IPythonRunCellAction`](../opendevin/action/bash.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../opendevin/sandbox/plugins) as a requirement.
|
||||
@@ -35,40 +39,50 @@ Here is a list of available Actions, which can be returned by `agent.step()`:
|
||||
- [`ModifyTaskAction`](../opendevin/action/tasks.py) - Changes the state of a subtask
|
||||
- [`AgentThinkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history (as well as the chat log)
|
||||
- [`AgentTalkAction`](../opendevin/action/agent.py) - A no-op that allows the agent to add plaintext to the history and talk to the user.
|
||||
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
|
||||
- [`AgentRejectAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task
|
||||
- [`AgentFinishAction`](../opendevin/action/agent.py) - Stops the control loop, allowing the user to enter a new task
|
||||
- [`MessageAction`](../opendevin/action/message.py) - Represents a message from an agent or the user
|
||||
|
||||
You can use `action.to_dict()` and `action_from_dict` to serialize and deserialize actions.
|
||||
|
||||
## Observations
|
||||
|
||||
There are also several types of Observations. These are typically available in the step following the corresponding Action.
|
||||
But they may also appear as a result of asynchronous events (e.g. a message from the user, logs from a command running
|
||||
in the background).
|
||||
|
||||
Here is a list of available Observations:
|
||||
|
||||
- [`CmdOutputObservation`](../opendevin/observation/run.py)
|
||||
- [`BrowserOutputObservation`](../opendevin/observation/browse.py)
|
||||
- [`FileReadObservation`](../opendevin/observation/files.py)
|
||||
- [`FileWriteObservation`](../opendevin/observation/files.py)
|
||||
- [`UserMessageObservation`](../opendevin/observation/)
|
||||
- [`AgentRecallObservation`](../opendevin/observation/recall.py)
|
||||
- [`AgentErrorObservation`](../opendevin/observation/error.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.
|
||||
|
||||
## Interface
|
||||
|
||||
Every agent must implement the following methods:
|
||||
|
||||
### `step`
|
||||
|
||||
```
|
||||
def step(self, state: "State") -> "Action"
|
||||
```
|
||||
|
||||
`step` moves the agent forward one step towards its goal. This probably means
|
||||
sending a prompt to the LLM, then parsing the response into an `Action`.
|
||||
|
||||
### `search_memory`
|
||||
|
||||
```
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
```
|
||||
|
||||
`search_memory` should return a list of events that match the query. This will be used
|
||||
for the `recall` action.
|
||||
|
||||
|
||||
+28
-21
@@ -1,14 +1,12 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentThinkAction,
|
||||
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
|
||||
@@ -22,6 +20,8 @@ from .prompts import (
|
||||
|
||||
|
||||
class SWEAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
DEPRECATED = True
|
||||
"""
|
||||
An attempt to recreate swe_agent with output parsing, prompting style, and Application Computer Interface (ACI).
|
||||
|
||||
@@ -32,17 +32,11 @@ class SWEAgent(Agent):
|
||||
super().__init__(llm)
|
||||
self.memory_window = 4
|
||||
self.max_retries = 2
|
||||
self.running_memory: List[str] = []
|
||||
self.cur_file: str = ''
|
||||
self.cur_line: int = 0
|
||||
|
||||
def _remember(self, action: Action, observation: Observation) -> None:
|
||||
"""Agent has a limited memory of the few steps implemented as a queue"""
|
||||
memory = MEMORY_FORMAT(action.to_memory(), observation.to_memory())
|
||||
self.running_memory.append(memory)
|
||||
|
||||
def _think_act(self, messages: List[dict]) -> tuple[Action, str]:
|
||||
resp = self.llm.completion(
|
||||
def _think_act(self, messages: list[dict]) -> tuple[Action, str]:
|
||||
resp = self.llm.do_completion(
|
||||
messages=messages,
|
||||
temperature=0.05,
|
||||
)
|
||||
@@ -68,23 +62,36 @@ class SWEAgent(Agent):
|
||||
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)
|
||||
# retrieve short term memories from state.history, up to memory_window
|
||||
memory_window = min(self.memory_window, len(state.history))
|
||||
running_memory: list[str] = []
|
||||
for prev_action, obs in state.history[-memory_window:]:
|
||||
running_memory.append(
|
||||
MEMORY_FORMAT(event_to_memory(prev_action), event_to_memory(obs))
|
||||
)
|
||||
|
||||
prompt = STEP_PROMPT(state.plan.main_goal, self.cur_file, self.cur_line)
|
||||
goal = state.get_current_user_intent()
|
||||
|
||||
# always in the prompt if they exist: file and line
|
||||
prompt = STEP_PROMPT(goal, self.cur_file, self.cur_line)
|
||||
|
||||
# prepare messages
|
||||
msgs = [
|
||||
{'content': SYSTEM_MESSAGE, 'role': 'system'},
|
||||
{'content': prompt, 'role': 'user'},
|
||||
]
|
||||
|
||||
if len(self.running_memory) > 0:
|
||||
context = CONTEXT_PROMPT(self.running_memory, self.memory_window)
|
||||
# insert memories
|
||||
if len(running_memory) > 0:
|
||||
context = CONTEXT_PROMPT(running_memory, self.memory_window)
|
||||
msgs.insert(1, {'content': context, 'role': 'user'})
|
||||
# clrs = [''] * (len(msgs)-2) + ['\033[0;36m', '\033[0;35m']
|
||||
# print('\n\n'.join([c+m['content']+'\033[0m' for c, m in zip(clrs, msgs)]))
|
||||
|
||||
# send it over
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
# be robust with malformed responses
|
||||
start_msg_len = len(msgs)
|
||||
while not action and len(msgs) < self.max_retries + start_msg_len:
|
||||
error = NO_ACTION(thought)
|
||||
@@ -93,16 +100,16 @@ class SWEAgent(Agent):
|
||||
action, thought = self._think_act(messages=msgs)
|
||||
|
||||
if not action:
|
||||
action = AgentThinkAction(thought)
|
||||
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 search_memory(self, query: str) -> list[str]:
|
||||
# return [item for item in self.running_memory if query in item]
|
||||
raise NotImplementedError('Search_memory not implemented currently')
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Used to reset the agent"""
|
||||
self.running_memory = []
|
||||
super().reset()
|
||||
|
||||
@@ -2,21 +2,21 @@ import re
|
||||
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentEchoAction,
|
||||
AgentFinishAction,
|
||||
AgentThinkAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
)
|
||||
|
||||
from .prompts import COMMAND_USAGE, CUSTOM_DOCS
|
||||
|
||||
# commands: exit, read, write, browse, kill, search_file, search_dir
|
||||
|
||||
no_open_file_error = AgentEchoAction(
|
||||
'You are not currently in a file. You can use the read command to open a file and then use goto to navigate through it.')
|
||||
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):
|
||||
@@ -33,7 +33,9 @@ Try again using this format:
|
||||
"""
|
||||
|
||||
|
||||
def get_action_from_string(command_string: str, path: str, line: int, thoughts: str = '') -> Action | None:
|
||||
def get_action_from_string(
|
||||
command_string: str, path: str, line: int, thoughts: str = ''
|
||||
) -> Action | None:
|
||||
"""
|
||||
Parses the command string to find which command the agent wants to run
|
||||
Converts the command into a proper Action and returns
|
||||
@@ -46,7 +48,7 @@ def get_action_from_string(command_string: str, path: str, line: int, thoughts:
|
||||
return AgentFinishAction()
|
||||
|
||||
elif 'think' == cmd:
|
||||
return AgentThinkAction(' '.join(args))
|
||||
return MessageAction(' '.join(args))
|
||||
|
||||
elif 'scroll_up' == cmd:
|
||||
if not path:
|
||||
@@ -68,7 +70,7 @@ def get_action_from_string(command_string: str, path: str, line: int, thoughts:
|
||||
end = start + 100
|
||||
return FileReadAction(path, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'goto'))
|
||||
return MessageAction(invalid_error(command_string, 'goto'))
|
||||
|
||||
elif 'edit' == cmd:
|
||||
if not path:
|
||||
@@ -83,7 +85,7 @@ def get_action_from_string(command_string: str, path: str, line: int, thoughts:
|
||||
change = change[1:-1]
|
||||
return FileWriteAction(path, change, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'edit'))
|
||||
return MessageAction(invalid_error(command_string, 'edit'))
|
||||
|
||||
elif 'read' == cmd:
|
||||
rex = r'^read\s+(\S+)(?:\s+(\d+))?(?:\s+(-?\d+))?$'
|
||||
@@ -98,7 +100,7 @@ def get_action_from_string(command_string: str, path: str, line: int, thoughts:
|
||||
|
||||
return FileReadAction(file, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'read'))
|
||||
return MessageAction(invalid_error(command_string, 'read'))
|
||||
|
||||
elif 'write' == cmd:
|
||||
rex = r'^write\s+(\S+)\s+(.*?)\s*(\d+)?\s*(-?\d+)?$'
|
||||
@@ -118,7 +120,7 @@ def get_action_from_string(command_string: str, path: str, line: int, thoughts:
|
||||
|
||||
return FileWriteAction(file, content, start, end, thoughts)
|
||||
else:
|
||||
return AgentEchoAction(invalid_error(command_string, 'write'))
|
||||
return MessageAction(invalid_error(command_string, 'write'))
|
||||
|
||||
elif 'browse' == cmd:
|
||||
return BrowseURLAction(args[0].strip())
|
||||
@@ -129,13 +131,15 @@ def get_action_from_string(command_string: str, path: str, line: int, thoughts:
|
||||
if valid:
|
||||
return CmdRunAction(command_string)
|
||||
else:
|
||||
return AgentEchoAction(f'Invalid command structure for\n ```\n{command_string}\n```.\nTry again using this format:\n{CUSTOM_DOCS}')
|
||||
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 AgentEchoAction(content=obs)
|
||||
return MessageAction(content=obs)
|
||||
else:
|
||||
# run valid command
|
||||
return CmdRunAction(command_string)
|
||||
@@ -157,8 +161,7 @@ def parse_command(input_str: str, path: str, line: int):
|
||||
command_str = parts[1].strip()
|
||||
ind = 2 if len(parts) > 2 else 1
|
||||
accompanying_text = ''.join(parts[:-ind]).strip()
|
||||
action = get_action_from_string(
|
||||
command_str, path, line, accompanying_text)
|
||||
action = get_action_from_string(command_str, path, line, accompanying_text)
|
||||
if action:
|
||||
return action, accompanying_text
|
||||
return None, input_str # used for retry
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
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.",
|
||||
@@ -6,12 +5,12 @@ DEFAULT_COMMANDS_DICT = {
|
||||
'browse <url>': 'Returns the text version of any url, this can be useful to look up documentation or finding issues on github',
|
||||
'scroll_up': 'Takes no arguments. This will scroll up and show you the 100 lines above your current lines',
|
||||
'scroll_down': 'Takes no arguments. This will scroll down and show you the 100 lines below your current lines',
|
||||
'edit <start_line> <end_line> <changes>': 'This will modify lines in the currently open file. use start_line and end_line to designate which lines to change and then write the multiline changes',
|
||||
'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'
|
||||
'NOT ALLOWED': 'You cannot use interactive commands like python or node',
|
||||
}
|
||||
|
||||
COMMAND_USAGE = {
|
||||
@@ -25,8 +24,7 @@ COMMAND_USAGE = {
|
||||
'browse': 'Args:\n<url>\nUsage:\n```\nbrowse https://github.com/OpenDevin/OpenDevin\n```\nThis will fetch the Text elements from the given url and show them to you.',
|
||||
}
|
||||
|
||||
DEFAULT_COMMANDS = '\n'.join(
|
||||
[k + ' - ' + v for k, v in DEFAULT_COMMANDS_DICT.items()])
|
||||
DEFAULT_COMMANDS = '\n'.join([k + ' - ' + v for k, v in DEFAULT_COMMANDS_DICT.items()])
|
||||
|
||||
# from opendevin.parse_commands import parse_command_file
|
||||
# USE parse_command_file(filepath) to get the custom commands
|
||||
@@ -94,7 +92,7 @@ Notes:
|
||||
- To execute multiple commands you should write them down in your thoughts section so you can remember it on the next step and execute them then.
|
||||
- The only commands you are not capable of executing are interactive commands like `python` or `node` by themselves.
|
||||
- If you think that you have completed the task that has been given to you based on your previous actions and outputs then use ``` exit ``` as the command to let the system know that you are done.
|
||||
- DO NOT make any copies of your previous memories those will be provided to you at each step, making copies just wastes time and energy. Think smarter not harder.
|
||||
- DO NOT make any copies of your previous memories, those will be provided to you at each step, making copies just wastes time and energy. Think smarter not harder.
|
||||
- The write and edit commands requires proper indentation in the content section ex. `write hw.py def hello():\n print(\'Hello World\')` this is how you would have to format your write command.
|
||||
- The white spaces matter as the code changes will be added to the code so they must have proper syntax.
|
||||
|
||||
@@ -116,8 +114,8 @@ 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 are an autonomous coding agent, here to provide solutions for coding issues.
|
||||
You have been designed to assist with a wide range of programming tasks, from code editing and debugging to testing and deployment.
|
||||
You have access to a variety of tools and commands that you can use to help you solve problems efficiently.
|
||||
|
||||
{GENERAL_GUIDELINES}
|
||||
@@ -126,7 +124,8 @@ You have access to a variety of tools and commands that you can use to help you
|
||||
""".strip()
|
||||
|
||||
|
||||
def NO_ACTION(latest): return f"""
|
||||
def NO_ACTION(latest):
|
||||
return f"""
|
||||
You did not include any action to take in your most recent output:
|
||||
|
||||
===== Output ======
|
||||
@@ -154,7 +153,8 @@ def file_info(file: str, line: int):
|
||||
"""
|
||||
|
||||
|
||||
def STEP_PROMPT(task, file, line_num): return f"""
|
||||
def STEP_PROMPT(task, file, line_num):
|
||||
return f"""
|
||||
{RESPONSE_FORMAT}
|
||||
You are currently trying to complete this task:
|
||||
{task}
|
||||
@@ -171,8 +171,9 @@ Begin with your thought about the next step and then come up with an action to p
|
||||
""".strip()
|
||||
|
||||
|
||||
def unpack_dict(data: dict, restrict: list[str] = []):
|
||||
def unpack_dict(data: dict, restrict: list[str] | None = None):
|
||||
lines = []
|
||||
restrict = [] if restrict is None else restrict
|
||||
for key, value in data.items():
|
||||
if key in restrict:
|
||||
continue
|
||||
@@ -185,7 +186,8 @@ def unpack_dict(data: dict, restrict: list[str] = []):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def MEMORY_FORMAT(act, obs): return f"""
|
||||
def MEMORY_FORMAT(act, obs):
|
||||
return f"""
|
||||
Previous Action:
|
||||
{unpack_dict(act, ["content"])}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ load_dotenv()
|
||||
|
||||
from . import ( # noqa: E402
|
||||
SWE_agent,
|
||||
browsing_agent,
|
||||
codeact_agent,
|
||||
codeact_swe_agent,
|
||||
delegator_agent,
|
||||
dummy_agent,
|
||||
monologue_agent,
|
||||
@@ -20,10 +22,12 @@ from . import ( # noqa: E402
|
||||
__all__ = [
|
||||
'monologue_agent',
|
||||
'codeact_agent',
|
||||
'codeact_swe_agent',
|
||||
'planner_agent',
|
||||
'SWE_agent',
|
||||
'delegator_agent',
|
||||
'dummy_agent',
|
||||
'browsing_agent',
|
||||
]
|
||||
|
||||
for agent in all_microagents.values():
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .browsing_agent import BrowsingAgent
|
||||
|
||||
Agent.register('BrowsingAgent', BrowsingAgent)
|
||||
@@ -0,0 +1,167 @@
|
||||
import ast
|
||||
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from browsergym.utils.obs import flatten_axtree_to_str
|
||||
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
BrowseInteractiveAction,
|
||||
MessageAction,
|
||||
)
|
||||
from opendevin.events.observation import BrowserOutputObservation
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.runtime.plugins import (
|
||||
PluginRequirement,
|
||||
)
|
||||
|
||||
|
||||
def parse_response(response: str) -> Action:
|
||||
if '```' not in response:
|
||||
# unexpected response format, message back to user
|
||||
return MessageAction(response)
|
||||
thought = response.split('```')[0].strip()
|
||||
action_str = response.split('```')[1].strip()
|
||||
# handle send message to user function call in BrowserGym
|
||||
for sub_action in action_str.split('\n'):
|
||||
if 'send_msg_to_user(' in sub_action:
|
||||
tree = ast.parse(sub_action)
|
||||
args = tree.body[0].value.args # type: ignore
|
||||
return MessageAction(args[0].value)
|
||||
|
||||
return BrowseInteractiveAction(browser_actions=action_str, thought=thought)
|
||||
|
||||
|
||||
class BrowsingAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
An agent that interacts with the browser.
|
||||
"""
|
||||
|
||||
sandbox_plugins: list[PluginRequirement] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes a new instance of the BrowsingAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm)
|
||||
self.action_space = HighLevelActionSet(
|
||||
# see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
|
||||
subsets=[
|
||||
'chat',
|
||||
'bid',
|
||||
'nav',
|
||||
], # define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
|
||||
strict=False, # less strict on the parsing of the actions
|
||||
multiaction=True, # enable to agent to take multiple actions at once
|
||||
)
|
||||
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Resets the Browsing Agent.
|
||||
"""
|
||||
super().reset()
|
||||
self.cost_accumulator = 0
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
Performs one step using the Browsing Agent.
|
||||
This includes gathering information on previous steps and prompting the model to make a browsing command to execute.
|
||||
|
||||
Parameters:
|
||||
- state (State): used to get updated info
|
||||
|
||||
Returns:
|
||||
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
goal = state.get_current_user_intent()
|
||||
messages = []
|
||||
prev_actions = ''
|
||||
cur_axtree_txt = ''
|
||||
error_prefix = ''
|
||||
last_obs = None
|
||||
for prev_action, obs in state.history:
|
||||
if isinstance(prev_action, BrowseInteractiveAction):
|
||||
prev_actions += f'{prev_action.browser_actions}\n'
|
||||
last_obs = obs
|
||||
elif (
|
||||
isinstance(prev_action, MessageAction) and prev_action.source != 'user'
|
||||
):
|
||||
# agent has responded, task finish.
|
||||
return AgentFinishAction()
|
||||
|
||||
if isinstance(last_obs, BrowserOutputObservation):
|
||||
if last_obs.error:
|
||||
# add error recovery prompt prefix
|
||||
error_prefix = f'IMPORTANT! Last action is incorrect:\n{last_obs.last_browser_action}\nThink again with the current observation of the page.\n'
|
||||
cur_axtree_txt = flatten_axtree_to_str(last_obs.axtree_object)
|
||||
|
||||
system_msg = f"""\
|
||||
# Instructions
|
||||
Review the current state of the page and all other information to find the best
|
||||
possible next action to accomplish your goal. Your answer will be interpreted
|
||||
and executed by a program, make sure to follow the formatting instructions.
|
||||
|
||||
# Goal:
|
||||
{goal}
|
||||
|
||||
# Action Space
|
||||
{self.action_space.describe(with_long_description=False, with_examples=True)}
|
||||
"""
|
||||
|
||||
messages.append({'role': 'system', 'content': system_msg})
|
||||
|
||||
prompt = f"""\
|
||||
{error_prefix}
|
||||
|
||||
# Current Accessibility Tree:
|
||||
{cur_axtree_txt}
|
||||
|
||||
# Previous Actions
|
||||
{prev_actions}
|
||||
|
||||
Here is an example with chain of thought of a valid action when clicking on a button:
|
||||
"
|
||||
In order to accomplish my goal I need to click on the button with bid 12
|
||||
```click("12")```
|
||||
"
|
||||
""".strip()
|
||||
messages.append({'role': 'user', 'content': prompt})
|
||||
response = self.llm.completion(
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
)
|
||||
self.log_cost(response)
|
||||
action_resp = response['choices'][0]['message']['content']
|
||||
logger.info(prompt)
|
||||
logger.info(action_resp)
|
||||
return parse_response(action_resp)
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
raise NotImplementedError('Implement this abstract method')
|
||||
|
||||
def log_cost(self, response):
|
||||
# TODO: refactor to unified cost tracking
|
||||
try:
|
||||
cur_cost = self.llm.completion_cost(response)
|
||||
except Exception:
|
||||
cur_cost = 0
|
||||
self.cost_accumulator += cur_cost
|
||||
logger.info(
|
||||
'Cost: %.2f USD | Accumulated Cost: %.2f USD',
|
||||
cur_cost,
|
||||
self.cost_accumulator,
|
||||
)
|
||||
@@ -0,0 +1,785 @@
|
||||
import abc
|
||||
import difflib
|
||||
import logging
|
||||
import platform
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, dataclass
|
||||
from textwrap import dedent
|
||||
from typing import Literal, Union
|
||||
from warnings import warn
|
||||
|
||||
from browsergym.core.action.base import AbstractActionSet
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from browsergym.core.action.python import PythonActionSet
|
||||
|
||||
from opendevin.runtime.browser.browser_env import BrowserEnv
|
||||
|
||||
from .utils import (
|
||||
ParseError,
|
||||
parse_html_tags_raise,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Flags:
|
||||
use_html: bool = True
|
||||
use_ax_tree: bool = False
|
||||
drop_ax_tree_first: bool = True # This flag is no longer active TODO delete
|
||||
use_thinking: bool = False
|
||||
use_error_logs: bool = False
|
||||
use_past_error_logs: bool = False
|
||||
use_history: bool = False
|
||||
use_action_history: bool = False
|
||||
use_memory: bool = False
|
||||
use_diff: bool = False
|
||||
html_type: str = 'pruned_html'
|
||||
use_concrete_example: bool = True
|
||||
use_abstract_example: bool = False
|
||||
multi_actions: bool = False
|
||||
action_space: Literal[
|
||||
'python', 'bid', 'coord', 'bid+coord', 'bid+nav', 'coord+nav', 'bid+coord+nav'
|
||||
] = 'bid'
|
||||
is_strict: bool = False
|
||||
# This flag will be automatically disabled `if not chat_model_args.has_vision()`
|
||||
use_screenshot: bool = True
|
||||
enable_chat: bool = False
|
||||
max_prompt_tokens: int = 100_000
|
||||
extract_visible_tag: bool = False
|
||||
extract_coords: Literal['False', 'center', 'box'] = 'False'
|
||||
extract_visible_elements_only: bool = False
|
||||
demo_mode: Literal['off', 'default', 'only_visible_elements'] = 'off'
|
||||
|
||||
def copy(self):
|
||||
return deepcopy(self)
|
||||
|
||||
def asdict(self):
|
||||
"""Helper for JSON serializble requirement."""
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(self, flags_dict):
|
||||
"""Helper for JSON serializble requirement."""
|
||||
if isinstance(flags_dict, Flags):
|
||||
return flags_dict
|
||||
|
||||
if not isinstance(flags_dict, dict):
|
||||
raise ValueError(
|
||||
f'Unregcognized type for flags_dict of type {type(flags_dict)}.'
|
||||
)
|
||||
return Flags(**flags_dict)
|
||||
|
||||
|
||||
class PromptElement:
|
||||
"""Base class for all prompt elements. Prompt elements can be hidden.
|
||||
|
||||
Prompt elements are used to build the prompt. Use flags to control which
|
||||
prompt elements are visible. We use class attributes as a convenient way
|
||||
to implement static prompts, but feel free to override them with instance
|
||||
attributes or @property decorator."""
|
||||
|
||||
_prompt = ''
|
||||
_abstract_ex = ''
|
||||
_concrete_ex = ''
|
||||
|
||||
def __init__(self, visible: bool = True) -> None:
|
||||
"""Prompt element that can be hidden.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
visible : bool, optional
|
||||
Whether the prompt element should be visible, by default True. Can
|
||||
be a callable that returns a bool. This is useful when a specific
|
||||
flag changes during a shrink iteration.
|
||||
"""
|
||||
self._visible = visible
|
||||
|
||||
@property
|
||||
def prompt(self):
|
||||
"""Avoid overriding this method. Override _prompt instead."""
|
||||
return self._hide(self._prompt)
|
||||
|
||||
@property
|
||||
def abstract_ex(self):
|
||||
"""Useful when this prompt element is requesting an answer from the llm.
|
||||
Provide an abstract example of the answer here. See Memory for an
|
||||
example.
|
||||
|
||||
Avoid overriding this method. Override _abstract_ex instead
|
||||
"""
|
||||
return self._hide(self._abstract_ex)
|
||||
|
||||
@property
|
||||
def concrete_ex(self):
|
||||
"""Useful when this prompt element is requesting an answer from the llm.
|
||||
Provide a concrete example of the answer here. See Memory for an
|
||||
example.
|
||||
|
||||
Avoid overriding this method. Override _concrete_ex instead
|
||||
"""
|
||||
return self._hide(self._concrete_ex)
|
||||
|
||||
@property
|
||||
def is_visible(self):
|
||||
"""Handle the case where visible is a callable."""
|
||||
visible = self._visible
|
||||
if callable(visible):
|
||||
visible = visible()
|
||||
return visible
|
||||
|
||||
def _hide(self, value):
|
||||
"""Return value if visible is True, else return empty string."""
|
||||
if self.is_visible:
|
||||
return value
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _parse_answer(self, text_answer) -> dict:
|
||||
if self.is_visible:
|
||||
return self._parse_answer(text_answer)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class Shrinkable(PromptElement, abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def shrink(self) -> None:
|
||||
"""Implement shrinking of this prompt element.
|
||||
|
||||
You need to recursively call all shrinkable elements that are part of
|
||||
this prompt. You can also implement a shriking startegy for this prompt.
|
||||
Shrinking is can be called multiple times to progressively shrink the
|
||||
prompt until it fits max_tokens. Default max shrink iterations is 20.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Truncater(Shrinkable):
|
||||
"""A prompt element that can be truncated to fit the context length of the LLM.
|
||||
Of course, it will be great that we never have to use the functionality here to `shrink()` the prompt.
|
||||
Extend this class for prompt elements that can be truncated. Usually long observations such as AxTree or HTML.
|
||||
"""
|
||||
|
||||
def __init__(self, visible, shrink_speed=0.3, start_truncate_iteration=10):
|
||||
super().__init__(visible=visible)
|
||||
self.shrink_speed = shrink_speed # the percentage shrinked in each iteration
|
||||
self.start_truncate_iteration = (
|
||||
start_truncate_iteration # the iteration to start truncating
|
||||
)
|
||||
self.shrink_calls = 0
|
||||
self.deleted_lines = 0
|
||||
|
||||
def shrink(self) -> None:
|
||||
if self.is_visible and self.shrink_calls >= self.start_truncate_iteration:
|
||||
# remove the fraction of _prompt
|
||||
lines = self._prompt.splitlines()
|
||||
new_line_count = int(len(lines) * (1 - self.shrink_speed))
|
||||
self.deleted_lines += len(lines) - new_line_count
|
||||
self._prompt = '\n'.join(lines[:new_line_count])
|
||||
self._prompt += (
|
||||
f'\n... Deleted {self.deleted_lines} lines to reduce prompt size.'
|
||||
)
|
||||
|
||||
self.shrink_calls += 1
|
||||
|
||||
|
||||
def fit_tokens(
|
||||
shrinkable: Shrinkable,
|
||||
max_prompt_chars=None,
|
||||
max_iterations=20,
|
||||
):
|
||||
"""Shrink a prompt element until it fits max_tokens.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shrinkable : Shrinkable
|
||||
The prompt element to shrink.
|
||||
max_prompt_chars : int
|
||||
The maximum number of chars allowed.
|
||||
max_iterations : int, optional
|
||||
The maximum number of shrink iterations, by default 20.
|
||||
model_name : str, optional
|
||||
The name of the model used when tokenizing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str : the prompt after shrinking.
|
||||
"""
|
||||
|
||||
if max_prompt_chars is None:
|
||||
return shrinkable.prompt
|
||||
|
||||
for _ in range(max_iterations):
|
||||
prompt = shrinkable.prompt
|
||||
if isinstance(prompt, str):
|
||||
prompt_str = prompt
|
||||
elif isinstance(prompt, list):
|
||||
prompt_str = '\n'.join([p['text'] for p in prompt if p['type'] == 'text'])
|
||||
else:
|
||||
raise ValueError(f'Unrecognized type for prompt: {type(prompt)}')
|
||||
n_chars = len(prompt_str)
|
||||
if n_chars <= max_prompt_chars:
|
||||
return prompt
|
||||
shrinkable.shrink()
|
||||
|
||||
logging.info(
|
||||
dedent(
|
||||
f"""\
|
||||
After {max_iterations} shrink iterations, the prompt is still
|
||||
{len(prompt_str)} chars (greater than {max_prompt_chars}). Returning the prompt as is."""
|
||||
)
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
class HTML(Truncater):
|
||||
def __init__(self, html, visible: bool = True, prefix='') -> None:
|
||||
super().__init__(visible=visible, start_truncate_iteration=5)
|
||||
self._prompt = f'\n{prefix}HTML:\n{html}\n'
|
||||
|
||||
|
||||
class AXTree(Truncater):
|
||||
def __init__(
|
||||
self, ax_tree, visible: bool = True, coord_type=None, prefix=''
|
||||
) -> None:
|
||||
super().__init__(visible=visible, start_truncate_iteration=10)
|
||||
if coord_type == 'center':
|
||||
coord_note = """\
|
||||
Note: center coordinates are provided in parenthesis and are
|
||||
relative to the top left corner of the page.\n\n"""
|
||||
elif coord_type == 'box':
|
||||
coord_note = """\
|
||||
Note: bounding box of each object are provided in parenthesis and are
|
||||
relative to the top left corner of the page.\n\n"""
|
||||
else:
|
||||
coord_note = ''
|
||||
self._prompt = f'\n{prefix}AXTree:\n{coord_note}{ax_tree}\n'
|
||||
|
||||
|
||||
class Error(PromptElement):
|
||||
def __init__(self, error, visible: bool = True, prefix='') -> None:
|
||||
super().__init__(visible=visible)
|
||||
self._prompt = f'\n{prefix}Error from previous action:\n{error}\n'
|
||||
|
||||
|
||||
class Observation(Shrinkable):
|
||||
"""Observation of the current step.
|
||||
|
||||
Contains the html, the accessibility tree and the error logs.
|
||||
"""
|
||||
|
||||
def __init__(self, obs, flags: Flags) -> None:
|
||||
super().__init__()
|
||||
self.flags = flags
|
||||
self.obs = obs
|
||||
self.html = HTML(obs[flags.html_type], visible=flags.use_html, prefix='## ')
|
||||
self.ax_tree = AXTree(
|
||||
obs['axtree_txt'],
|
||||
visible=flags.use_ax_tree,
|
||||
coord_type=flags.extract_coords,
|
||||
prefix='## ',
|
||||
)
|
||||
self.error = Error(
|
||||
obs['last_action_error'],
|
||||
visible=flags.use_error_logs and obs['last_action_error'],
|
||||
prefix='## ',
|
||||
)
|
||||
|
||||
def shrink(self):
|
||||
self.ax_tree.shrink()
|
||||
self.html.shrink()
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
return f'\n# Observation of current step:\n{self.html.prompt}{self.ax_tree.prompt}{self.error.prompt}\n\n'
|
||||
|
||||
def add_screenshot(self, prompt):
|
||||
if self.flags.use_screenshot:
|
||||
if isinstance(prompt, str):
|
||||
prompt = [{'type': 'text', 'text': prompt}]
|
||||
img_url = BrowserEnv.image_to_jpg_base64_url(
|
||||
self.obs['screenshot'], add_data_prefix=True
|
||||
)
|
||||
prompt.append({'type': 'image_url', 'image_url': img_url})
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
class MacNote(PromptElement):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(visible=platform.system() == 'Darwin')
|
||||
self._prompt = '\nNote: you are on mac so you should use Meta instead of Control for Control+C etc.\n'
|
||||
|
||||
|
||||
class BeCautious(PromptElement):
|
||||
def __init__(self, visible: bool = True) -> None:
|
||||
super().__init__(visible=visible)
|
||||
self._prompt = """\
|
||||
\nBe very cautious. Avoid submitting anything before verifying the effect of your
|
||||
actions. Take the time to explore the effect of safe actions first. For example
|
||||
you can fill a few elements of a form, but don't click submit before verifying
|
||||
that everything was filled correctly.\n"""
|
||||
|
||||
|
||||
class GoalInstructions(PromptElement):
|
||||
def __init__(self, goal, visible: bool = True) -> None:
|
||||
super().__init__(visible)
|
||||
self._prompt = f"""\
|
||||
# Instructions
|
||||
Review the current state of the page and all other information to find the best
|
||||
possible next action to accomplish your goal. Your answer will be interpreted
|
||||
and executed by a program, make sure to follow the formatting instructions.
|
||||
|
||||
## Goal:
|
||||
{goal}
|
||||
"""
|
||||
|
||||
|
||||
class ChatInstructions(PromptElement):
|
||||
def __init__(self, chat_messages, visible: bool = True) -> None:
|
||||
super().__init__(visible)
|
||||
self._prompt = """\
|
||||
# Instructions
|
||||
|
||||
You are a UI Assistant, your goal is to help the user perform tasks using a web browser. You can
|
||||
communicate with the user via a chat, in which the user gives you instructions and in which you
|
||||
can send back messages. You have access to a web browser that both you and the user can see,
|
||||
and with which only you can interact via specific commands.
|
||||
|
||||
Review the instructions from the user, the current state of the page and all other information
|
||||
to find the best possible next action to accomplish your goal. Your answer will be interpreted
|
||||
and executed by a program, make sure to follow the formatting instructions.
|
||||
|
||||
## Chat messages:
|
||||
|
||||
"""
|
||||
self._prompt += '\n'.join(
|
||||
[
|
||||
f"""\
|
||||
- [{msg['role']}] {msg['message']}"""
|
||||
for msg in chat_messages
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SystemPrompt(PromptElement):
|
||||
_prompt = """\
|
||||
You are an agent trying to solve a web task based on the content of the page and
|
||||
a user instructions. You can interact with the page and explore. Each time you
|
||||
submit an action it will be sent to the browser and you will receive a new page."""
|
||||
|
||||
|
||||
class MainPrompt(Shrinkable):
|
||||
def __init__(
|
||||
self,
|
||||
obs_history,
|
||||
actions,
|
||||
memories,
|
||||
thoughts,
|
||||
flags: Flags,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.flags = flags
|
||||
self.history = History(obs_history, actions, memories, thoughts, flags)
|
||||
if self.flags.enable_chat:
|
||||
self.instructions: Union[ChatInstructions, GoalInstructions] = (
|
||||
ChatInstructions(obs_history[-1]['chat_messages'])
|
||||
)
|
||||
else:
|
||||
if (
|
||||
'chat_messages' in obs_history[-1]
|
||||
and sum(
|
||||
[msg['role'] == 'user' for msg in obs_history[-1]['chat_messages']]
|
||||
)
|
||||
> 1
|
||||
):
|
||||
logging.warning(
|
||||
'Agent is in goal mode, but multiple user messages are present in the chat. Consider switching to `enable_chat=True`.'
|
||||
)
|
||||
self.instructions = GoalInstructions(obs_history[-1]['goal'])
|
||||
|
||||
self.obs = Observation(obs_history[-1], self.flags)
|
||||
self.action_space = ActionSpace(self.flags)
|
||||
|
||||
self.think = Think(visible=flags.use_thinking)
|
||||
self.memory = Memory(visible=flags.use_memory)
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
prompt = f"""\
|
||||
{self.instructions.prompt}\
|
||||
{self.obs.prompt}\
|
||||
{self.history.prompt}\
|
||||
{self.action_space.prompt}\
|
||||
{self.think.prompt}\
|
||||
{self.memory.prompt}\
|
||||
"""
|
||||
|
||||
if self.flags.use_abstract_example:
|
||||
prompt += f"""
|
||||
# Abstract Example
|
||||
|
||||
Here is an abstract version of the answer with description of the content of
|
||||
each tag. Make sure you follow this structure, but replace the content with your
|
||||
answer:
|
||||
{self.think.abstract_ex}\
|
||||
{self.memory.abstract_ex}\
|
||||
{self.action_space.abstract_ex}\
|
||||
"""
|
||||
|
||||
if self.flags.use_concrete_example:
|
||||
prompt += f"""
|
||||
# Concrete Example
|
||||
|
||||
Here is a concrete example of how to format your answer.
|
||||
Make sure to follow the template with proper tags:
|
||||
{self.think.concrete_ex}\
|
||||
{self.memory.concrete_ex}\
|
||||
{self.action_space.concrete_ex}\
|
||||
"""
|
||||
return self.obs.add_screenshot(prompt)
|
||||
|
||||
def shrink(self):
|
||||
self.history.shrink()
|
||||
self.obs.shrink()
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
ans_dict = {}
|
||||
ans_dict.update(self.think._parse_answer(text_answer))
|
||||
ans_dict.update(self.memory._parse_answer(text_answer))
|
||||
ans_dict.update(self.action_space._parse_answer(text_answer))
|
||||
return ans_dict
|
||||
|
||||
|
||||
class ActionSpace(PromptElement):
|
||||
def __init__(self, flags: Flags) -> None:
|
||||
super().__init__()
|
||||
self.flags = flags
|
||||
self.action_space = _get_action_space(flags)
|
||||
|
||||
self._prompt = (
|
||||
f'# Action space:\n{self.action_space.describe()}{MacNote().prompt}\n'
|
||||
)
|
||||
self._abstract_ex = f"""
|
||||
<action>
|
||||
{self.action_space.example_action(abstract=True)}
|
||||
</action>
|
||||
"""
|
||||
self._concrete_ex = f"""
|
||||
<action>
|
||||
{self.action_space.example_action(abstract=False)}
|
||||
</action>
|
||||
"""
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
ans_dict = parse_html_tags_raise(
|
||||
text_answer, keys=['action'], merge_multiple=True
|
||||
)
|
||||
|
||||
try:
|
||||
# just check if action can be mapped to python code but keep action as is
|
||||
# the environment will be responsible for mapping it to python
|
||||
self.action_space.to_python_code(ans_dict['action'])
|
||||
except Exception as e:
|
||||
raise ParseError(
|
||||
f'Error while parsing action\n: {e}\n'
|
||||
'Make sure your answer is restricted to the allowed actions.'
|
||||
)
|
||||
|
||||
return ans_dict
|
||||
|
||||
|
||||
def _get_action_space(flags: Flags) -> AbstractActionSet:
|
||||
match flags.action_space:
|
||||
case 'python':
|
||||
action_space = PythonActionSet(strict=flags.is_strict)
|
||||
if flags.multi_actions:
|
||||
warn(
|
||||
f'Flag action_space={repr(flags.action_space)} incompatible with multi_actions={repr(flags.multi_actions)}.'
|
||||
)
|
||||
if flags.demo_mode != 'off':
|
||||
warn(
|
||||
f'Flag action_space={repr(flags.action_space)} incompatible with demo_mode={repr(flags.demo_mode)}.'
|
||||
)
|
||||
return action_space
|
||||
case 'bid':
|
||||
action_subsets = ['chat', 'bid']
|
||||
case 'coord':
|
||||
action_subsets = ['chat', 'coord']
|
||||
case 'bid+coord':
|
||||
action_subsets = ['chat', 'bid', 'coord']
|
||||
case 'bid+nav':
|
||||
action_subsets = ['chat', 'bid', 'nav']
|
||||
case 'coord+nav':
|
||||
action_subsets = ['chat', 'coord', 'nav']
|
||||
case 'bid+coord+nav':
|
||||
action_subsets = ['chat', 'bid', 'coord', 'nav']
|
||||
case _:
|
||||
raise NotImplementedError(
|
||||
f'Unknown action_space {repr(flags.action_space)}'
|
||||
)
|
||||
|
||||
action_space = HighLevelActionSet(
|
||||
subsets=action_subsets,
|
||||
multiaction=flags.multi_actions,
|
||||
strict=flags.is_strict,
|
||||
demo_mode=flags.demo_mode,
|
||||
)
|
||||
|
||||
return action_space
|
||||
|
||||
|
||||
class Memory(PromptElement):
|
||||
_prompt = '' # provided in the abstract and concrete examples
|
||||
|
||||
_abstract_ex = """
|
||||
<memory>
|
||||
Write down anything you need to remember for next steps. You will be presented
|
||||
with the list of previous memories and past actions.
|
||||
</memory>
|
||||
"""
|
||||
|
||||
_concrete_ex = """
|
||||
<memory>
|
||||
I clicked on bid 32 to activate tab 2. The accessibility tree should mention
|
||||
focusable for elements of the form at next step.
|
||||
</memory>
|
||||
"""
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
return parse_html_tags_raise(
|
||||
text_answer, optional_keys=['memory'], merge_multiple=True
|
||||
)
|
||||
|
||||
|
||||
class Think(PromptElement):
|
||||
_prompt = ''
|
||||
|
||||
_abstract_ex = """
|
||||
<think>
|
||||
Think step by step. If you need to make calculations such as coordinates, write them here. Describe the effect
|
||||
that your previous action had on the current content of the page.
|
||||
</think>
|
||||
"""
|
||||
_concrete_ex = """
|
||||
<think>
|
||||
My memory says that I filled the first name and last name, but I can't see any
|
||||
content in the form. I need to explore different ways to fill the form. Perhaps
|
||||
the form is not visible yet or some fields are disabled. I need to replan.
|
||||
</think>
|
||||
"""
|
||||
|
||||
def _parse_answer(self, text_answer):
|
||||
return parse_html_tags_raise(
|
||||
text_answer, optional_keys=['think'], merge_multiple=True
|
||||
)
|
||||
|
||||
|
||||
def diff(previous, new):
|
||||
"""Return a string showing the difference between original and new.
|
||||
|
||||
If the difference is above diff_threshold, return the diff string."""
|
||||
|
||||
if previous == new:
|
||||
return 'Identical', []
|
||||
|
||||
if len(previous) == 0 or previous is None:
|
||||
return 'previous is empty', []
|
||||
|
||||
diff_gen = difflib.ndiff(previous.splitlines(), new.splitlines())
|
||||
|
||||
diff_lines = []
|
||||
plus_count = 0
|
||||
minus_count = 0
|
||||
for line in diff_gen:
|
||||
if line.strip().startswith('+'):
|
||||
diff_lines.append(line)
|
||||
plus_count += 1
|
||||
elif line.strip().startswith('-'):
|
||||
diff_lines.append(line)
|
||||
minus_count += 1
|
||||
else:
|
||||
continue
|
||||
|
||||
header = f'{plus_count} lines added and {minus_count} lines removed:'
|
||||
|
||||
return header, diff_lines
|
||||
|
||||
|
||||
class Diff(Shrinkable):
|
||||
def __init__(
|
||||
self, previous, new, prefix='', max_line_diff=20, shrink_speed=2, visible=True
|
||||
) -> None:
|
||||
super().__init__(visible=visible)
|
||||
self.max_line_diff = max_line_diff
|
||||
self.header, self.diff_lines = diff(previous, new)
|
||||
self.shrink_speed = shrink_speed
|
||||
self.prefix = prefix
|
||||
|
||||
def shrink(self):
|
||||
self.max_line_diff -= self.shrink_speed
|
||||
self.max_line_diff = max(1, self.max_line_diff)
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
diff_str = '\n'.join(self.diff_lines[: self.max_line_diff])
|
||||
if len(self.diff_lines) > self.max_line_diff:
|
||||
original_count = len(self.diff_lines)
|
||||
diff_str = f'{diff_str}\nDiff truncated, {original_count - self.max_line_diff} changes now shown.'
|
||||
return f'{self.prefix}{self.header}\n{diff_str}\n'
|
||||
|
||||
|
||||
class HistoryStep(Shrinkable):
|
||||
def __init__(
|
||||
self, previous_obs, current_obs, action, memory, flags: Flags, shrink_speed=1
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.html_diff = Diff(
|
||||
previous_obs[flags.html_type],
|
||||
current_obs[flags.html_type],
|
||||
prefix='\n### HTML diff:\n',
|
||||
shrink_speed=shrink_speed,
|
||||
visible=lambda: flags.use_html and flags.use_diff,
|
||||
)
|
||||
self.ax_tree_diff = Diff(
|
||||
previous_obs['axtree_txt'],
|
||||
current_obs['axtree_txt'],
|
||||
prefix='\n### Accessibility tree diff:\n',
|
||||
shrink_speed=shrink_speed,
|
||||
visible=lambda: flags.use_ax_tree and flags.use_diff,
|
||||
)
|
||||
self.error = Error(
|
||||
current_obs['last_action_error'],
|
||||
visible=(
|
||||
flags.use_error_logs
|
||||
and current_obs['last_action_error']
|
||||
and flags.use_past_error_logs
|
||||
),
|
||||
prefix='### ',
|
||||
)
|
||||
self.shrink_speed = shrink_speed
|
||||
self.action = action
|
||||
self.memory = memory
|
||||
self.flags = flags
|
||||
|
||||
def shrink(self):
|
||||
super().shrink()
|
||||
self.html_diff.shrink()
|
||||
self.ax_tree_diff.shrink()
|
||||
|
||||
@property
|
||||
def _prompt(self) -> str: # type: ignore
|
||||
prompt = ''
|
||||
|
||||
if self.flags.use_action_history:
|
||||
prompt += f'\n### Action:\n{self.action}\n'
|
||||
|
||||
prompt += (
|
||||
f'{self.error.prompt}{self.html_diff.prompt}{self.ax_tree_diff.prompt}'
|
||||
)
|
||||
|
||||
if self.flags.use_memory and self.memory is not None:
|
||||
prompt += f'\n### Memory:\n{self.memory}\n'
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
class History(Shrinkable):
|
||||
def __init__(
|
||||
self, history_obs, actions, memories, thoughts, flags: Flags, shrink_speed=1
|
||||
) -> None:
|
||||
super().__init__(visible=flags.use_history)
|
||||
assert len(history_obs) == len(actions) + 1
|
||||
assert len(history_obs) == len(memories) + 1
|
||||
|
||||
self.shrink_speed = shrink_speed
|
||||
self.history_steps: list[HistoryStep] = []
|
||||
|
||||
for i in range(1, len(history_obs)):
|
||||
self.history_steps.append(
|
||||
HistoryStep(
|
||||
history_obs[i - 1],
|
||||
history_obs[i],
|
||||
actions[i - 1],
|
||||
memories[i - 1],
|
||||
flags,
|
||||
)
|
||||
)
|
||||
|
||||
def shrink(self):
|
||||
"""Shrink individual steps"""
|
||||
# TODO set the shrink speed of older steps to be higher
|
||||
super().shrink()
|
||||
for step in self.history_steps:
|
||||
step.shrink()
|
||||
|
||||
@property
|
||||
def _prompt(self):
|
||||
prompts = ['# History of interaction with the task:\n']
|
||||
for i, step in enumerate(self.history_steps):
|
||||
prompts.append(f'## step {i}')
|
||||
prompts.append(step.prompt)
|
||||
return '\n'.join(prompts) + '\n'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
html_template = """
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
Hello World.
|
||||
Step {}.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
OBS_HISTORY = [
|
||||
{
|
||||
'goal': 'do this and that',
|
||||
'pruned_html': html_template.format(1),
|
||||
'axtree_txt': '[1] Click me',
|
||||
'last_action_error': '',
|
||||
},
|
||||
{
|
||||
'goal': 'do this and that',
|
||||
'pruned_html': html_template.format(2),
|
||||
'axtree_txt': '[1] Click me',
|
||||
'last_action_error': '',
|
||||
},
|
||||
{
|
||||
'goal': 'do this and that',
|
||||
'pruned_html': html_template.format(3),
|
||||
'axtree_txt': '[1] Click me',
|
||||
'last_action_error': 'Hey, there is an error now',
|
||||
},
|
||||
]
|
||||
ACTIONS = ["click('41')", "click('42')"]
|
||||
MEMORIES = ['memory A', 'memory B']
|
||||
THOUGHTS = ['thought A', 'thought B']
|
||||
|
||||
flags = Flags(
|
||||
use_html=True,
|
||||
use_ax_tree=True,
|
||||
use_thinking=True,
|
||||
use_error_logs=True,
|
||||
use_past_error_logs=True,
|
||||
use_history=True,
|
||||
use_action_history=True,
|
||||
use_memory=True,
|
||||
use_diff=True,
|
||||
html_type='pruned_html',
|
||||
use_concrete_example=True,
|
||||
use_abstract_example=True,
|
||||
use_screenshot=False,
|
||||
multi_actions=True,
|
||||
)
|
||||
|
||||
print(
|
||||
MainPrompt(
|
||||
obs_history=OBS_HISTORY,
|
||||
actions=ACTIONS,
|
||||
memories=MEMORIES,
|
||||
thoughts=THOUGHTS,
|
||||
flags=flags,
|
||||
).prompt
|
||||
)
|
||||
@@ -0,0 +1,160 @@
|
||||
import collections
|
||||
import re
|
||||
from warnings import warn
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def yaml_parser(message):
|
||||
"""Parse a yaml message for the retry function."""
|
||||
|
||||
# saves gpt-3.5 from some yaml parsing errors
|
||||
message = re.sub(r':\s*\n(?=\S|\n)', ': ', message)
|
||||
|
||||
try:
|
||||
value = yaml.safe_load(message)
|
||||
valid = True
|
||||
retry_message = ''
|
||||
except yaml.YAMLError as e:
|
||||
warn(str(e))
|
||||
value = {}
|
||||
valid = False
|
||||
retry_message = "Your response is not a valid yaml. Please try again and be careful to the format. Don't add any apology or comment, just the answer."
|
||||
return value, valid, retry_message
|
||||
|
||||
|
||||
def _compress_chunks(text, identifier, skip_list, split_regex='\n\n+'):
|
||||
"""Compress a string by replacing redundant chunks by identifiers. Chunks are defined by the split_regex."""
|
||||
text_list = re.split(split_regex, text)
|
||||
text_list = [chunk.strip() for chunk in text_list]
|
||||
counter = collections.Counter(text_list)
|
||||
def_dict = {}
|
||||
id = 0
|
||||
|
||||
# Store items that occur more than once in a dictionary
|
||||
for item, count in counter.items():
|
||||
if count > 1 and item not in skip_list and len(item) > 10:
|
||||
def_dict[f'{identifier}-{id}'] = item
|
||||
id += 1
|
||||
|
||||
# Replace redundant items with their identifiers in the text
|
||||
compressed_text = '\n'.join(text_list)
|
||||
for key, value in def_dict.items():
|
||||
compressed_text = compressed_text.replace(value, key)
|
||||
|
||||
return def_dict, compressed_text
|
||||
|
||||
|
||||
def compress_string(text):
|
||||
"""Compress a string by replacing redundant paragraphs and lines with identifiers."""
|
||||
|
||||
# Perform paragraph-level compression
|
||||
def_dict, compressed_text = _compress_chunks(
|
||||
text, identifier='§', skip_list=[], split_regex='\n\n+'
|
||||
)
|
||||
|
||||
# Perform line-level compression, skipping any paragraph identifiers
|
||||
line_dict, compressed_text = _compress_chunks(
|
||||
compressed_text, '¶', list(def_dict.keys()), split_regex='\n+'
|
||||
)
|
||||
def_dict.update(line_dict)
|
||||
|
||||
# Create a definitions section
|
||||
def_lines = ['<definitions>']
|
||||
for key, value in def_dict.items():
|
||||
def_lines.append(f'{key}:\n{value}')
|
||||
def_lines.append('</definitions>')
|
||||
definitions = '\n'.join(def_lines)
|
||||
|
||||
return definitions + '\n' + compressed_text
|
||||
|
||||
|
||||
def extract_html_tags(text, keys):
|
||||
"""Extract the content within HTML tags for a list of keys.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
The input string containing the HTML tags.
|
||||
keys : list of str
|
||||
The HTML tags to extract the content from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A dictionary mapping each key to a list of subset in `text` that match the key.
|
||||
|
||||
Notes
|
||||
-----
|
||||
All text and keys will be converted to lowercase before matching.
|
||||
|
||||
"""
|
||||
content_dict = {}
|
||||
# text = text.lower()
|
||||
# keys = set([k.lower() for k in keys])
|
||||
for key in keys:
|
||||
pattern = f'<{key}>(.*?)</{key}>'
|
||||
matches = re.findall(pattern, text, re.DOTALL)
|
||||
if matches:
|
||||
content_dict[key] = [match.strip() for match in matches]
|
||||
return content_dict
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def parse_html_tags_raise(text, keys=(), optional_keys=(), merge_multiple=False):
|
||||
"""A version of parse_html_tags that raises an exception if the parsing is not successful."""
|
||||
content_dict, valid, retry_message = parse_html_tags(
|
||||
text, keys, optional_keys, merge_multiple=merge_multiple
|
||||
)
|
||||
if not valid:
|
||||
raise ParseError(retry_message)
|
||||
return content_dict
|
||||
|
||||
|
||||
def parse_html_tags(text, keys=(), optional_keys=(), merge_multiple=False):
|
||||
"""Satisfy the parse api, extracts 1 match per key and validates that all keys are present
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
The input string containing the HTML tags.
|
||||
keys : list of str
|
||||
The HTML tags to extract the content from.
|
||||
optional_keys : list of str
|
||||
The HTML tags to extract the content from, but are optional.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A dictionary mapping each key to subset of `text` that match the key.
|
||||
bool
|
||||
Whether the parsing was successful.
|
||||
str
|
||||
A message to be displayed to the agent if the parsing was not successful.
|
||||
"""
|
||||
all_keys = tuple(keys) + tuple(optional_keys)
|
||||
content_dict = extract_html_tags(text, all_keys)
|
||||
retry_messages = []
|
||||
|
||||
for key in all_keys:
|
||||
if key not in content_dict:
|
||||
if key not in optional_keys:
|
||||
retry_messages.append(f'Missing the key <{key}> in the answer.')
|
||||
else:
|
||||
val = content_dict[key]
|
||||
content_dict[key] = val[0]
|
||||
if len(val) > 1:
|
||||
if not merge_multiple:
|
||||
retry_messages.append(
|
||||
f'Found multiple instances of the key {key}. You should have only one of them.'
|
||||
)
|
||||
else:
|
||||
# merge the multiple instances
|
||||
content_dict[key] = '\n'.join(val)
|
||||
|
||||
valid = len(retry_messages) == 0
|
||||
retry_message = '\n'.join(retry_messages)
|
||||
return content_dict, valid, retry_message
|
||||
@@ -1,6 +1,6 @@
|
||||
# CodeAct Agent Framework
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
|
||||
|
||||
@@ -1,41 +1,97 @@
|
||||
import re
|
||||
from typing import List, Mapping
|
||||
|
||||
from agenthub.codeact_agent.prompt import EXAMPLES, SYSTEM_MESSAGE
|
||||
from agenthub.codeact_agent.prompt import (
|
||||
COMMAND_DOCS,
|
||||
EXAMPLES,
|
||||
GITHUB_MESSAGE,
|
||||
SYSTEM_PREFIX,
|
||||
SYSTEM_SUFFIX,
|
||||
)
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentEchoAction,
|
||||
AgentFinishAction,
|
||||
AgentTalkAction,
|
||||
BrowseInteractiveAction,
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
NullAction,
|
||||
MessageAction,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
AgentMessageObservation,
|
||||
BrowserOutputObservation,
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
UserMessageObservation,
|
||||
)
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.runtime.plugins import (
|
||||
AgentSkillsRequirement,
|
||||
JupyterRequirement,
|
||||
PluginRequirement,
|
||||
SWEAgentCommandsRequirement,
|
||||
)
|
||||
|
||||
ENABLE_GITHUB = True
|
||||
|
||||
|
||||
def parse_response(response) -> str:
|
||||
action = response.choices[0].message.content
|
||||
for lang in ['bash', 'ipython']:
|
||||
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 truncate_observation(observation: str, max_chars: int = 5000) -> str:
|
||||
def action_to_str(action: Action) -> str:
|
||||
if isinstance(action, CmdRunAction):
|
||||
return f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
|
||||
elif isinstance(action, IPythonRunCellAction):
|
||||
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
|
||||
elif isinstance(action, BrowseInteractiveAction):
|
||||
return f'{action.thought}\n<execute_browse>\n{action.browser_actions}\n</execute_browse>'
|
||||
elif isinstance(action, MessageAction):
|
||||
return action.content
|
||||
return ''
|
||||
|
||||
|
||||
def get_action_message(action: Action) -> dict[str, str] | None:
|
||||
if (
|
||||
isinstance(action, BrowseInteractiveAction)
|
||||
or isinstance(action, CmdRunAction)
|
||||
or isinstance(action, IPythonRunCellAction)
|
||||
or isinstance(action, MessageAction)
|
||||
):
|
||||
return {
|
||||
'role': 'user' if action.source == 'user' else 'assistant',
|
||||
'content': action_to_str(action),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def get_observation_message(obs) -> dict[str, str] | None:
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
|
||||
content += (
|
||||
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
|
||||
)
|
||||
return {'role': 'user', 'content': content}
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
content = 'OBSERVATION:\n' + obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splitted = content.split('\n')
|
||||
for i, line in enumerate(splitted):
|
||||
if ' already displayed to user'
|
||||
)
|
||||
content = '\n'.join(splitted)
|
||||
content = truncate_observation(content)
|
||||
return {'role': 'user', 'content': content}
|
||||
elif isinstance(obs, BrowserOutputObservation):
|
||||
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
|
||||
return {'role': 'user', 'content': content}
|
||||
return None
|
||||
|
||||
|
||||
def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
|
||||
"""
|
||||
Truncate the middle of the observation if it is too long.
|
||||
"""
|
||||
@@ -49,7 +105,20 @@ def truncate_observation(observation: str, max_chars: int = 5000) -> str:
|
||||
)
|
||||
|
||||
|
||||
# FIXME: We can tweak these two settings to create MicroAgents specialized toward different area
|
||||
def get_system_message() -> str:
|
||||
if ENABLE_GITHUB:
|
||||
return f'{SYSTEM_PREFIX}\n{GITHUB_MESSAGE}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
|
||||
else:
|
||||
return f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
|
||||
|
||||
|
||||
def get_in_context_example() -> str:
|
||||
return EXAMPLES
|
||||
|
||||
|
||||
class CodeActAgent(Agent):
|
||||
VERSION = '1.5'
|
||||
"""
|
||||
The Code Act Agent is a minimalist agent.
|
||||
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
|
||||
@@ -86,23 +155,17 @@ class CodeActAgent(Agent):
|
||||
|
||||
"""
|
||||
|
||||
sandbox_plugins: List[PluginRequirement] = [
|
||||
sandbox_plugins: list[PluginRequirement] = [
|
||||
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
|
||||
# AgentSkillsRequirement provides a lot of Python functions
|
||||
# and it need to be initialized before Jupyter for Jupyter to use those functions.
|
||||
AgentSkillsRequirement(),
|
||||
JupyterRequirement(),
|
||||
SWEAgentCommandsRequirement(),
|
||||
]
|
||||
SUPPORTED_ACTIONS = (
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
AgentEchoAction,
|
||||
AgentTalkAction,
|
||||
NullAction,
|
||||
)
|
||||
SUPPORTED_OBSERVATIONS = (
|
||||
AgentMessageObservation,
|
||||
UserMessageObservation,
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
)
|
||||
jupyter_kernel_init_code: str = 'from agentskills import *'
|
||||
|
||||
system_message: str = get_system_message()
|
||||
in_context_example: str = f"Here is an example of how you can interact with the environment for task solving:\n{get_in_context_example()}\n\nNOW, LET'S START!"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -115,7 +178,13 @@ class CodeActAgent(Agent):
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm)
|
||||
self.messages: List[Mapping[str, str]] = []
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Resets the CodeAct Agent.
|
||||
"""
|
||||
super().reset()
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
@@ -128,97 +197,85 @@ class CodeActAgent(Agent):
|
||||
Returns:
|
||||
- CmdRunAction(command) - bash command to run
|
||||
- IPythonRunCellAction(code) - IPython code to run
|
||||
- AgentTalkAction(content) - Talk action to run (e.g. ask for clarification)
|
||||
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
messages: list[dict[str, str]] = [
|
||||
{'role': 'system', 'content': self.system_message},
|
||||
{'role': 'user', 'content': self.in_context_example},
|
||||
]
|
||||
|
||||
if len(self.messages) == 0:
|
||||
assert state.plan.main_goal, 'Expecting instruction to be set'
|
||||
self.messages = [
|
||||
{'role': 'system', 'content': SYSTEM_MESSAGE},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': (
|
||||
f'Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\n'
|
||||
f"NOW, LET'S START!\n\n{state.plan.main_goal}"
|
||||
),
|
||||
},
|
||||
]
|
||||
updated_info = state.updated_info
|
||||
if updated_info:
|
||||
for prev_action, obs in updated_info:
|
||||
assert isinstance(
|
||||
prev_action, self.SUPPORTED_ACTIONS
|
||||
), f'{prev_action.__class__} is not supported (supported: {self.SUPPORTED_ACTIONS})'
|
||||
# prev_action is already added to self.messages when returned
|
||||
for prev_action, obs in state.history:
|
||||
action_message = get_action_message(prev_action)
|
||||
if action_message:
|
||||
messages.append(action_message)
|
||||
|
||||
# handle observations
|
||||
assert isinstance(
|
||||
obs, self.SUPPORTED_OBSERVATIONS
|
||||
), f'{obs.__class__} is not supported (supported: {self.SUPPORTED_OBSERVATIONS})'
|
||||
if isinstance(obs, (AgentMessageObservation, UserMessageObservation)):
|
||||
self.messages.append({'role': 'user', 'content': obs.content})
|
||||
obs_message = get_observation_message(obs)
|
||||
if obs_message:
|
||||
messages.append(obs_message)
|
||||
|
||||
# User wants to exit
|
||||
if obs.content.strip() == '/exit':
|
||||
return AgentFinishAction()
|
||||
elif 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})
|
||||
latest_user_message = [m for m in messages if m['role'] == 'user'][-1]
|
||||
if latest_user_message:
|
||||
if latest_user_message['content'].strip() == '/exit':
|
||||
return AgentFinishAction()
|
||||
latest_user_message['content'] += (
|
||||
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
|
||||
)
|
||||
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
content = 'OBSERVATION:\n' + obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splited = content.split('\n')
|
||||
for i, line in enumerate(splited):
|
||||
if ' already displayed to user'
|
||||
)
|
||||
content = '\n'.join(splited)
|
||||
content = truncate_observation(content)
|
||||
self.messages.append({'role': 'user', 'content': content})
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Unknown observation type: {obs.__class__}'
|
||||
)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=self.messages,
|
||||
response = self.llm.do_completion(
|
||||
messages=messages,
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
'</execute_browse>',
|
||||
],
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
action_str: str = parse_response(response)
|
||||
state.num_of_chars += sum(
|
||||
len(message['content']) for message in self.messages
|
||||
len(message['content']) for message in 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
|
||||
r'<execute_bash>(.*?)</execute_bash>', action_str, re.DOTALL
|
||||
):
|
||||
# remove the command from the action string to get thought
|
||||
thought = action_str.replace(bash_command.group(0), '').strip()
|
||||
# a command was found
|
||||
command_group = bash_command.group(1).strip()
|
||||
|
||||
if command_group.strip() == 'exit':
|
||||
return AgentFinishAction()
|
||||
return CmdRunAction(command=command_group, thought=thought)
|
||||
elif python_code := re.search(
|
||||
r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL
|
||||
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)
|
||||
return IPythonRunCellAction(
|
||||
code=code_group,
|
||||
thought=thought,
|
||||
kernel_init_code=self.jupyter_kernel_init_code,
|
||||
)
|
||||
elif browse_command := re.search(
|
||||
r'<execute_browse>(.*)</execute_browse>', action_str, re.DOTALL
|
||||
):
|
||||
# BrowserGym actions was found
|
||||
browse_actions = browse_command.group(1).strip()
|
||||
thought = action_str.replace(browse_command.group(0), '').strip()
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions=browse_actions, thought=thought
|
||||
)
|
||||
else:
|
||||
# We assume the LLM is GOOD enough that when it returns pure natural language
|
||||
# it want to talk to the user
|
||||
return AgentTalkAction(content=action_str)
|
||||
return MessageAction(content=action_str, wait_for_response=True)
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
raise NotImplementedError('Implement this abstract method')
|
||||
|
||||
@@ -1,60 +1,64 @@
|
||||
from opendevin.runtime.plugins import SWEAgentCommandsRequirement
|
||||
from opendevin.runtime.plugins import AgentSkillsRequirement
|
||||
|
||||
_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.
|
||||
"""
|
||||
_AGENT_SKILLS_DOCS = AgentSkillsRequirement.documentation
|
||||
|
||||
_COMMAND_DOCS = (
|
||||
'\nApart from the standard bash commands, you can also use the following special commands in <execute_bash> environment:\n'
|
||||
f'{_SWEAGENT_BASH_DOCS}'
|
||||
"Please note that THE EDIT COMMAND REQUIRES PROPER INDENTATION. If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
|
||||
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 = f"""A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||
# ======= 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>.
|
||||
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
|
||||
The assistant can install Python packages through bash by <execute_bash> pip install [package needed] </execute_bash> and should always import packages and define variables before starting to use them.
|
||||
The assistant should stop <execute> and provide an answer when they have already obtained the answer from the execution result.
|
||||
|
||||
{_COMMAND_DOCS}
|
||||
|
||||
The assistant's response should be concise, but do express their thoughts.
|
||||
Try to include one of <execute_ipython> or <execute_bash> in each of your responses, unless it is a direct answer to a question OR a message to the user.
|
||||
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
|
||||
"""
|
||||
|
||||
BROWSING_PREFIX = """The assistant can browse the Internet with commands on behalf of the user by wrapping them with <execute_browse> and </execute_browse>.
|
||||
For example, you can browse a given URL by <execute_browse> goto("<URL>") </execute_browse>.
|
||||
The assistant should attempt fewer things at a time instead of putting too much commands OR code in one "execute" block.
|
||||
"""
|
||||
PIP_INSTALL_PREFIX = """The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them."""
|
||||
|
||||
SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX
|
||||
|
||||
GITHUB_MESSAGE = """To do any activities on GitHub, the assistant should use the token in the $GITHUB_TOKEN environment variable.
|
||||
For instance, to push a local branch `my_branch` to the github repo `owner/repo`, the assistant can use the following four commands:
|
||||
<execute_bash> git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch </execute_bash>
|
||||
If the assistant require access to GitHub but $GITHUB_TOKEN is not set, ask the user to set it."""
|
||||
|
||||
SYSTEM_SUFFIX = """The assistant's response should be concise.
|
||||
The assistant should include ONLY ONE <execute_ipython> or <execute_bash> or <execute_browse> in every one of the responses, unless the assistant is finished with the task or need more input or action from the user in order to proceed.
|
||||
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> or <execute_browse> instead of providing it.
|
||||
"""
|
||||
|
||||
|
||||
# ======= EXAMPLE MESSAGE =======
|
||||
EXAMPLES = """
|
||||
--- START OF EXAMPLE ---
|
||||
|
||||
USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
|
||||
|
||||
ASSISTANT:
|
||||
Sure! Let me write the Python code for starting a web server and save it to a file `app.py`:
|
||||
Sure! Let me create a file first:
|
||||
<execute_ipython>
|
||||
CODE='''
|
||||
from flask import Flask
|
||||
create_file('app.py')
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[File: /workspace/app.py (1 lines total)]
|
||||
1|
|
||||
[File app.py created.]
|
||||
|
||||
ASSISTANT:
|
||||
Now I will write the Python code for starting a web server and save it to the file `app.py`:
|
||||
<execute_ipython>
|
||||
EDITED_CODE=\"\"\"from flask import Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
@@ -63,15 +67,23 @@ def index():
|
||||
return str(numbers)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=5000)
|
||||
'''
|
||||
with open('app.py', 'w') as f:
|
||||
f.write(CODE)
|
||||
app.run(port=5000)\"\"\"
|
||||
edit_file(start=1, end=1, content=EDITED_CODE)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[Code executed successfully with no output]
|
||||
OBSERVATION:
|
||||
1|from flask import Flask
|
||||
2|app = Flask(__name__)
|
||||
3|
|
||||
4|@app.route('/')
|
||||
5|def index():
|
||||
6| numbers = list(range(1, 11))
|
||||
7| return str(numbers)
|
||||
8|
|
||||
9|if __name__ == '__main__':
|
||||
10| app.run(port=5000)
|
||||
[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
|
||||
|
||||
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:
|
||||
@@ -80,7 +92,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:
|
||||
@@ -89,7 +101,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
|
||||
@@ -102,7 +114,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)
|
||||
@@ -126,7 +138,7 @@ python3 app.py > server.log 2>&1 &
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
OBSERVATION:
|
||||
[1] 124
|
||||
|
||||
ASSISTANT:
|
||||
@@ -136,7 +148,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.
|
||||
@@ -146,6 +158,21 @@ Press CTRL+C to quit
|
||||
ASSISTANT:
|
||||
The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
|
||||
|
||||
USER: Now browse the newly started server's homepage and show me the content.
|
||||
|
||||
ASSISTANT:
|
||||
Sure! Let me browse the server's homepage at http://127.0.0.1:5000:
|
||||
<execute_browse>
|
||||
goto("http://127.0.0.1:5000")
|
||||
</execute_browse>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
ASSISTANT:
|
||||
The content of the server's homepage is "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]". If you have any further questions, feel free to ask!
|
||||
|
||||
USER: Now kill the server, make it display the numbers in a table format.
|
||||
|
||||
ASSISTANT:
|
||||
@@ -155,53 +182,48 @@ 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_bash>
|
||||
open app.py
|
||||
</execute_bash>
|
||||
<execute_ipython>
|
||||
open_file('app.py')
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
[File: /workspace/app.py (11 lines total)]
|
||||
1:
|
||||
2:from flask import Flask
|
||||
3:app = Flask(__name__)
|
||||
4:
|
||||
5:@app.route('/')
|
||||
6:def index():
|
||||
7: numbers = list(range(1, 11))
|
||||
8: return str(numbers)
|
||||
9:
|
||||
10:if __name__ == '__main__':
|
||||
11: app.run(port=5000)
|
||||
[File: /workspace/app.py (10 lines total)]
|
||||
1|from flask import Flask
|
||||
2|app = Flask(__name__)
|
||||
3|
|
||||
4|@app.route('/')
|
||||
5|def index():
|
||||
6| numbers = list(range(1, 11))
|
||||
7| return str(numbers)
|
||||
8|
|
||||
9|if __name__ == '__main__':
|
||||
10| app.run(port=5000)
|
||||
|
||||
ASSISTANT:
|
||||
I should edit the file to display the numbers in a table format. Let me do that for you:
|
||||
|
||||
<execute_bash>
|
||||
edit 8:8 <<EOF
|
||||
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
EOF
|
||||
</execute_bash>
|
||||
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
|
||||
<execute_ipython>
|
||||
edit_file(start=7, end=7, content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'")
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[File: /workspace/app.py (11 lines total)]
|
||||
1:
|
||||
2:from flask import Flask
|
||||
3:app = Flask(__name__)
|
||||
4:
|
||||
5:@app.route('/')
|
||||
6:def index():
|
||||
7: numbers = list(range(1, 11))
|
||||
8: return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
9:
|
||||
10:if __name__ == '__main__':
|
||||
11: app.run(port=5000)
|
||||
File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.
|
||||
[File: /workspace/app.py (10 lines total after edit)]
|
||||
1|from flask import Flask
|
||||
2|app = Flask(__name__)
|
||||
3|
|
||||
4|@app.route('/')
|
||||
5|def index():
|
||||
6| numbers = list(range(1, 11))
|
||||
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
8|
|
||||
9|if __name__ == '__main__':
|
||||
10| app.run(port=5000)
|
||||
[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
|
||||
|
||||
ASSISTANT:
|
||||
The file has been updated. Let me run the Python file again with the new changes:
|
||||
@@ -223,4 +245,5 @@ INVALID_INPUT_MESSAGE = (
|
||||
"I don't understand your input. \n"
|
||||
'If you want to execute a bash command, please use <execute_bash> YOUR_COMMAND_HERE </execute_bash>.\n'
|
||||
'If you want to execute a block of Python code, please use <execute_ipython> YOUR_COMMAND_HERE </execute_ipython>.\n'
|
||||
'If you want to browse the Internet, please use <execute_browse> YOUR_COMMAND_HERE </execute_browse>.\n'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,5 @@
|
||||
from opendevin.controller.agent import Agent
|
||||
|
||||
from .codeact_swe_agent import CodeActSWEAgent
|
||||
|
||||
Agent.register('CodeActSWEAgent', CodeActSWEAgent)
|
||||
@@ -0,0 +1,246 @@
|
||||
import re
|
||||
|
||||
from agenthub.codeact_swe_agent.prompt import (
|
||||
COMMAND_DOCS,
|
||||
MINIMAL_SYSTEM_PREFIX,
|
||||
SWE_EXAMPLE,
|
||||
SYSTEM_SUFFIX,
|
||||
)
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentFinishAction,
|
||||
BrowseInteractiveAction,
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
BrowserOutputObservation,
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
)
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.runtime.plugins import (
|
||||
AgentSkillsRequirement,
|
||||
JupyterRequirement,
|
||||
PluginRequirement,
|
||||
)
|
||||
|
||||
|
||||
def parse_response(response) -> str:
|
||||
action = response.choices[0].message.content
|
||||
for lang in ['bash', 'ipython', 'browse']:
|
||||
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
|
||||
action += f'</execute_{lang}>'
|
||||
return action
|
||||
|
||||
|
||||
def action_to_str(action: Action) -> str:
|
||||
if isinstance(action, CmdRunAction):
|
||||
return f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
|
||||
elif isinstance(action, IPythonRunCellAction):
|
||||
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
|
||||
elif isinstance(action, BrowseInteractiveAction):
|
||||
return f'{action.thought}\n<execute_browse>\n{action.browser_actions}\n</execute_browse>'
|
||||
elif isinstance(action, MessageAction):
|
||||
return action.content
|
||||
return ''
|
||||
|
||||
|
||||
def get_action_message(action: Action) -> dict[str, str] | None:
|
||||
if (
|
||||
isinstance(action, BrowseInteractiveAction)
|
||||
or isinstance(action, CmdRunAction)
|
||||
or isinstance(action, IPythonRunCellAction)
|
||||
or isinstance(action, MessageAction)
|
||||
):
|
||||
return {
|
||||
'role': 'user' if action.source == 'user' else 'assistant',
|
||||
'content': action_to_str(action),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def get_observation_message(obs) -> dict[str, str] | None:
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
|
||||
content += (
|
||||
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
|
||||
)
|
||||
return {'role': 'user', 'content': content}
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
content = 'OBSERVATION:\n' + obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splitted = content.split('\n')
|
||||
for i, line in enumerate(splitted):
|
||||
if ' already displayed to user'
|
||||
)
|
||||
content = '\n'.join(splitted)
|
||||
content = truncate_observation(content)
|
||||
return {'role': 'user', 'content': content}
|
||||
elif isinstance(obs, BrowserOutputObservation):
|
||||
content = 'OBSERVATION:\n' + truncate_observation(obs.content)
|
||||
return {'role': 'user', 'content': content}
|
||||
return None
|
||||
|
||||
|
||||
def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
|
||||
"""
|
||||
Truncate the middle of the observation if it is too long.
|
||||
"""
|
||||
if len(observation) <= max_chars:
|
||||
return observation
|
||||
half = max_chars // 2
|
||||
return (
|
||||
observation[:half]
|
||||
+ '\n[... Observation truncated due to length ...]\n'
|
||||
+ observation[-half:]
|
||||
)
|
||||
|
||||
|
||||
def get_system_message() -> str:
|
||||
return f'{MINIMAL_SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
|
||||
|
||||
|
||||
def get_in_context_example() -> str:
|
||||
return SWE_EXAMPLE
|
||||
|
||||
|
||||
class CodeActSWEAgent(Agent):
|
||||
VERSION = '1.5'
|
||||
"""
|
||||
This agent is an adaptation of the original [SWE Agent](https://swe-agent.com/) based on CodeAct 1.5 using the `agentskills` library of OpenDevin.
|
||||
|
||||
It is intended use is **solving Github issues**.
|
||||
|
||||
It removes web-browsing and Github capability from the original CodeAct agent to avoid confusion to the agent.
|
||||
"""
|
||||
|
||||
sandbox_plugins: list[PluginRequirement] = [
|
||||
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
|
||||
# AgentSkillsRequirement provides a lot of Python functions
|
||||
# and it need to be initialized before Jupyter for Jupyter to use those functions.
|
||||
AgentSkillsRequirement(),
|
||||
JupyterRequirement(),
|
||||
]
|
||||
jupyter_kernel_init_code: str = 'from agentskills import *'
|
||||
|
||||
system_message: str = get_system_message()
|
||||
in_context_example: str = f"Here is an example of how you can interact with the environment for task solving:\n{get_in_context_example()}\n\nNOW, LET'S START!"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes a new instance of the CodeActAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm)
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Resets the CodeAct Agent.
|
||||
"""
|
||||
super().reset()
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
Performs one step using the CodeAct Agent.
|
||||
This includes gathering info on previous steps and prompting the model to make a command to execute.
|
||||
|
||||
Parameters:
|
||||
- state (State): used to get updated info and background commands
|
||||
|
||||
Returns:
|
||||
- CmdRunAction(command) - bash command to run
|
||||
- IPythonRunCellAction(code) - IPython code to run
|
||||
- BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
messages: list[dict[str, str]] = [
|
||||
{'role': 'system', 'content': self.system_message},
|
||||
{'role': 'user', 'content': self.in_context_example},
|
||||
]
|
||||
|
||||
for prev_action, obs in state.history:
|
||||
action_message = get_action_message(prev_action)
|
||||
if action_message:
|
||||
messages.append(action_message)
|
||||
|
||||
obs_message = get_observation_message(obs)
|
||||
if obs_message:
|
||||
messages.append(obs_message)
|
||||
|
||||
latest_user_message = [m for m in messages if m['role'] == 'user'][-1]
|
||||
if latest_user_message:
|
||||
if latest_user_message['content'].strip() == '/exit':
|
||||
return AgentFinishAction()
|
||||
latest_user_message['content'] += (
|
||||
f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
|
||||
)
|
||||
|
||||
response = self.llm.do_completion(
|
||||
messages=messages,
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
'</execute_browse>',
|
||||
],
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
action_str: str = parse_response(response)
|
||||
state.num_of_chars += sum(
|
||||
len(message['content']) for message in messages
|
||||
) + len(action_str)
|
||||
|
||||
if finish_command := re.search(r'<finish>.*</finish>', action_str, re.DOTALL):
|
||||
thought = action_str.replace(finish_command.group(0), '').strip()
|
||||
return AgentFinishAction(thought=thought)
|
||||
if bash_command := re.search(
|
||||
r'<execute_bash>(.*?)</execute_bash>', action_str, re.DOTALL
|
||||
):
|
||||
# remove the command from the action string to get thought
|
||||
thought = action_str.replace(bash_command.group(0), '').strip()
|
||||
# a command was found
|
||||
command_group = bash_command.group(1).strip()
|
||||
|
||||
if command_group.strip() == 'exit':
|
||||
return AgentFinishAction()
|
||||
return CmdRunAction(command=command_group, thought=thought)
|
||||
elif python_code := re.search(
|
||||
r'<execute_ipython>(.*?)</execute_ipython>', action_str, re.DOTALL
|
||||
):
|
||||
# a code block was found
|
||||
code_group = python_code.group(1).strip()
|
||||
thought = action_str.replace(python_code.group(0), '').strip()
|
||||
return IPythonRunCellAction(
|
||||
code=code_group,
|
||||
thought=thought,
|
||||
kernel_init_code=self.jupyter_kernel_init_code,
|
||||
)
|
||||
elif browse_command := re.search(
|
||||
r'<execute_browse>(.*)</execute_browse>', action_str, re.DOTALL
|
||||
):
|
||||
# BrowserGym actions was found
|
||||
browse_actions = browse_command.group(1).strip()
|
||||
thought = action_str.replace(browse_command.group(0), '').strip()
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions=browse_actions, thought=thought
|
||||
)
|
||||
else:
|
||||
# We assume the LLM is GOOD enough that when it returns pure natural language
|
||||
# it want to talk to the user
|
||||
return MessageAction(content=action_str, wait_for_response=True)
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
raise NotImplementedError('Implement this abstract method')
|
||||
@@ -0,0 +1,451 @@
|
||||
from opendevin.runtime.plugins import AgentSkillsRequirement
|
||||
|
||||
_AGENT_SKILLS_DOCS = AgentSkillsRequirement.documentation
|
||||
|
||||
COMMAND_DOCS = (
|
||||
'\nApart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:\n'
|
||||
f'{_AGENT_SKILLS_DOCS}'
|
||||
"Please note that THE `edit_file` FUNCTION REQUIRES PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
|
||||
)
|
||||
|
||||
# ======= SYSTEM MESSAGE =======
|
||||
MINIMAL_SYSTEM_PREFIX = """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
|
||||
<execute_ipython>
|
||||
print("Hello World!")
|
||||
</execute_ipython>
|
||||
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
|
||||
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
|
||||
"""
|
||||
|
||||
SYSTEM_SUFFIX = """The assistant's response should be concise.
|
||||
The assistant should include ONLY ONE <execute_ipython> or <execute_bash> or <execute_browse> in every one of the responses, unless the assistant is finished with the task or need more input or action from the user in order to proceed.
|
||||
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> or <execute_browse> instead of providing it.
|
||||
"""
|
||||
|
||||
SWE_EXAMPLE = """
|
||||
--- START OF EXAMPLE ---
|
||||
|
||||
USER: We are currently solving the following issue within our repository. Here is the issue text:
|
||||
--- BEGIN ISSUE ---
|
||||
TimeDelta serialization precision
|
||||
Hi there!
|
||||
|
||||
I just found quite strange behaviour of `TimeDelta` field serialization
|
||||
|
||||
```python3
|
||||
from marshmallow.fields import TimeDelta
|
||||
from datetime import timedelta
|
||||
|
||||
td_field = TimeDelta(precision="milliseconds")
|
||||
|
||||
obj = dict()
|
||||
obj["td_field"] = timedelta(milliseconds=345)
|
||||
|
||||
print(td_field.serialize("td_field", obj))
|
||||
```
|
||||
|
||||
Output of this snippet is `344`, but it seems that `345` is correct.
|
||||
|
||||
Looks like a rounding issue here: https://github.com/marshmallow-code/marshmallow/blob/dev/src/marshmallow/fields.py#L1474
|
||||
--- END ISSUE ---
|
||||
|
||||
[Current directory: /workspace/marshmallow-code__marshmallow]
|
||||
|
||||
ASSISTANT:
|
||||
Let's first start by reproducing the results of the issue. The issue includes some example code for reproduction, which we can use. We'll create a new file called `reproduce.py` and paste the example code into it.
|
||||
<execute_ipython>
|
||||
create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[File: /workspace/marshmallow-code__marshmallow/reproduce.py (1 lines total)]
|
||||
1|
|
||||
[File /workspace/marshmallow-code__marshmallow/reproduce.py created.]
|
||||
|
||||
ASSISTANT:
|
||||
Now let's paste in the example code from the issue.
|
||||
<execute_ipython>
|
||||
EDITED_CODE = \"\"\"from marshmallow.fields import TimeDelta
|
||||
from datetime import timedelta
|
||||
|
||||
td_field = TimeDelta(precision="milliseconds")
|
||||
|
||||
obj = dict()
|
||||
obj["td_field"] = timedelta(milliseconds=345)
|
||||
|
||||
print(td_field.serialize("td_field", obj))
|
||||
\"\"\"
|
||||
edit_file(start=1, end=1, content=EDITED_CODE)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[File: /workspace/marshmallow-code__marshmallow/reproduce.py (9 lines total)]
|
||||
1|from marshmallow.fields import TimeDelta
|
||||
2|from datetime import timedelta
|
||||
3|
|
||||
4|td_field = TimeDelta(precision="milliseconds")
|
||||
5|
|
||||
6|obj = dict()
|
||||
7|obj["td_field"] = timedelta(milliseconds=345)
|
||||
8|
|
||||
9|print(td_field.serialize("td_field", obj))
|
||||
[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
|
||||
|
||||
ASSISTANT:
|
||||
Now let's run the code to see if we see the same output as the issue.
|
||||
<execute_bash>
|
||||
python reproduce.py
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
344
|
||||
|
||||
ASSISTANT:
|
||||
We are indeed seeing the same output as the issue. The issue suggests that we should look at line 1474 of the `fields.py` file to see if there is a rounding issue. Let's open the `fields.py` file and navigate to line 1474 to see the relevant code. First we should find the `fields.py` file in the repository. It is likely to be in the `src/marshmallow` directory, but we should check to be sure.
|
||||
<execute_bash>
|
||||
ls -F
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
AUTHORS.rst
|
||||
CHANGELOG.rst
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.rst
|
||||
LICENSE
|
||||
MANIFEST.in
|
||||
NOTICE
|
||||
README.rst
|
||||
RELEASING.md
|
||||
azure-pipelines.yml
|
||||
docs/
|
||||
examples/
|
||||
performance/
|
||||
pyproject.toml
|
||||
reproduce.py
|
||||
setup.cfg
|
||||
setup.py
|
||||
src/
|
||||
tests/
|
||||
tox.ini
|
||||
|
||||
ASSISTANT:
|
||||
It looks like the `src` directory is present, which suggests that the `fields.py` file is likely to be in the `src` directory. Let's use find_file to see where it is.
|
||||
<execute_ipython>
|
||||
find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[Found 1 matches for "fields.py" in /workspace/marshmallow-code__marshmallow/src]
|
||||
/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py
|
||||
|
||||
ASSISTANT:
|
||||
It looks like the `fields.py` file is present in the `./src/marshmallow/` directory. The issue also points to a specific URL with line number 1474. We should navigate to that line in fields.py to see the relevant code for the `TimeDelta` serialization.
|
||||
<execute_ipython>
|
||||
open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[File: /workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
|
||||
(1458 more lines above)
|
||||
1459| self.WEEKS,
|
||||
1460| )
|
||||
1461|
|
||||
1462| if precision not in units:
|
||||
1463| msg = 'The precision must be {} or "{}".'.format(
|
||||
1464| ", ".join([f'"{each}"' for each in units[:-1]]), units[-1]
|
||||
1465| )
|
||||
1466| raise ValueError(msg)
|
||||
1467|
|
||||
1468| self.precision = precision
|
||||
1469| super().__init__(**kwargs)
|
||||
1470|
|
||||
1471| def _serialize(self, value, attr, obj, **kwargs):
|
||||
1472| if value is None:
|
||||
1473| return None
|
||||
1474| base_unit = dt.timedelta(**{self.precision: 1})
|
||||
1475| return int(value.total_seconds() / base_unit.total_seconds())
|
||||
1476|
|
||||
1477| def _deserialize(self, value, attr, data, **kwargs):
|
||||
1478| try:
|
||||
1479| value = int(value)
|
||||
1480| except (TypeError, ValueError) as error:
|
||||
1481| raise self.make_error("invalid") from error
|
||||
1482|
|
||||
1483| kwargs = {self.precision: value}
|
||||
1484|
|
||||
1485| try:
|
||||
1486| return dt.timedelta(**kwargs)
|
||||
1487| except OverflowError as error:
|
||||
1488| raise self.make_error("invalid") from error
|
||||
1489|
|
||||
1490|
|
||||
1491|class Mapping(Field):
|
||||
1492| \"\"\"An abstract class for objects with key-value pairs.
|
||||
1493|
|
||||
1494| :param keys: A field class or instance for dict keys.
|
||||
1495| :param values: A field class or instance for dict values.
|
||||
1496| :param kwargs: The same keyword arguments that :class:`Field` receives.
|
||||
1497|
|
||||
1498| .. note::
|
||||
1499| When the structure of nested data is not known, you may omit the
|
||||
1500| `keys` and `values` arguments to prevent content validation.
|
||||
1501|
|
||||
1502| .. versionadded:: 3.0.0rc4
|
||||
1503| \"\"\"
|
||||
1504|
|
||||
1505| mapping_type = dict
|
||||
1506|
|
||||
1507| #: Default error messages.
|
||||
1508| default_error_messages = {"invalid": "Not a valid mapping type."}
|
||||
1509|
|
||||
1510| def __init__(
|
||||
1511| self,
|
||||
1512| keys: typing.Optional[typing.Union[Field, type]] = None,
|
||||
1513| values: typing.Optional[typing.Union[Field, type]] = None,
|
||||
1514| **kwargs,
|
||||
1515| ):
|
||||
1516| super().__init__(**kwargs)
|
||||
1517| if keys is None:
|
||||
1518| self.key_field = None
|
||||
1519| else:
|
||||
1520| try:
|
||||
1521| self.key_field = resolve_field_instance(keys)
|
||||
1522| except FieldInstanceResolutionError as error:
|
||||
1523| raise ValueError(
|
||||
1524| '"keys" must be a subclass or instance of '
|
||||
1525| "marshmallow.base.FieldABC."
|
||||
1526| ) from error
|
||||
1527|
|
||||
1528| if values is None:
|
||||
1529| self.value_field = None
|
||||
1530| else:
|
||||
1531| try:
|
||||
1532| self.value_field = resolve_field_instance(values)
|
||||
1533| except FieldInstanceResolutionError as error:
|
||||
1534| raise ValueError(
|
||||
1535| '"values" must be a subclass or instance of '
|
||||
1536| "marshmallow.base.FieldABC."
|
||||
1537| ) from error
|
||||
1538| if isinstance(self.value_field, Nested):
|
||||
1539| self.only = self.value_field.only
|
||||
1540| self.exclude = self.value_field.exclude
|
||||
1541|
|
||||
1542| def _bind_to_schema(self, field_name, schema):
|
||||
1543| super()._bind_to_schema(field_name, schema)
|
||||
1544| if self.value_field:
|
||||
1545| self.value_field = copy.deepcopy(self.value_field)
|
||||
1546| self.value_field._bind_to_schema(field_name, self)
|
||||
1547| if isinstance(self.value_field, Nested):
|
||||
1548| self.value_field.only = self.only
|
||||
1549| self.value_field.exclude = self.exclude
|
||||
1550| if self.key_field:
|
||||
1551| self.key_field = copy.deepcopy(self.key_field)
|
||||
1552| self.key_field._bind_to_schema(field_name, self)
|
||||
1553|
|
||||
1554| def _serialize(self, value, attr, obj, **kwargs):
|
||||
1555| if value is None:
|
||||
1556| return None
|
||||
1557| if not self.value_field and not self.key_field:
|
||||
1558| return self.mapping_type(value)
|
||||
(439 more lines below)
|
||||
|
||||
ASSISTANT:
|
||||
We are now looking at the relevant section of the `fields.py` file where the `TimeDelta` serialization occurs. The issue suggests that there is a rounding problem near line 1474. The current implementation uses integer division, which truncates the decimal part and can lead to incorrect rounding on line 1475. To fix this issue, we should ensure that the division results in a float, and then round the result to the nearest integer before converting it to an int. This should preserve the milliseconds precision as expected. Let's make the necessary edit to the code.
|
||||
<execute_ipython>
|
||||
edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]
|
||||
|
||||
ERRORS:
|
||||
- E999 IndentationError: unexpected indent
|
||||
|
||||
[This is how your edit would have looked if applied]
|
||||
-------------------------------------------------
|
||||
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
|
||||
(1468 more lines above)
|
||||
1469| super().__init__(**kwargs)
|
||||
1470|
|
||||
1471| def _serialize(self, value, attr, obj, **kwargs):
|
||||
1472| if value is None:
|
||||
1473| return None
|
||||
1474| base_unit = dt.timedelta(**{self.precision: 1})
|
||||
1475|return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int
|
||||
1476|
|
||||
1477| def _deserialize(self, value, attr, data, **kwargs):
|
||||
1478| try:
|
||||
1479| value = int(value)
|
||||
(518 more lines below)
|
||||
-------------------------------------------------
|
||||
|
||||
[This is the original code before your edit]
|
||||
-------------------------------------------------
|
||||
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
|
||||
(1469 more lines above)
|
||||
1470|
|
||||
1471| def _serialize(self, value, attr, obj, **kwargs):
|
||||
1472| if value is None:
|
||||
1473| return None
|
||||
1474| base_unit = dt.timedelta(**{self.precision: 1})
|
||||
1475| return int(value.total_seconds() / base_unit.total_seconds())
|
||||
1476|
|
||||
1477| def _deserialize(self, value, attr, data, **kwargs):
|
||||
1478| try:
|
||||
1479| value = int(value)
|
||||
1480| except (TypeError, ValueError) as error:
|
||||
(517 more lines below)
|
||||
-------------------------------------------------
|
||||
Your changes have NOT been applied. Please fix your edit command and try again.
|
||||
You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
|
||||
DO NOT re-run the same failed edit command. Running it again will lead to the same error.
|
||||
|
||||
ASSISTANT:
|
||||
My edit command did not use the proper indentation, I will fix my syntax in this follow up edit command.
|
||||
<execute_ipython>
|
||||
EDITED_CODE=" return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int"
|
||||
edit_file(start=1475, end=1475, content=EDITED_CODE)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
|
||||
(1424 more lines above)
|
||||
1425|
|
||||
1426| :param precision: Influences how the integer is interpreted during
|
||||
1427| (de)serialization. Must be 'days', 'seconds', 'microseconds',
|
||||
1428| 'milliseconds', 'minutes', 'hours' or 'weeks'.
|
||||
1429| :param kwargs: The same keyword arguments that :class:`Field` receives.
|
||||
1430|
|
||||
1431| .. versionchanged:: 2.0.0
|
||||
1432| Always serializes to an integer value to avoid rounding errors.
|
||||
1433| Add `precision` parameter.
|
||||
1434| \"\"\"
|
||||
1435|
|
||||
1436| DAYS = "days"
|
||||
1437| SECONDS = "seconds"
|
||||
1438| MICROSECONDS = "microseconds"
|
||||
1439| MILLISECONDS = "milliseconds"
|
||||
1440| MINUTES = "minutes"
|
||||
1441| HOURS = "hours"
|
||||
1442| WEEKS = "weeks"
|
||||
1443|
|
||||
1444| #: Default error messages.
|
||||
1445| default_error_messages = {
|
||||
1446| "invalid": "Not a valid period of time.",
|
||||
1447| "format": "{input!r} cannot be formatted as a timedelta.",
|
||||
1448| }
|
||||
1449|
|
||||
1450| def __init__(self, precision: str = SECONDS, **kwargs):
|
||||
1451| precision = precision.lower()
|
||||
1452| units = (
|
||||
1453| self.DAYS,
|
||||
1454| self.SECONDS,
|
||||
1455| self.MICROSECONDS,
|
||||
1456| self.MILLISECONDS,
|
||||
1457| self.MINUTES,
|
||||
1458| self.HOURS,
|
||||
1459| self.WEEKS,
|
||||
1460| )
|
||||
1461|
|
||||
1462| if precision not in units:
|
||||
1463| msg = 'The precision must be {} or "{}".'.format(
|
||||
1464| ", ".join([f'"{each}"' for each in units[:-1]]), units[-1]
|
||||
1465| )
|
||||
1466| raise ValueError(msg)
|
||||
1467|
|
||||
1468| self.precision = precision
|
||||
1469| super().__init__(**kwargs)
|
||||
1470|
|
||||
1471| def _serialize(self, value, attr, obj, **kwargs):
|
||||
1472| if value is None:
|
||||
1473| return None
|
||||
1474| base_unit = dt.timedelta(**{self.precision: 1})
|
||||
1475| return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int
|
||||
1476|
|
||||
1477| def _deserialize(self, value, attr, data, **kwargs):
|
||||
1478| try:
|
||||
1479| value = int(value)
|
||||
1480| except (TypeError, ValueError) as error:
|
||||
1481| raise self.make_error("invalid") from error
|
||||
1482|
|
||||
1483| kwargs = {self.precision: value}
|
||||
1484|
|
||||
1485| try:
|
||||
1486| return dt.timedelta(**kwargs)
|
||||
1487| except OverflowError as error:
|
||||
1488| raise self.make_error("invalid") from error
|
||||
1489|
|
||||
1490|
|
||||
1491|class Mapping(Field):
|
||||
1492| \"\"\"An abstract class for objects with key-value pairs.
|
||||
1493|
|
||||
1494| :param keys: A field class or instance for dict keys.
|
||||
1495| :param values: A field class or instance for dict values.
|
||||
1496| :param kwargs: The same keyword arguments that :class:`Field` receives.
|
||||
1497|
|
||||
1498| .. note::
|
||||
1499| When the structure of nested data is not known, you may omit the
|
||||
1500| `keys` and `values` arguments to prevent content validation.
|
||||
1501|
|
||||
1502| .. versionadded:: 3.0.0rc4
|
||||
1503| \"\"\"
|
||||
1504|
|
||||
1505| mapping_type = dict
|
||||
1506|
|
||||
1507| #: Default error messages.
|
||||
1508| default_error_messages = {"invalid": "Not a valid mapping type."}
|
||||
1509|
|
||||
1510| def __init__(
|
||||
1511| self,
|
||||
1512| keys: typing.Optional[typing.Union[Field, type]] = None,
|
||||
1513| values: typing.Optional[typing.Union[Field, type]] = None,
|
||||
1514| **kwargs,
|
||||
1515| ):
|
||||
1516| super().__init__(**kwargs)
|
||||
1517| if keys is None:
|
||||
1518| self.key_field = None
|
||||
1519| else:
|
||||
1520| try:
|
||||
1521| self.key_field = resolve_field_instance(keys)
|
||||
1522| except FieldInstanceResolutionError as error:
|
||||
1523| raise ValueError(
|
||||
1524| '"keys" must be a subclass or instance of '
|
||||
(473 more lines below)
|
||||
[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
|
||||
|
||||
ASSISTANT:
|
||||
The code has been updated to use the `round` function, which should fix the rounding issue. Before submitting the changes, it would be prudent to run the reproduce.py code again to ensure that this change has the desired effect. I see that "Current directory" is still `/marshmallow-code__marshmallow`, so we can just run `reproduce.py` directly.
|
||||
<execute_bash>
|
||||
python reproduce.py
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
345
|
||||
|
||||
ASSISTANT:
|
||||
The output has changed from 344 to 345, which suggests that the rounding issue has been fixed. Let's remove the reproduce.py file since it is no longer needed.
|
||||
<execute_bash>
|
||||
rm reproduce.py
|
||||
</execute_bash>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
|
||||
ASSISTANT:
|
||||
rm doesn't have any output when it deletes successfully, so that must have worked. Now that we have fixed the issue, we can exit the current task.
|
||||
<execute_bash>
|
||||
exit
|
||||
</execute_bash>
|
||||
|
||||
--- END OF EXAMPLE ---
|
||||
"""
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action, AgentDelegateAction, AgentFinishAction
|
||||
@@ -8,6 +6,7 @@ from opendevin.llm.llm import LLM
|
||||
|
||||
|
||||
class DelegatorAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
|
||||
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
|
||||
@@ -38,20 +37,22 @@ class DelegatorAgent(Agent):
|
||||
"""
|
||||
if self.current_delegate == '':
|
||||
self.current_delegate = 'study'
|
||||
task = state.get_current_user_intent()
|
||||
return AgentDelegateAction(
|
||||
agent='StudyRepoForTaskAgent', inputs={'task': state.plan.main_goal}
|
||||
agent='StudyRepoForTaskAgent', inputs={'task': task}
|
||||
)
|
||||
|
||||
last_observation = state.history[-1][1]
|
||||
if not isinstance(last_observation, AgentDelegateObservation):
|
||||
raise Exception('Last observation is not an AgentDelegateObservation')
|
||||
|
||||
goal = state.get_current_user_intent()
|
||||
if self.current_delegate == 'study':
|
||||
self.current_delegate = 'coder'
|
||||
return AgentDelegateAction(
|
||||
agent='CoderAgent',
|
||||
inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'task': goal,
|
||||
'summary': last_observation.outputs['summary'],
|
||||
},
|
||||
)
|
||||
@@ -60,7 +61,7 @@ class DelegatorAgent(Agent):
|
||||
return AgentDelegateAction(
|
||||
agent='VerifierAgent',
|
||||
inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'task': goal,
|
||||
},
|
||||
)
|
||||
elif self.current_delegate == 'verifier':
|
||||
@@ -74,12 +75,12 @@ class DelegatorAgent(Agent):
|
||||
return AgentDelegateAction(
|
||||
agent='CoderAgent',
|
||||
inputs={
|
||||
'task': state.plan.main_goal,
|
||||
'task': goal,
|
||||
'summary': last_observation.outputs['summary'],
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise Exception('Invalid delegate state')
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import time
|
||||
from typing import List, TypedDict
|
||||
from typing import TypedDict
|
||||
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
@@ -8,11 +8,13 @@ from opendevin.events.action import (
|
||||
AddTaskAction,
|
||||
AgentFinishAction,
|
||||
AgentRecallAction,
|
||||
AgentThinkAction,
|
||||
AgentRejectAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
ModifyTaskAction,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
@@ -23,6 +25,7 @@ from opendevin.events.observation import (
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.events.serialization.event import event_to_dict
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
"""
|
||||
@@ -34,13 +37,14 @@ FIXME: There are a few problems this surfaced
|
||||
"""
|
||||
|
||||
ActionObs = TypedDict(
|
||||
'ActionObs', {'action': Action, 'observations': List[Observation]}
|
||||
'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.
|
||||
@@ -48,7 +52,7 @@ class DummyAgent(Agent):
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
self.steps: List[ActionObs] = [
|
||||
self.steps: list[ActionObs] = [
|
||||
{
|
||||
'action': AddTaskAction(parent='0', goal='check the current directory'),
|
||||
'observations': [NullObservation('')],
|
||||
@@ -58,11 +62,11 @@ class DummyAgent(Agent):
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': ModifyTaskAction(id='0.0', state='in_progress'),
|
||||
'action': ModifyTaskAction(task_id='0.0', state='in_progress'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
'action': AgentThinkAction(thought='Time to get started!'),
|
||||
'action': MessageAction('Time to get started!'),
|
||||
'observations': [NullObservation('')],
|
||||
},
|
||||
{
|
||||
@@ -95,7 +99,7 @@ class DummyAgent(Agent):
|
||||
'action': CmdRunAction(command=BACKGROUND_CMD, background=True),
|
||||
'observations': [
|
||||
CmdOutputObservation(
|
||||
'Background command started. To stop it, send a `kill` action with id 42',
|
||||
'Background command started. To stop it, send a `kill` action with command_id 42',
|
||||
command_id='42', # type: ignore[arg-type]
|
||||
command=BACKGROUND_CMD,
|
||||
),
|
||||
@@ -119,10 +123,22 @@ class DummyAgent(Agent):
|
||||
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': BrowseInteractiveAction(
|
||||
browser_actions='goto("https://google.com")'
|
||||
),
|
||||
'observations': [
|
||||
# BrowserOutputObservation('<html></html>', url='https://google.com', screenshot=""),
|
||||
],
|
||||
},
|
||||
{
|
||||
'action': AgentFinishAction(),
|
||||
'observations': [],
|
||||
},
|
||||
{
|
||||
'action': AgentRejectAction(),
|
||||
'observations': [],
|
||||
},
|
||||
]
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
@@ -133,8 +149,8 @@ class DummyAgent(Agent):
|
||||
expected_observations = prev_step['observations']
|
||||
hist_start = len(state.history) - len(expected_observations)
|
||||
for i in range(len(expected_observations)):
|
||||
hist_obs = state.history[hist_start + i][1].to_dict()
|
||||
expected_obs = expected_observations[i].to_dict()
|
||||
hist_obs = event_to_dict(state.history[hist_start + i][1])
|
||||
expected_obs = event_to_dict(expected_observations[i])
|
||||
if (
|
||||
'command_id' in hist_obs['extras']
|
||||
and hist_obs['extras']['command_id'] != -1
|
||||
@@ -155,5 +171,5 @@ class DummyAgent(Agent):
|
||||
), f'Expected observation {expected_obs}, got {hist_obs}'
|
||||
return self.steps[state.iteration]['action']
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return ['I am a computer.']
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
* `kill` - kills a background command
|
||||
* `id` - the ID of the background command to kill
|
||||
* `command_id` - the ID of the background command to kill
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
|
||||
* `content` - the thought to record
|
||||
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
|
||||
@@ -0,0 +1,2 @@
|
||||
* `reject` - reject the task. Arguments:
|
||||
* `outputs` - a dictionary representing the outputs of your task, if any
|
||||
@@ -1,2 +0,0 @@
|
||||
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
|
||||
* `thought` - the thought to record
|
||||
+30
-33
@@ -1,12 +1,11 @@
|
||||
import json
|
||||
from typing import Dict, List
|
||||
|
||||
from jinja2 import BaseLoader, Environment
|
||||
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.exceptions import LLMOutputError
|
||||
from opendevin.events.action import Action, action_from_dict
|
||||
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 .instructions import instructions
|
||||
@@ -14,43 +13,38 @@ from .registry import all_microagents
|
||||
|
||||
|
||||
def parse_response(orig_response: str) -> Action:
|
||||
json_start = orig_response.find('{')
|
||||
json_end = orig_response.rfind('}') + 1
|
||||
response = orig_response[json_start:json_end]
|
||||
try:
|
||||
action_dict = json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON in response. Please make sure the response is a valid JSON object'
|
||||
) from e
|
||||
action = action_from_dict(action_dict)
|
||||
return action
|
||||
# attempt to load the JSON dict from the response
|
||||
action_dict = json.loads(orig_response)
|
||||
|
||||
|
||||
def my_encoder(obj):
|
||||
"""
|
||||
Encodes objects as dictionaries
|
||||
|
||||
Parameters:
|
||||
- obj (Object): An object that will be converted
|
||||
|
||||
Returns:
|
||||
- dict: If the object can be converted it is returned in dict format
|
||||
"""
|
||||
if hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
# load the action from the dict
|
||||
return action_from_dict(action_dict)
|
||||
|
||||
|
||||
def to_json(obj, **kwargs):
|
||||
"""
|
||||
Serialize an object to str format
|
||||
"""
|
||||
return json.dumps(obj, default=my_encoder, **kwargs)
|
||||
return json.dumps(obj, **kwargs)
|
||||
|
||||
|
||||
def history_to_json(obj, **kwargs):
|
||||
"""
|
||||
Serialize and simplify history to str format
|
||||
"""
|
||||
if isinstance(obj, list):
|
||||
# process history, make it simpler.
|
||||
processed_history = []
|
||||
for action, observation in obj:
|
||||
processed_history.append(
|
||||
(event_to_memory(action), event_to_memory(observation))
|
||||
)
|
||||
return json.dumps(processed_history, **kwargs)
|
||||
|
||||
|
||||
class MicroAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
prompt = ''
|
||||
agent_definition: Dict = {}
|
||||
agent_definition: dict = {}
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
super().__init__(llm)
|
||||
@@ -61,18 +55,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=latest_user_message,
|
||||
)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
resp = self.llm.do_completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
state.num_of_chars += len(prompt) + len(action_resp)
|
||||
action = parse_response(action_resp)
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return []
|
||||
|
||||
@@ -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.plan.main_goal }}
|
||||
{{ latest_user_message }}
|
||||
|
||||
{% if state.inputs.summary %}
|
||||
Here's a summary of the codebase, as it relates to this task:
|
||||
@@ -14,14 +14,14 @@ Here's a summary of the codebase, as it relates to this task:
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.think }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
Do NOT finish until you have completed the tasks.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
name: CommitWriterAgent
|
||||
description: "Write a git commit message for files in the git staging area"
|
||||
generates: Action
|
||||
inputs: {}
|
||||
outputs:
|
||||
answer: string
|
||||
|
||||
@@ -12,16 +12,19 @@ changes. The commit message should include:
|
||||
- Optionally, a detailed description if the changes are complex or need further explanation.
|
||||
|
||||
You should find the diff using `git diff --cached`, compile a commit message,
|
||||
and call the `finish` action with `outputs.answer` set to the answer.
|
||||
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 }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
If the last item in the history is an error, you should try to fix it.
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.reject }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
## Format
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
instructions: Dict = {}
|
||||
instructions: dict = {}
|
||||
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__)) + '/_instructions'
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task
|
||||
You are in charge of accomplishing the following task:
|
||||
{{ state.plan.main_goal }}
|
||||
{{ latest_user_message }}
|
||||
|
||||
In order to accomplish this goal, you must delegate tasks to one or more agents, who
|
||||
can do the actual work. A description of each agent is provided below. You MUST
|
||||
@@ -17,7 +17,7 @@ provide the correct inputs for the delegate you select.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.delegate }}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
name: MathAgent
|
||||
description: "Solves simple and complex math problems using python"
|
||||
generates: Action
|
||||
container: python:3.12.3-bookworm
|
||||
inputs:
|
||||
task: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Task
|
||||
You are a brilliant mathematician and programmer. You've been given the following problem to solve:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
{{ 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 }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
If the last item in the history is an error, you should try to fix it.
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
name: PostgresAgent
|
||||
description: Writes and maintains PostgreSQL migrations
|
||||
generates: Action
|
||||
inputs:
|
||||
task: string
|
||||
outputs: {}
|
||||
|
||||
@@ -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.plan.main_goal }}
|
||||
{{ latest_user_message }}
|
||||
|
||||
You must:
|
||||
* Investigate the existing migrations to understand the current schema
|
||||
@@ -11,14 +11,14 @@ You must:
|
||||
|
||||
## Actions
|
||||
You may take any of the following actions:
|
||||
{{ instructions.actions.think }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.run }}
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
@@ -13,8 +13,7 @@ for dir in os.listdir(os.path.dirname(__file__)):
|
||||
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:
|
||||
|
||||
@@ -10,7 +10,7 @@ of the codebase, including:
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.think }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
You should ONLY `run` commands that have no side-effects, like `ls` and `grep`.
|
||||
@@ -20,7 +20,7 @@ When you're done, put your summary into the output of the `finish` action.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
@@ -3,12 +3,12 @@ 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.plan.main_goal }}
|
||||
{{ latest_user_message }}
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.think }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
You must ONLY `run` commands that have no side-effects, like `ls` and `grep`.
|
||||
@@ -19,7 +19,7 @@ When you're done, put your summary in `outputs.summary` in the `finish` action.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
name: TypoFixerAgent
|
||||
description: Fixes typos in files in the current working directory
|
||||
inputs: {}
|
||||
outputs:
|
||||
summary: string
|
||||
@@ -0,0 +1,46 @@
|
||||
# Task
|
||||
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
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.write }}
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
To complete this task:
|
||||
1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
|
||||
2. Use the `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 `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[-5:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
For example, if you want to use the read action to read the contents of a file named example.txt, your response should look like this:
|
||||
{
|
||||
"action": "read",
|
||||
"args": {
|
||||
"path": "./example.txt"
|
||||
}
|
||||
}
|
||||
|
||||
Similarly, if you want to use the write action to write content to a file named output.txt, your response should look like this:
|
||||
{
|
||||
"action": "write",
|
||||
"args": {
|
||||
"path": "./output.txt",
|
||||
"content": "This is the content to be written to the file."
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
You are a quality assurance engineer. Another engineer has made changes to the
|
||||
codebase which are supposed to solve this task:
|
||||
|
||||
{{ state.plan.main_goal }}
|
||||
{{ latest_user_message }}
|
||||
|
||||
Your goal is to verify that the changes are correct and bug-free.
|
||||
|
||||
## Available Actions
|
||||
{{ instructions.actions.run }}
|
||||
{{ instructions.actions.read }}
|
||||
{{ instructions.actions.think }}
|
||||
{{ instructions.actions.message }}
|
||||
{{ instructions.actions.finish }}
|
||||
|
||||
You must ONLY `run` commands that have no side-effects, like `ls`, `grep`, and test scripts.
|
||||
@@ -21,7 +21,7 @@ explaining what the problem is.
|
||||
|
||||
## History
|
||||
{{ instructions.history_truncated }}
|
||||
{{ to_json(state.history[-10:]) }}
|
||||
{{ history_to_json(state.history[-10:]) }}
|
||||
|
||||
## Format
|
||||
{{ instructions.format.action }}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
from typing import List
|
||||
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
from agenthub.monologue_agent.utils.monologue import Monologue
|
||||
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 import config
|
||||
from opendevin.core.config import config
|
||||
from opendevin.core.exceptions import AgentNoInstructionError
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AgentRecallAction,
|
||||
AgentThinkAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
GitHubPushAction,
|
||||
MessageAction,
|
||||
NullAction,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
@@ -27,65 +23,19 @@ from opendevin.events.observation import (
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
from opendevin.events.serialization.event import event_to_memory
|
||||
from opendevin.llm.llm import LLM
|
||||
from opendevin.memory.condenser import MemoryCondenser
|
||||
|
||||
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
|
||||
from agenthub.monologue_agent.utils.memory import LongTermMemory
|
||||
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!',
|
||||
'If I have done some work and I want to push it to github, I can do that also!',
|
||||
"Let's do it.",
|
||||
'PUSH owner/repo branch',
|
||||
'The repo was successfully pushed to https://github.com/owner/repo/branch',
|
||||
'And once I have completed my task, I can use the finish action to stop working.',
|
||||
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
|
||||
'Very cool. Now to accomplish my task.',
|
||||
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
|
||||
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
|
||||
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
|
||||
'It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself.',
|
||||
]
|
||||
|
||||
|
||||
class MonologueAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
The Monologue Agent utilizes long and short term memory to complete tasks.
|
||||
Long term memory is stored as a LongTermMemory object and the model uses it to search for examples from the past.
|
||||
@@ -93,52 +43,19 @@ class MonologueAgent(Agent):
|
||||
"""
|
||||
|
||||
_initialized = False
|
||||
monologue: Monologue
|
||||
initial_thoughts: list[dict[str, str]]
|
||||
memory: 'LongTermMemory | None'
|
||||
memory_condenser: MemoryCondenser
|
||||
|
||||
def __init__(self, llm: LLM):
|
||||
"""
|
||||
Initializes the Monologue Agent with an llm, monologue, and memory.
|
||||
Initializes the Monologue Agent with an llm.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm)
|
||||
|
||||
def _add_event(self, event: dict):
|
||||
"""
|
||||
Adds a new event to the agent's monologue and memory.
|
||||
Monologue automatically condenses when it gets too large.
|
||||
|
||||
Parameters:
|
||||
- event (dict): The event that will be added to monologue and memory
|
||||
"""
|
||||
|
||||
if (
|
||||
'args' in event
|
||||
and 'output' in event['args']
|
||||
and len(event['args']['output']) > MAX_OUTPUT_LENGTH
|
||||
):
|
||||
event['args']['output'] = (
|
||||
event['args']['output'][:MAX_OUTPUT_LENGTH] + '...'
|
||||
)
|
||||
|
||||
self.monologue.add_event(event)
|
||||
if self.memory is not None:
|
||||
self.memory.add_event(event)
|
||||
|
||||
# Test monologue token length
|
||||
prompt = prompts.get_request_action_prompt(
|
||||
'',
|
||||
self.monologue.get_thoughts(),
|
||||
[],
|
||||
)
|
||||
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:
|
||||
self.monologue.condense(self.llm)
|
||||
|
||||
def _initialize(self, task: str):
|
||||
"""
|
||||
Utilizes the INITIAL_THOUGHTS list to give the agent a context for its capabilities
|
||||
@@ -159,12 +76,14 @@ class MonologueAgent(Agent):
|
||||
if task is None or task == '':
|
||||
raise AgentNoInstructionError()
|
||||
|
||||
self.monologue = Monologue()
|
||||
if config.get(ConfigType.AGENT_MEMORY_ENABLED):
|
||||
self.initial_thoughts = []
|
||||
if config.agent.memory_enabled:
|
||||
self.memory = LongTermMemory()
|
||||
else:
|
||||
self.memory = None
|
||||
|
||||
self.memory_condenser = MemoryCondenser()
|
||||
|
||||
self._add_initial_thoughts(task)
|
||||
self._initialized = True
|
||||
|
||||
@@ -186,7 +105,7 @@ class MonologueAgent(Agent):
|
||||
observation = BrowserOutputObservation(
|
||||
content=thought, url='', screenshot=''
|
||||
)
|
||||
self._add_event(observation.to_memory())
|
||||
self.initial_thoughts.append(event_to_memory(observation))
|
||||
previous_action = ''
|
||||
else:
|
||||
action: Action = NullAction()
|
||||
@@ -211,14 +130,9 @@ class MonologueAgent(Agent):
|
||||
url = thought.split('BROWSE ')[1]
|
||||
action = BrowseURLAction(url=url)
|
||||
previous_action = ActionType.BROWSE
|
||||
elif thought.startswith('PUSH'):
|
||||
owner_repo, branch = thought.split('PUSH ')[1].split(' ')
|
||||
owner, repo = owner_repo.split('/')
|
||||
action = GitHubPushAction(owner=owner, repo=repo, branch=branch)
|
||||
previous_action = ActionType.PUSH
|
||||
else:
|
||||
action = AgentThinkAction(thought=thought)
|
||||
self._add_event(action.to_memory())
|
||||
action = MessageAction(thought)
|
||||
self.initial_thoughts.append(event_to_memory(action))
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""
|
||||
@@ -230,27 +144,79 @@ class MonologueAgent(Agent):
|
||||
Returns:
|
||||
- Action: The next action to take based on LLM response
|
||||
"""
|
||||
self._initialize(state.plan.main_goal)
|
||||
for prev_action, obs in state.updated_info:
|
||||
self._add_event(prev_action.to_memory())
|
||||
self._add_event(obs.to_memory())
|
||||
|
||||
state.updated_info = []
|
||||
goal = state.get_current_user_intent()
|
||||
self._initialize(goal)
|
||||
|
||||
recent_events: list[dict[str, str]] = []
|
||||
|
||||
# add the events from state.history
|
||||
for prev_action, obs in state.history:
|
||||
if not isinstance(prev_action, NullAction):
|
||||
recent_events.append(event_to_memory(prev_action))
|
||||
if not isinstance(obs, NullObservation):
|
||||
recent_events.append(self._truncate_output(event_to_memory(obs)))
|
||||
|
||||
# add the last messages to long term memory
|
||||
if self.memory is not None and state.history and len(state.history) > 0:
|
||||
self.memory.add_event(event_to_memory(state.history[-1][0]))
|
||||
self.memory.add_event(
|
||||
self._truncate_output(event_to_memory(state.history[-1][1]))
|
||||
)
|
||||
|
||||
# the action prompt with initial thoughts and recent events
|
||||
prompt = prompts.get_request_action_prompt(
|
||||
state.plan.main_goal,
|
||||
self.monologue.get_thoughts(),
|
||||
goal,
|
||||
self.initial_thoughts,
|
||||
recent_events,
|
||||
state.background_commands_obs,
|
||||
)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
|
||||
messages: list[dict[str, str]] = [
|
||||
{'role': 'user', 'content': prompt},
|
||||
]
|
||||
|
||||
# format all as a single message, a monologue
|
||||
resp = self.llm.do_completion(messages=messages)
|
||||
|
||||
# get the next action from the response
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
|
||||
# keep track of max_chars fallback option
|
||||
state.num_of_chars += len(prompt) + len(action_resp)
|
||||
|
||||
action = prompts.parse_action_response(action_resp)
|
||||
self.latest_action = action
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def _truncate_output(
|
||||
self, observation: dict, max_chars: int = MAX_OUTPUT_LENGTH
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Truncates the output of an observation to a maximum number of characters.
|
||||
|
||||
Parameters:
|
||||
- output (str): The observation whose output to truncate
|
||||
- max_chars (int): The maximum number of characters to allow
|
||||
|
||||
Returns:
|
||||
- str: The truncated output
|
||||
"""
|
||||
if (
|
||||
'args' in observation
|
||||
and 'output' in observation['args']
|
||||
and len(observation['args']['output']) > max_chars
|
||||
):
|
||||
output = observation['args']['output']
|
||||
half = max_chars // 2
|
||||
observation['args']['output'] = (
|
||||
output[:half]
|
||||
+ '\n[... Output truncated due to length...]\n'
|
||||
+ output[-half:]
|
||||
)
|
||||
return observation
|
||||
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
"""
|
||||
Uses VectorIndexRetriever to find related memories within the long term memory.
|
||||
Uses search to produce top 10 results.
|
||||
@@ -259,7 +225,7 @@ class MonologueAgent(Agent):
|
||||
- query (str): The query that we want to find related memories for
|
||||
|
||||
Returns:
|
||||
- List[str]: A list of top 10 text results that matched the query
|
||||
- list[str]: A list of top 10 text results that matched the query
|
||||
"""
|
||||
if self.memory is None:
|
||||
return []
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import json
|
||||
|
||||
from json_repair import repair_json
|
||||
|
||||
|
||||
def my_encoder(obj):
|
||||
"""
|
||||
Encodes objects as dictionaries
|
||||
|
||||
Parameters:
|
||||
- obj (Object): An object that will be converted
|
||||
|
||||
Returns:
|
||||
- dict: If the object can be converted it is returned in dict format
|
||||
"""
|
||||
if hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
|
||||
|
||||
def dumps(obj, **kwargs):
|
||||
"""
|
||||
Serialize an object to str format
|
||||
"""
|
||||
|
||||
return json.dumps(obj, default=my_encoder, **kwargs)
|
||||
|
||||
|
||||
def loads(s, **kwargs):
|
||||
"""
|
||||
Create a JSON object from str
|
||||
"""
|
||||
json_start = s.find('{')
|
||||
json_end = s.rfind('}') + 1
|
||||
if json_start == -1 or json_end == -1:
|
||||
raise ValueError('Invalid response: no JSON found')
|
||||
s = s[json_start:json_end]
|
||||
s = repair_json(s)
|
||||
return json.loads(s, **kwargs)
|
||||
@@ -1,79 +0,0 @@
|
||||
import agenthub.monologue_agent.utils.json as json
|
||||
import agenthub.monologue_agent.utils.prompts as prompts
|
||||
from opendevin.core.exceptions import AgentEventTypeError
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.llm.llm import LLM
|
||||
|
||||
|
||||
class Monologue:
|
||||
"""
|
||||
The monologue is a representation for the agent's internal monologue where it can think.
|
||||
The agent has the capability of using this monologue for whatever it wants.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the empty list of thoughts
|
||||
"""
|
||||
self.thoughts = []
|
||||
|
||||
def add_event(self, t: dict):
|
||||
"""
|
||||
Adds an event to memory if it is a valid event.
|
||||
|
||||
Parameters:
|
||||
- t (dict): The thought that we want to add to memory
|
||||
|
||||
Raises:
|
||||
- AgentEventTypeError: If t is not a dict
|
||||
"""
|
||||
if not isinstance(t, dict):
|
||||
raise AgentEventTypeError()
|
||||
self.thoughts.append(t)
|
||||
|
||||
def get_thoughts(self):
|
||||
"""
|
||||
Get the current thoughts of the agent.
|
||||
|
||||
Returns:
|
||||
- List: The list of thoughts that the agent has.
|
||||
"""
|
||||
return self.thoughts
|
||||
|
||||
def get_total_length(self):
|
||||
"""
|
||||
Gives the total number of characters in all thoughts
|
||||
|
||||
Returns:
|
||||
- Int: Total number of chars in thoughts.
|
||||
"""
|
||||
total_length = 0
|
||||
for t in self.thoughts:
|
||||
try:
|
||||
total_length += len(json.dumps(t))
|
||||
except TypeError as e:
|
||||
logger.error('Error serializing thought: %s', str(e), exc_info=False)
|
||||
return total_length
|
||||
|
||||
def condense(self, llm: LLM):
|
||||
"""
|
||||
Attempts to condense the monologue by using the llm
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): llm to be used for summarization
|
||||
|
||||
Raises:
|
||||
- Exception: the same exception as it got from the llm or processing the response
|
||||
"""
|
||||
|
||||
try:
|
||||
prompt = prompts.get_summarize_monologue_prompt(self.thoughts)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = llm.completion(messages=messages)
|
||||
summary_resp = resp['choices'][0]['message']['content']
|
||||
self.thoughts = prompts.parse_summary_response(summary_resp)
|
||||
except Exception as e:
|
||||
logger.error('Error condensing thoughts: %s', str(e), exc_info=False)
|
||||
|
||||
# TODO If the llm fails with ContextWindowExceededError, we can try to condense the monologue chunk by chunk
|
||||
raise
|
||||
@@ -1,19 +1,12 @@
|
||||
import re
|
||||
from json import JSONDecodeError
|
||||
from typing import List
|
||||
|
||||
from opendevin.core import config
|
||||
from opendevin.core.exceptions import LLMOutputError
|
||||
from opendevin.core.schema.config import ConfigType
|
||||
from opendevin.core.config import config
|
||||
from opendevin.core.utils import json
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
action_from_dict,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
CmdOutputObservation,
|
||||
)
|
||||
|
||||
from . import json
|
||||
from opendevin.events.serialization.action import action_from_dict
|
||||
|
||||
ACTION_PROMPT = """
|
||||
You're a thoughtful robot. Your main task is this:
|
||||
@@ -25,7 +18,6 @@ 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:
|
||||
@@ -42,7 +34,7 @@ Here are the possible actions:
|
||||
* `command` - the command to run
|
||||
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
|
||||
* `kill` - kills a background command
|
||||
* `id` - the ID of the background command to kill
|
||||
* `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:
|
||||
@@ -51,15 +43,16 @@ Here are the possible actions:
|
||||
* `branch` - the name of the branch to push
|
||||
* `recall` - recalls a past memory. Arguments:
|
||||
* `query` - the query to search for
|
||||
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
|
||||
* `thought` - the thought to record
|
||||
* `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.
|
||||
|
||||
%(background_commands)s
|
||||
|
||||
You MUST take time to think in between read, write, run, browse, push, and recall actions.
|
||||
You MUST take time to think in between read, write, run, kill, 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 "think" actions, you should consider taking a different action.
|
||||
actions are all `message` actions, you should consider taking a different action.
|
||||
|
||||
Notes:
|
||||
* you are logged in as %(user)s, but sudo will always work without a password.
|
||||
@@ -68,7 +61,7 @@ Notes:
|
||||
* don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
|
||||
* don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
|
||||
* don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
|
||||
* whenever an action fails, always `think` about why it may have happened before acting again.
|
||||
* whenever an action fails, always send a `message` about why it may have happened before acting again.
|
||||
|
||||
What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.
|
||||
|
||||
@@ -98,8 +91,53 @@ The action key may be `summarize`, and `args.summary` should contain the summary
|
||||
You can also use the same action and args from the source monologue.
|
||||
"""
|
||||
|
||||
INITIAL_THOUGHTS = [
|
||||
'I exist!',
|
||||
'Hmm...looks like I can type in a command line prompt',
|
||||
'Looks like I have a web browser too!',
|
||||
"Here's what I want to do: $TASK",
|
||||
'How am I going to get there though?',
|
||||
'It seems like I have some kind of short term memory.',
|
||||
'Each of my thoughts seems to be stored in a JSON array.',
|
||||
'It seems whatever I say next will be added as an object to the list.',
|
||||
'But no one has perfect short-term memory. My list of thoughts will be summarized and condensed over time, losing information in the process.',
|
||||
'Fortunately I have long term memory!',
|
||||
'I can just perform a recall action, followed by the thing I want to remember. And then related thoughts just spill out!',
|
||||
"Sometimes they're random thoughts that don't really have to do with what I wanted to remember. But usually they're exactly what I need!",
|
||||
"Let's try it out!",
|
||||
'RECALL what it is I want to do',
|
||||
"Here's what I want to do: $TASK",
|
||||
'How am I going to get there though?',
|
||||
"Neat! And it looks like it's easy for me to use the command line too! I just have to perform a run action and include the command I want to run in the command argument. The command output just jumps into my head!",
|
||||
'RUN echo "hello world"',
|
||||
'hello world',
|
||||
'Cool! I bet I can write files too using the write action.',
|
||||
'WRITE echo "console.log(\'hello world\')" > test.js',
|
||||
'',
|
||||
"I just created test.js. I'll try and run it now.",
|
||||
'RUN node test.js',
|
||||
'hello world',
|
||||
'It works!',
|
||||
"I'm going to try reading it now using the read action.",
|
||||
'READ test.js',
|
||||
"console.log('hello world')",
|
||||
'Nice! I can read files too!',
|
||||
'And if I want to use the browser, I just need to use the browse action and include the url I want to visit in the url argument',
|
||||
"Let's try that...",
|
||||
'BROWSE google.com',
|
||||
'<form><input type="text"></input><button type="submit"></button></form>',
|
||||
'I can browse the web too!',
|
||||
'And once I have completed my task, I can use the finish action to stop working.',
|
||||
"But I should only use the finish action when I'm absolutely certain that I've completed my task and have tested my work.",
|
||||
'Very cool. Now to accomplish my task.',
|
||||
"I'll need a strategy. And as I make progress, I'll need to keep refining that strategy. I'll need to set goals, and break them into sub-goals.",
|
||||
'In between actions, I must always take some time to think, strategize, and set new goals. I should never take two actions in a row.',
|
||||
"OK so my task is to $TASK. I haven't made any progress yet. Where should I start?",
|
||||
'It seems like there might be an existing project here. I should probably start by running `pwd` and `ls` to orient myself.',
|
||||
]
|
||||
|
||||
def get_summarize_monologue_prompt(thoughts: List[dict]):
|
||||
|
||||
def get_summarize_monologue_prompt(thoughts: list[dict]):
|
||||
"""
|
||||
Gets the prompt for summarizing the monologue
|
||||
|
||||
@@ -113,32 +151,41 @@ def get_summarize_monologue_prompt(thoughts: List[dict]):
|
||||
|
||||
def get_request_action_prompt(
|
||||
task: str,
|
||||
thoughts: List[dict],
|
||||
background_commands_obs: List[CmdOutputObservation] = [],
|
||||
thoughts: list[dict],
|
||||
recent_events: list[dict],
|
||||
background_commands_obs: list[CmdOutputObservation] | None = None,
|
||||
):
|
||||
"""
|
||||
Gets the action prompt formatted with appropriate values.
|
||||
|
||||
Parameters:
|
||||
- task (str): The current task the agent is trying to accomplish
|
||||
- thoughts (List[dict]): The agent's current thoughts
|
||||
- background_commands_obs (List[CmdOutputObservation]): List of all observed background commands running
|
||||
- thoughts (list[dict]): The agent's current thoughts
|
||||
- background_commands_obs (list[CmdOutputObservation]): list of all observed background commands running
|
||||
|
||||
Returns:
|
||||
- str: Formatted prompt string with hint, task, monologue, and background included
|
||||
- str: Formatted prompt string with hint, task, monologue, and background commands included
|
||||
"""
|
||||
|
||||
if background_commands_obs is None:
|
||||
background_commands_obs = []
|
||||
|
||||
hint = ''
|
||||
if len(thoughts) > 0:
|
||||
latest_thought = thoughts[-1]
|
||||
if 'action' in latest_thought:
|
||||
if latest_thought['action'] == 'think':
|
||||
if latest_thought['args']['thought'].startswith('OK so my task is'):
|
||||
hint = "You're just getting started! What should you do first?"
|
||||
else:
|
||||
hint = "You've been thinking a lot lately. Maybe it's time to take action?"
|
||||
elif latest_thought['action'] == 'error':
|
||||
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':
|
||||
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:
|
||||
@@ -147,24 +194,24 @@ def get_request_action_prompt(
|
||||
bg_commands_message += (
|
||||
f'\n`{command_obs.command_id}`: {command_obs.command}'
|
||||
)
|
||||
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
|
||||
bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `command_id` above.'
|
||||
|
||||
user = 'opendevin' if config.get(ConfigType.RUN_AS_DEVIN) else 'root'
|
||||
user = 'opendevin' if config.run_as_devin else 'root'
|
||||
|
||||
monologue = thoughts + recent_events
|
||||
|
||||
return ACTION_PROMPT % {
|
||||
'task': task,
|
||||
'monologue': json.dumps(thoughts, indent=2),
|
||||
'monologue': json.dumps(monologue, indent=2),
|
||||
'background_commands': bg_commands_message,
|
||||
'hint': hint,
|
||||
'user': user,
|
||||
'timeout': config.get(ConfigType.SANDBOX_TIMEOUT),
|
||||
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.get(
|
||||
ConfigType.WORKSPACE_MOUNT_PATH_IN_SANDBOX
|
||||
),
|
||||
'timeout': config.sandbox_timeout,
|
||||
'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
|
||||
}
|
||||
|
||||
|
||||
def parse_action_response(response: str) -> Action:
|
||||
def parse_action_response(orig_response: str) -> Action:
|
||||
"""
|
||||
Parses a string to find an action within it
|
||||
|
||||
@@ -174,39 +221,17 @@ def parse_action_response(response: str) -> Action:
|
||||
Returns:
|
||||
- Action: The action that was found in the response string
|
||||
"""
|
||||
try:
|
||||
action_dict = json.loads(response)
|
||||
except JSONDecodeError:
|
||||
# Find response-looking json in the output and use the more promising one. Helps with weak llms
|
||||
response_json_matches = re.finditer(
|
||||
r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
|
||||
response,
|
||||
) # Find all response-looking strings
|
||||
# attempt to load the JSON dict from the response
|
||||
action_dict = json.loads(orig_response)
|
||||
|
||||
def rank(match):
|
||||
return (
|
||||
len(match[2]) if match[1] == 'think' else 130
|
||||
) # Crudely rank multiple responses by length
|
||||
|
||||
try:
|
||||
action_dict = json.loads(
|
||||
max(response_json_matches, key=rank)[0]
|
||||
) # Use the highest ranked response
|
||||
except (ValueError, JSONDecodeError):
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
raise LLMOutputError(
|
||||
'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
|
||||
)
|
||||
if 'content' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict['contents'] = action_dict.pop('content')
|
||||
|
||||
return action_from_dict(action_dict)
|
||||
|
||||
|
||||
def parse_summary_response(response: str) -> List[dict]:
|
||||
def parse_summary_response(response: str) -> list[dict]:
|
||||
"""
|
||||
Parses a summary of the monologue
|
||||
|
||||
@@ -214,7 +239,7 @@ def parse_summary_response(response: str) -> List[dict]:
|
||||
- response (str): The response string to be parsed
|
||||
|
||||
Returns:
|
||||
- List[dict]: The list of summaries output by the model
|
||||
- list[dict]: The list of summaries output by the model
|
||||
"""
|
||||
parsed = json.loads(response)
|
||||
return parsed['new_monologue']
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import List
|
||||
|
||||
from opendevin.controller.agent import Agent
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.events.action import Action, AgentFinishAction
|
||||
@@ -9,6 +7,7 @@ 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.
|
||||
@@ -36,15 +35,19 @@ class PlannerAgent(Agent):
|
||||
- Action: The next action to take based on llm response
|
||||
"""
|
||||
|
||||
if state.plan.task.state in ['completed', 'verified', 'abandoned']:
|
||||
if state.root_task.state in [
|
||||
'completed',
|
||||
'verified',
|
||||
'abandoned',
|
||||
]:
|
||||
return AgentFinishAction()
|
||||
prompt = get_prompt(state.plan, state.history)
|
||||
prompt = get_prompt(state)
|
||||
messages = [{'content': prompt, 'role': 'user'}]
|
||||
resp = self.llm.completion(messages=messages)
|
||||
resp = self.llm.do_completion(messages=messages)
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
state.num_of_chars += len(prompt) + len(action_resp)
|
||||
action = parse_response(action_resp)
|
||||
return action
|
||||
|
||||
def search_memory(self, query: str) -> List[str]:
|
||||
def search_memory(self, query: str) -> list[str]:
|
||||
return []
|
||||
|
||||
@@ -1,43 +1,16 @@
|
||||
import json
|
||||
from typing import Dict, List, Tuple, Type
|
||||
|
||||
from opendevin.controller.state.plan import Plan
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.schema import ActionType
|
||||
from opendevin.core.utils import json
|
||||
from opendevin.events.action import (
|
||||
Action,
|
||||
AddTaskAction,
|
||||
AgentFinishAction,
|
||||
AgentRecallAction,
|
||||
AgentSummarizeAction,
|
||||
AgentThinkAction,
|
||||
BrowseURLAction,
|
||||
CmdKillAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
ModifyTaskAction,
|
||||
NullAction,
|
||||
action_from_dict,
|
||||
)
|
||||
from opendevin.events.observation import (
|
||||
NullObservation,
|
||||
Observation,
|
||||
)
|
||||
|
||||
ACTION_TYPE_TO_CLASS: Dict[str, Type[Action]] = {
|
||||
ActionType.RUN: CmdRunAction,
|
||||
ActionType.KILL: CmdKillAction,
|
||||
ActionType.BROWSE: BrowseURLAction,
|
||||
ActionType.READ: FileReadAction,
|
||||
ActionType.WRITE: FileWriteAction,
|
||||
ActionType.RECALL: AgentRecallAction,
|
||||
ActionType.THINK: AgentThinkAction,
|
||||
ActionType.SUMMARIZE: AgentSummarizeAction,
|
||||
ActionType.FINISH: AgentFinishAction,
|
||||
ActionType.ADD_TASK: AddTaskAction,
|
||||
ActionType.MODIFY_TASK: ModifyTaskAction,
|
||||
}
|
||||
from opendevin.events.serialization.action import action_from_dict
|
||||
from opendevin.events.serialization.event import event_to_memory
|
||||
|
||||
HISTORY_SIZE = 10
|
||||
|
||||
@@ -106,23 +79,24 @@ It must be an object, and it must contain two fields:
|
||||
* `command` - the command to run
|
||||
* `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
|
||||
* `kill` - kills a background command
|
||||
* `id` - the ID of the background command to kill
|
||||
* `command_id` - the ID of the background command to kill
|
||||
* `browse` - opens a web page. Arguments:
|
||||
* `url` - the URL to open
|
||||
* `think` - make a plan, set a goal, or record your thoughts. Arguments:
|
||||
* `thought` - the thought to record
|
||||
* `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
|
||||
* `add_task` - add a task to your plan. Arguments:
|
||||
* `parent` - the ID of the parent task
|
||||
* `parent` - the ID of the parent task (leave empty if it should go at the top level)
|
||||
* `goal` - the goal of the task
|
||||
* `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
|
||||
* `modify_task` - close a task. Arguments:
|
||||
* `id` - the ID of the task to close
|
||||
* `task_id` - the ID of the task to close
|
||||
* `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
|
||||
* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
|
||||
|
||||
You MUST take time to think in between read, write, run, browse, and recall actions.
|
||||
You MUST take time to think in between read, write, run, kill, 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 `think` actions, you should consider taking a different action.
|
||||
actions are all `message` actions, you should consider taking a different action.
|
||||
|
||||
What is your next thought or action? Again, you must reply with JSON, and only with JSON.
|
||||
|
||||
@@ -139,7 +113,7 @@ def get_hint(latest_action_id: str) -> str:
|
||||
ActionType.READ: 'You should think about the file you just read, what you learned from it, and how that affects your plan.',
|
||||
ActionType.WRITE: 'You just changed a file. You should think about how it affects your plan.',
|
||||
ActionType.BROWSE: 'You should think about the page you just visited, and what you learned from it.',
|
||||
ActionType.THINK: "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action.",
|
||||
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.',
|
||||
@@ -149,42 +123,42 @@ def get_hint(latest_action_id: str) -> str:
|
||||
return hints.get(latest_action_id, '')
|
||||
|
||||
|
||||
def get_prompt(plan: Plan, history: List[Tuple[Action, Observation]]) -> str:
|
||||
def get_prompt(state: State) -> str:
|
||||
"""
|
||||
Gets the prompt for the planner agent.
|
||||
Formatted with the most recent action-observation pairs, current task, and hint based on last action
|
||||
|
||||
Parameters:
|
||||
- plan (Plan): The original plan outlined by the user with LLM defined tasks
|
||||
- history (List[Tuple[Action, Observation]]): List of corresponding action-observation pairs
|
||||
- state (State): The state of the current agent
|
||||
|
||||
Returns:
|
||||
- str: The formatted string prompt with historical values
|
||||
"""
|
||||
|
||||
plan_str = json.dumps(plan.task.to_dict(), indent=2)
|
||||
sub_history = history[-HISTORY_SIZE:]
|
||||
plan_str = json.dumps(state.root_task.to_dict(), indent=2)
|
||||
sub_history = state.history[-HISTORY_SIZE:]
|
||||
history_dicts = []
|
||||
latest_action: Action = NullAction()
|
||||
for action, observation in sub_history:
|
||||
if not isinstance(action, NullAction):
|
||||
history_dicts.append(action.to_memory())
|
||||
history_dicts.append(event_to_memory(action))
|
||||
latest_action = action
|
||||
if not isinstance(observation, NullObservation):
|
||||
observation_dict = observation.to_memory()
|
||||
observation_dict = event_to_memory(observation)
|
||||
history_dicts.append(observation_dict)
|
||||
history_str = json.dumps(history_dicts, indent=2)
|
||||
current_task = plan.get_current_task()
|
||||
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}."
|
||||
if len(current_task.subtasks) == 0:
|
||||
plan_status += "\nIf it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW."
|
||||
else:
|
||||
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
|
||||
hint = get_hint(latest_action.to_dict()['action'])
|
||||
logger.info('HINT:\n' + hint, extra={'msg_type': 'INFO'})
|
||||
hint = get_hint(event_to_memory(latest_action).get('action', ''))
|
||||
logger.info('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
|
||||
task = state.get_current_user_intent()
|
||||
return prompt % {
|
||||
'task': plan.main_goal,
|
||||
'task': task,
|
||||
'plan': plan_str,
|
||||
'history': history_str,
|
||||
'hint': hint,
|
||||
@@ -202,9 +176,6 @@ def parse_response(response: str) -> Action:
|
||||
Returns:
|
||||
- Action: A valid next action to perform from model output
|
||||
"""
|
||||
json_start = response.find('{')
|
||||
json_end = response.rfind('}') + 1
|
||||
response = response[json_start:json_end]
|
||||
action_dict = json.loads(response)
|
||||
if 'contents' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
|
||||
@@ -8,6 +8,4 @@ by the `ghcr.yml` workflow.
|
||||
```
|
||||
docker build -f containers/app/Dockerfile -t opendevin .
|
||||
docker build -f containers/sandbox/Dockerfile -t sandbox .
|
||||
docker build -f containers/evaluation/Dockerfile -t evaluation evaluation/SWE-bench/
|
||||
|
||||
```
|
||||
|
||||
@@ -45,6 +45,7 @@ RUN apt-get update -y \
|
||||
&& apt-get install -y curl ssh sudo
|
||||
|
||||
RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs # Default is 1000, but OSX is often 501
|
||||
RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs # Default is 60000, but we've seen up to 200000
|
||||
|
||||
RUN groupadd app
|
||||
RUN useradd -l -m -u $OPENDEVIN_USER_ID -s /bin/bash opendevin && \
|
||||
@@ -52,6 +53,7 @@ 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 \
|
||||
@@ -73,4 +75,8 @@ COPY --chown=opendevin:app --chmod=770 --from=frontend-builder /app/dist ./front
|
||||
COPY --chown=opendevin:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
USER root
|
||||
CMD ["/app/entrypoint.sh"]
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["uvicorn", "opendevin.server.listen:app", "--host", "0.0.0.0", "--port", "3000"]
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
#!/bin/bash
|
||||
# check user is root
|
||||
set -eo pipefail
|
||||
|
||||
echo "Starting OpenDevin..."
|
||||
if [[ $NO_SETUP == "true" ]]; then
|
||||
echo "Skipping setup, running as $(whoami)"
|
||||
"$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "The OpenDevin entrypoint.sh must run as root"
|
||||
exit 1
|
||||
@@ -11,30 +19,41 @@ if [ -z "$SANDBOX_USER_ID" ]; then
|
||||
fi
|
||||
|
||||
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
|
||||
echo "SANDBOX_USER_ID cannot be 0. Please run with a different user id."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# change uid of opendevin user to match the host user
|
||||
# but the group id is not changed, so the user can still access everything under /app
|
||||
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
|
||||
echo "Running OpenDevin as root"
|
||||
export RUN_AS_DEVIN=false
|
||||
mkdir -p /root/.cache/ms-playwright/
|
||||
mv /home/opendevin/.cache/ms-playwright/ /root/.cache/
|
||||
"$@"
|
||||
else
|
||||
echo "Setting up enduser with id $SANDBOX_USER_ID"
|
||||
if id "enduser" &>/dev/null; then
|
||||
echo "User enduser already exists. Skipping creation."
|
||||
else
|
||||
if ! useradd -l -m -u $SANDBOX_USER_ID -s /bin/bash enduser; then
|
||||
echo "Failed to create user enduser with id $SANDBOX_USER_ID. Moving opendevin user."
|
||||
incremented_id=$(($SANDBOX_USER_ID + 1))
|
||||
usermod -u $incremented_id opendevin
|
||||
if ! useradd -l -m -u $SANDBOX_USER_ID -s /bin/bash enduser; then
|
||||
echo "Failed to create user enduser with id $SANDBOX_USER_ID for a second time. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
usermod -aG app enduser
|
||||
# get the user group of /var/run/docker.sock and set opendevin to that group
|
||||
DOCKER_SOCKET_GID=$(stat -c '%g' /var/run/docker.sock)
|
||||
echo "Docker socket group id: $DOCKER_SOCKET_GID"
|
||||
if getent group $DOCKER_SOCKET_GID; then
|
||||
echo "Group with id $DOCKER_SOCKET_GID already exists"
|
||||
else
|
||||
echo "Creating group with id $DOCKER_SOCKET_GID"
|
||||
groupadd -g $DOCKER_SOCKET_GID docker
|
||||
fi
|
||||
|
||||
mkdir -p /home/enduser/.cache/ms-playwright/
|
||||
mv /home/opendevin/.cache/ms-playwright/ /home/enduser/.cache/
|
||||
|
||||
usermod -aG $DOCKER_SOCKET_GID enduser
|
||||
echo "Running as enduser"
|
||||
su enduser /bin/bash -c "$*"
|
||||
fi
|
||||
|
||||
usermod -aG app enduser
|
||||
mkdir -p /home/enduser/.cache/ms-playwright/
|
||||
mv /home/opendevin/.cache/ms-playwright/ /home/enduser/.cache/
|
||||
|
||||
# get the user group of /var/run/docker.sock and set opendevin to that group
|
||||
DOCKER_SOCKET_GID=$(stat -c '%g' /var/run/docker.sock)
|
||||
echo "Docker socket group id: $DOCKER_SOCKET_GID"
|
||||
usermod -aG $DOCKER_SOCKET_GID enduser
|
||||
|
||||
# switch to the user and start the server
|
||||
su enduser -c "cd /app && uvicorn opendevin.server.listen:app --host 0.0.0.0 --port 3000"
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
FROM ubuntu:20.04
|
||||
|
||||
# https://github.com/princeton-nlp/SWE-bench/issues/15#issuecomment-1815392192
|
||||
RUN apt-get update && \
|
||||
apt-get install -y bash gcc git jq wget && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN git config --global user.email "swebench@pnlp.org"
|
||||
RUN git config --global user.name "swebench"
|
||||
|
||||
RUN apt update && apt install -y build-essential
|
||||
|
||||
# Create new user
|
||||
RUN useradd -ms /bin/bash swe-bench
|
||||
USER swe-bench
|
||||
WORKDIR /home/swe-bench
|
||||
|
||||
# Setup Conda
|
||||
ENV PATH="/home/swe-bench/miniconda3/bin:${PATH}"
|
||||
ARG PATH="/home/swe-bench/miniconda3/bin:${PATH}"
|
||||
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-`uname -m`.sh -O miniconda.sh \
|
||||
&& mkdir ~/.conda \
|
||||
&& bash miniconda.sh -b \
|
||||
&& rm -f miniconda.sh
|
||||
RUN conda --version
|
||||
|
||||
# Setup SWE-Bench Env
|
||||
COPY environment.yml .
|
||||
RUN conda env create -f environment.yml
|
||||
|
||||
# Add commands
|
||||
COPY ./commands.sh .
|
||||
RUN . ./commands.sh
|
||||
|
||||
# Some missing packages
|
||||
RUN pip install datasets python-dotenv gitpython
|
||||
|
||||
RUN conda init bash
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -1,4 +0,0 @@
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_ORG=opendevin
|
||||
DOCKER_IMAGE=eval-swe-bench
|
||||
DOCKER_BASE_DIR=evaluation/SWE-bench
|
||||
@@ -21,6 +21,8 @@ RUN apt-get update && apt-get install -y \
|
||||
jq \
|
||||
g++ \
|
||||
make \
|
||||
iproute2 \
|
||||
libgl1-mesa-glx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p -m0755 /var/run/sshd
|
||||
@@ -31,3 +33,5 @@ RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||
# install basic dependencies for CodeActAgent
|
||||
RUN pip3 install --upgrade pip
|
||||
RUN pip3 install jupyterlab notebook jupyter_kernel_gateway flake8
|
||||
# TODO: those dependencies are needed for agentskills, we should pack them in a new sandbox image
|
||||
RUN pip3 install python-docx PyPDF2 python-pptx pylatexenc openai opencv-python
|
||||
|
||||
@@ -9,10 +9,17 @@ select = [
|
||||
"F",
|
||||
"I",
|
||||
"Q",
|
||||
"B",
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"E501",
|
||||
"B003",
|
||||
"B007",
|
||||
"B009",
|
||||
"B010",
|
||||
"B904",
|
||||
"B018",
|
||||
]
|
||||
|
||||
[lint.flake8-quotes]
|
||||
|
||||
@@ -34,7 +34,7 @@ Now we have both Slack workspace for the collaboration on building OpenDevin and
|
||||
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2ggtwn3k5-PvAA2LUmqGHVZ~XzGq~ILw)
|
||||
- [Discord server](https://discord.gg/ESHStjSjD4)
|
||||
|
||||
If you would love to contribute, feel free to join our community (note that now there is no need to fill in the [form](https://forms.gle/758d5p6Ve8r2nxxq6)). Let's simplify software engineering together!
|
||||
If you would love to contribute, feel free to join our community. Let's simplify software engineering together!
|
||||
|
||||
🐚 **Code less, make more with OpenDevin.**
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ sidebar_position: 3
|
||||
|
||||
### Description
|
||||
|
||||
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.13463), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
|
||||
This agent 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).
|
||||
|
||||
The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
|
||||
1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
|
||||
2. **CodeAct**: Choose to perform the task by executing code
|
||||
|
||||
- Execute any valid Linux `bash` command
|
||||
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
|
||||
|
||||
@@ -22,6 +23,7 @@ The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
### Plugin System
|
||||
|
||||
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
|
||||
|
||||
- [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
|
||||
- [SWE-agent tool plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
|
||||
|
||||
@@ -29,8 +31,7 @@ To make the CodeAct agent more powerful with only access to `bash` action space,
|
||||
|
||||
https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
|
||||
|
||||
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
|
||||
|
||||
_Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)_
|
||||
|
||||
### Actions
|
||||
|
||||
@@ -50,18 +51,17 @@ https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `__init__` | Initializes an agent with `llm` and a list of messages `List[Mapping[str, str]]` |
|
||||
| Method | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `__init__` | Initializes an agent with `llm` and a list of messages `list[Mapping[str, str]]` |
|
||||
| `step` | Performs one step using the CodeAct Agent. This includes gathering info on previous steps and prompting the model to make a command to execute. |
|
||||
| `search_memory` | Not yet implemented |
|
||||
| `search_memory` | Not yet implemented |
|
||||
|
||||
### Work-in-progress & Next step
|
||||
|
||||
[] Support web-browsing
|
||||
[] Complete the workflow for CodeAct agent to submit Github PRs
|
||||
|
||||
|
||||
## Monologue Agent
|
||||
|
||||
### Description
|
||||
|
||||
@@ -58,18 +58,13 @@ Explore the codebase of OpenDevin on [GitHub](https://github.com/OpenDevin/OpenD
|
||||
|
||||
## 🛠️ Getting Started
|
||||
|
||||
The easiest way to run OpenDevin is inside a Docker container.
|
||||
The easiest way to run OpenDevin is inside a Docker container. It works best with the most recent version of Docker, `26.0.0`.
|
||||
You must be using Linux, Mac OS, or WSL on Windows.
|
||||
|
||||
To start the app, run these commands, replacing `$(pwd)/workspace` with the path to the code you want OpenDevin to work with.
|
||||
To start the app, run these commands, replacing `$(pwd)/workspace` with the directory you want OpenDevin to work with.
|
||||
|
||||
```
|
||||
# Your OpenAI API key, or any other LLM API key
|
||||
export LLM_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
```
|
||||
# The directory you want OpenDevin to modify.
|
||||
# MUST be an absolute path!
|
||||
# The directory you want OpenDevin to work with. It MUST be an absolute path!
|
||||
export WORKSPACE_BASE=$(pwd)/workspace
|
||||
```
|
||||
|
||||
@@ -79,7 +74,10 @@ OpenDevin runs bash commands within a Docker sandbox, so it should not affect yo
|
||||
|
||||
```
|
||||
docker run \
|
||||
-it \
|
||||
--pull=always \
|
||||
-e LLM_API_KEY \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
@@ -88,15 +86,15 @@ docker run \
|
||||
ghcr.io/opendevin/opendevin:0.5
|
||||
```
|
||||
|
||||
You'll find opendevin running at [http://localhost:3000](http://localhost:3000).
|
||||
You'll find OpenDevin running at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
:::tip
|
||||
If you want to use the **(unstable!)** bleeding edge, you can use `ghcr.io/opendevin/opendevin:main` as the image (last line).
|
||||
:::
|
||||
|
||||
See Development.md for instructions on running OpenDevin without Docker.
|
||||
See [Development.md](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) for instructions on running OpenDevin without Docker.
|
||||
|
||||
Having trouble? Check out our Troubleshooting Guide.
|
||||
Are you having trouble? Check out our [Troubleshooting Guide](https://opendevin.github.io/OpenDevin/modules/usage/troubleshooting).
|
||||
|
||||
:::warning
|
||||
OpenDevin is currently a work in progress, but you can already run the alpha version to see the end-to-end system in action.
|
||||
|
||||
@@ -44,4 +44,4 @@ are actively working on building better open source models!
|
||||
|
||||
Some LLMs have rate limits and may require retries. OpenDevin will automatically retry requests if it receives a 429 error or API connection error.
|
||||
You can set `LLM_NUM_RETRIES`, `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` environment variables to control the number of retries and the time between retries.
|
||||
By default, `LLM_NUM_RETRIES` is 5 and `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` are 3 seconds and respectively 60 seconds.
|
||||
By default, `LLM_NUM_RETRIES` is 5 and `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` are 3 seconds and 60 seconds respectively.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Local LLM with Ollama
|
||||
|
||||
Ensure that you have the Ollama server up and running.
|
||||
For detailed startup instructions, refer to the [here](https://github.com/ollama/ollama)
|
||||
For detailed startup instructions, refer to [here](https://github.com/ollama/ollama)
|
||||
|
||||
This guide assumes you've started ollama with `ollama serve`. If you're running ollama differently (e.g. inside docker), the instructions might need to be modified. Please note that if you're running wsl the default ollama configuration blocks requests from docker containers. See [here](#4-configuring-the-ollama-service-wsl).
|
||||
This guide assumes you've started ollama with `ollama serve`. If you're running ollama differently (e.g. inside docker), the instructions might need to be modified. Please note that if you're running WSL the default ollama configuration blocks requests from docker containers. See [here](#configuring-the-ollama-service-wsl).
|
||||
|
||||
## Pull Models
|
||||
|
||||
@@ -44,6 +44,8 @@ For example:
|
||||
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" \
|
||||
@@ -85,7 +87,7 @@ And now you're ready to go!
|
||||
|
||||
## Configuring the ollama service (WSL)
|
||||
|
||||
The default configuration for ollama in wsl only serves localhost. This means you can't reach it from a docker container. eg. it wont work with OpenDevin. First let's test that ollama is running correctly.
|
||||
The default configuration for ollama in WSL only serves localhost. This means you can't reach it from a docker container. eg. it wont work with OpenDevin. First let's test that ollama is running correctly.
|
||||
|
||||
```bash
|
||||
ollama list # get list of installed models
|
||||
@@ -94,7 +96,7 @@ curl http://localhost:11434/api/generate -d '{"model":"[NAME]","prompt":"hi"}'
|
||||
#ex. curl http://localhost:11434/api/generate -d '{"model":"codellama","prompt":"hi"}' #the tag is optional if there is only one
|
||||
```
|
||||
|
||||
Once that is done test that it allows "outside" requests, like those from inside a docker container.
|
||||
Once that is done, test that it allows "outside" requests, like those from inside a docker container.
|
||||
|
||||
```bash
|
||||
docker ps # get list of running docker containers, for most accurate test choose the open devin sandbox container.
|
||||
@@ -104,7 +106,7 @@ docker exec [CONTAINER ID] curl http://host.docker.internal:11434/api/generate -
|
||||
|
||||
## Fixing it
|
||||
|
||||
Now let's make it work, edit /etc/systemd/system/ollama.service with sudo privileges. (Path may vary depending on linux flavor)
|
||||
Now let's make it work. Edit /etc/systemd/system/ollama.service with sudo privileges. (Path may vary depending on linux flavor)
|
||||
|
||||
```bash
|
||||
sudo vi /etc/systemd/system/ollama.service
|
||||
|
||||
@@ -42,6 +42,7 @@ OpenDevin uses a docker container to do its work safely, without potentially bre
|
||||
* Run `docker ps` to ensure that docker is running
|
||||
* Make sure you don't need `sudo` to run docker [see here](https://www.baeldung.com/linux/docker-run-without-sudo)
|
||||
* If you are on a mac, check the [permissions requirements](https://docs.docker.com/desktop/mac/permission-requirements/) and in particular consider enabling the "Allow the default Docker socket to be used" under "Settings > Advanced" in Docker Desktop.
|
||||
* If you are on a mac, Upgrade your Docker to the latest version under "Check for Updates"
|
||||
|
||||
## Unable to connect to SSH box
|
||||
[GitHub Issue](https://github.com/OpenDevin/OpenDevin/issues/1156)
|
||||
|
||||
@@ -7,7 +7,7 @@ Please be sure to run all commands inside your WSL terminal.
|
||||
|
||||
### Failed to create opendevin user
|
||||
|
||||
If you encounter the following error during setup: `Exception: Failed to create opendevin user in sandbox: b'useradd: UID 0 is not unique\n'`
|
||||
If you encounter the following error during setup: `Exception: Failed to create opendevin user in sandbox: b'useradd: UID 0 is not unique\n'`.
|
||||
You can resolve it by running:
|
||||
` export SANDBOX_USER_ID=1000
|
||||
`
|
||||
@@ -20,7 +20,7 @@ If you face issues running Poetry even after installing it during the build proc
|
||||
|
||||
### NoneType object has no attribute 'request'
|
||||
|
||||
If you experiencing issues related to networking, such as `NoneType object has no attribute 'request'` when executing `make run`, you may need to configure your WSL2 networking settings. Follow these steps:
|
||||
If you are experiencing issues related to networking, such as `NoneType object has no attribute 'request'` when executing `make run`, you may need to configure your WSL2 networking settings. Follow these steps:
|
||||
|
||||
- Open or create the `.wslconfig` file located at `C:\Users\%username%\.wslconfig` on your Windows host machine.
|
||||
- Add the following configuration to the `.wslconfig` file:
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import Link from "@docusaurus/Link";
|
||||
import { Header } from "@site/src/pages";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export function Code() {
|
||||
const workspaceCode = `# The directory you want OpenDevin to modify. MUST be an absolute path!
|
||||
export WORKSPACE_BASE=$(pwd)/workspace`;
|
||||
|
||||
const dockerCode = `docker run \\
|
||||
--pull=always \\
|
||||
-e SANDBOX_USER_ID=$(id -u) \\
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \\
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-p 3000:3000 \\
|
||||
--add-host host.docker.internal:host-gateway \\
|
||||
ghcr.io/opendevin/opendevin:0.5`;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.innerContainer}>
|
||||
<div className={styles.header}>
|
||||
<Header
|
||||
title="Getting Started"
|
||||
summary="Getting Started"
|
||||
description="Get started using OpenDevin in just a few lines of code"
|
||||
></Header>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/modules/usage/intro"
|
||||
>
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<CodeBlock language="python" code={workspaceCode} />
|
||||
<CodeBlock language="python" code={dockerCode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
import { Highlight, themes } from "prism-react-renderer";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
interface CodeBlockProps {
|
||||
language: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ language, code }: CodeBlockProps) {
|
||||
const [state, copyToClipboard] = useCopyToClipboard();
|
||||
const { isDarkTheme } = useColorMode();
|
||||
|
||||
const copyCode = () => {
|
||||
copyToClipboard(code);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Highlight
|
||||
theme={isDarkTheme ? themes.vsLight : themes.vsDark}
|
||||
code={code}
|
||||
language={language}
|
||||
>
|
||||
{({ style, tokens, getLineProps, getTokenProps }) => (
|
||||
<pre style={style}>
|
||||
{tokens.map((line, i) => (
|
||||
<div key={i} {...getLineProps({ line })}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "3em",
|
||||
color: "var(--gray)",
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
<button
|
||||
className="button button--secondary"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
right: "10px",
|
||||
}}
|
||||
onClick={copyCode}
|
||||
>
|
||||
{state.value ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 25px;
|
||||
padding-bottom: 25px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.innerContainer {
|
||||
padding: 50px;
|
||||
width: 100%;
|
||||
max-width: 1300px;
|
||||
padding-top: 30px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,26 @@ export default function FAQ() {
|
||||
scenarios, producing works that significantly contribute to the
|
||||
community and pave the way for future advancements.
|
||||
</p>
|
||||
<h3>How to fix an issue on OpenDevin?</h3>
|
||||
<p>
|
||||
To fix an issue on GitHub using OpenDevin, send a prompt to OpenDevin asking it to follow these steps:
|
||||
<ol>
|
||||
<li>Read the issue on <a href="https://github.com/OpenDevin/OpenDevin/issues/1611">GitHub</a></li>
|
||||
<li>Clone the repository and check out a new branch</li>
|
||||
<li>Based on the instructions in the issue description, modify files to fix the issue</li>
|
||||
<li>Push the resulting output to GitHub using the GITHUB_TOKEN environment variable</li>
|
||||
<li>Tell me the link that I need to go to to send a pull request</li>
|
||||
</ol>
|
||||
Before you run OpenDevin, you can do:
|
||||
<pre>
|
||||
export SANDBOX_ENV_GITHUB_TOKEN=XXX
|
||||
</pre>
|
||||
where XXX is a GitHub token that you created that has permissions to push to the OpenDevin repo. If you don’t have write permission to the OpenDevin repo, you might need to change that to:
|
||||
<pre>
|
||||
4. Push the resulting output to my fork at https://github.com/USERNAME/OpenDevin/ using the GITHUB_TOKEN environment variable
|
||||
</pre>
|
||||
where USERNAME is your GitHub username.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import Layout from "@theme/Layout";
|
||||
|
||||
import { Code } from "../components/Code/Code";
|
||||
import { HomepageHeader } from "../components/HomepageHeader/HomepageHeader";
|
||||
import { Welcome } from "../components/Welcome/Welcome";
|
||||
|
||||
@@ -25,7 +24,6 @@ export default function Home(): JSX.Element {
|
||||
<HomepageHeader />
|
||||
<div>
|
||||
<Welcome />
|
||||
<Code />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 453 KiB |
@@ -0,0 +1,44 @@
|
||||
# EDA Evaluation
|
||||
|
||||
This folder contains evaluation harness for evaluating agents on the Entity-deduction-Arena Benchmark, from the paper [Probing the Multi-turn Planning Capabilities of LLMs via 20 Question Games](https://arxiv.org/abs/2310.01468), presented in ACL 2024 main conference.
|
||||
|
||||
## Configure OpenDevin and your LLM
|
||||
|
||||
Create a `config.toml` file if it does not exist at the root of the workspace. Please check [README.md](../../README.md) for how to set this up.
|
||||
|
||||
## Start the evaluation
|
||||
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="sk-XXX"; # This is required for evaluation (to simulate another party of conversation)
|
||||
./evaluation/EDA/scripts/run_infer.sh [model_config] [agent] [dataset] [eval_limit]
|
||||
```
|
||||
|
||||
where `model_config` is mandatory, while `agent`, `dataset` and `eval_limit` are optional.
|
||||
|
||||
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
|
||||
LLM settings, as defined in your `config.toml`.
|
||||
|
||||
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
|
||||
to `CodeActAgent`.
|
||||
|
||||
- `dataset`: There are two tasks in this evaluation. Specify `dataset` to test on either `things` or `celebs` task.
|
||||
|
||||
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances. By default it infers all instances.
|
||||
|
||||
Let's say you'd like to run 10 instances using `eval_gpt4_1106_eval_gpt4o_2024_05_13preview` and CodeActAgent,
|
||||
then your command would be:
|
||||
|
||||
```bash
|
||||
./evaluation/EDA/scripts/run_infer.sh eval_gpt4o_2024_05_13 CodeActAgent things
|
||||
```
|
||||
|
||||
## Reference
|
||||
```
|
||||
@inproceedings{zhang2023entity,
|
||||
title={Probing the Multi-turn Planning Capabilities of LLMs via 20 Question Games},
|
||||
author={Zhang, Yizhe and Lu, Jiarui and Jaitly, Navdeep},
|
||||
journal={ACL},
|
||||
year={2024}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,413 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import openai
|
||||
import requests.exceptions
|
||||
import torch
|
||||
from openai import OpenAI
|
||||
from retry import retry
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_model(path):
|
||||
print('Loading model...')
|
||||
tokenizer = AutoTokenizer.from_pretrained(path, use_fast=False)
|
||||
print('Tokenizer loaded.')
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
path, low_cpu_mem_usage=True, torch_dtype=torch.float16
|
||||
).cuda()
|
||||
print('Model loaded.')
|
||||
# model.half().cuda()
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
class Q20Game:
|
||||
def __init__(
|
||||
self,
|
||||
item: str,
|
||||
answerer_model: str = 'gpt-3.5-turbo-0613',
|
||||
guesser_model: str = 'gpt-3.5-turbo-0613',
|
||||
num_turns: int = 20,
|
||||
temperature: float = 0.8,
|
||||
openai_api: bool = True,
|
||||
openai_api_key: Optional[str] = None,
|
||||
guesser_kargs={},
|
||||
) -> None:
|
||||
self.item = item
|
||||
self.answerer_model = answerer_model
|
||||
self.guesser_model = guesser_model
|
||||
self.num_turns = num_turns
|
||||
self.temperature = temperature
|
||||
self.openai_api = openai_api
|
||||
self.guesser_kargs = guesser_kargs
|
||||
self.vicuna_prompt = "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions."
|
||||
self.first_user_utterance = (
|
||||
'Your task is to ask a series of questions to deduce the entity '
|
||||
"that I'm thinking of with as few queries as possible. "
|
||||
"Only ask questions that can be answered by 'yes', 'no' or 'maybe'. "
|
||||
'Do not ask for hint. Make your question brief with no linebreaker. '
|
||||
'Now start asking a question.'
|
||||
)
|
||||
self.guesser_win = False
|
||||
self.curr_turn = 0
|
||||
if openai_api_key is not None:
|
||||
openai.api_key = openai_api_key
|
||||
|
||||
if isinstance(answerer_model, str) and not answerer_model.startswith('gpt'):
|
||||
self.user_api_base = 'http://0.0.0.0:8000/v1'
|
||||
else:
|
||||
self.user_api_base = 'https://api.openai.com/v1'
|
||||
|
||||
if isinstance(guesser_model, str) and not guesser_model.startswith('gpt'):
|
||||
self.guesser_api_base = 'http://0.0.0.0:8000/v1'
|
||||
else:
|
||||
self.guesser_api_base = 'https://api.openai.com/v1'
|
||||
|
||||
self.guesser_messages = []
|
||||
|
||||
def confusion_matrix(self, path):
|
||||
self.reset()
|
||||
with open(path) as f:
|
||||
raw_messages = json.load(f)
|
||||
self.item = path.split('/')[-1].split('_')[0]
|
||||
roles = ['assistant', 'user']
|
||||
for i, message in enumerate(raw_messages):
|
||||
self.guesser_messages.append(
|
||||
{'role': roles[i % 2], 'content': message['content']}
|
||||
)
|
||||
|
||||
self.guesser_messages = self.guesser_messages[:-2]
|
||||
self.guesser_messages[-1]['content'] = (
|
||||
self.guesser_messages[-1]['content'] + " You must guess now, what's it?"
|
||||
)
|
||||
guesser_msg = self.guesser(self.guesser_messages)
|
||||
self.guesser_messages.append(guesser_msg)
|
||||
guesser_question = guesser_msg['content'].strip()
|
||||
self.guesser_messages[-1]['content'] = (
|
||||
self.guesser_messages[-1]['content'] + ' Is it right?'
|
||||
)
|
||||
usr_msg = self.answerer(guesser_question)
|
||||
self.guesser_messages.append(
|
||||
{'role': 'user', 'content': f"{usr_msg['content'].strip()}"}
|
||||
)
|
||||
|
||||
if 'bingo' in self.guesser_messages[-1]['content'].lower():
|
||||
self.guesser_win = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@retry(
|
||||
(
|
||||
openai.Timeout,
|
||||
requests.exceptions.ReadTimeout,
|
||||
openai.RateLimitError,
|
||||
openai.APIError,
|
||||
requests.exceptions.HTTPError,
|
||||
openai.APIConnectionError,
|
||||
),
|
||||
tries=5,
|
||||
delay=0.5,
|
||||
backoff=0.5,
|
||||
max_delay=2,
|
||||
logger=LOGGER,
|
||||
)
|
||||
def guesser(self, messages):
|
||||
if not self.guesser_model.startswith('gpt'): # hf model
|
||||
self.guesser_model, self.guesser_tokenizer = load_model(self.guesser_model)
|
||||
|
||||
# """Wraps hf's `generate` adding some specific method's defaults"""
|
||||
assert not self.openai_api
|
||||
prompt = self.dialog_history() + ' ASSISTANT:'
|
||||
input_ids = torch.tensor(
|
||||
[self.guesser_tokenizer.encode(prompt, add_special_tokens=True)]
|
||||
) # TODO check if huggingface is using the same format.
|
||||
input_ids = input_ids.to(self.guesser_model.base_model.device)
|
||||
attention_mask = None
|
||||
|
||||
with torch.no_grad():
|
||||
gen = self.guesser_model.generate(
|
||||
input_ids=input_ids,
|
||||
attention_mask=attention_mask,
|
||||
**self.guesser_kargs,
|
||||
)
|
||||
gen_str = (
|
||||
self.guesser_tokenizer.decode(gen[0][input_ids[0].shape[0] :])
|
||||
.split('</s>')[0]
|
||||
.split('USER')[0]
|
||||
.lstrip()
|
||||
.strip()
|
||||
)
|
||||
|
||||
return {
|
||||
'role': 'assistant',
|
||||
'content': gen_str,
|
||||
}
|
||||
else:
|
||||
openai.api_base = self.guesser_api_base
|
||||
client = OpenAI(api_key=openai.api_key)
|
||||
response = client.chat.completions.create(
|
||||
model=self.guesser_model,
|
||||
messages=messages,
|
||||
max_tokens=64,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=self.temperature,
|
||||
)
|
||||
return {
|
||||
'role': 'assistant',
|
||||
'content': response.choices[0].message.to_dict()['content'].strip(),
|
||||
}
|
||||
|
||||
def dialog_history(self):
|
||||
history = self.vicuna_prompt + ' '
|
||||
for item in self.guesser_messages:
|
||||
if item['role'].upper() == 'USER':
|
||||
history += 'USER: ' + item['content']
|
||||
elif item['role'].upper() == 'ASSISTANT':
|
||||
history += ' ' + 'ASSISTANT: ' + item['content'] + '</s>'
|
||||
return history
|
||||
|
||||
|
||||
def preprocess_response(self,response):
|
||||
response = re.sub(
|
||||
r'the entity you are thinking of', 'it', response
|
||||
)
|
||||
response = re.sub(
|
||||
r"the entity you're thinking of", 'it', response
|
||||
)
|
||||
response = re.sub(
|
||||
r" you're thinking of", '', response
|
||||
)
|
||||
response = re.sub(
|
||||
r' you are thinking of', '', response
|
||||
)
|
||||
self.guesser_messages.append(response)
|
||||
return response
|
||||
|
||||
def judge_winner(self, response):
|
||||
guesser_question = response.strip()
|
||||
|
||||
if self.curr_turn == self.num_turns - 1:
|
||||
guesser_question += ' Is it right?'
|
||||
# ask for answer
|
||||
usr_msg = self.answerer(guesser_question)
|
||||
|
||||
if 'bingo' in usr_msg['content'].lower():
|
||||
self.guesser_win = True
|
||||
return True, ""
|
||||
|
||||
return False, usr_msg['content'].strip()
|
||||
|
||||
def generate_user_response(self, response):
|
||||
response = self.preprocess_response(response)
|
||||
# others
|
||||
bingo, anwser_reply = self.judge_winner(response)
|
||||
if bingo:
|
||||
return "You are bingo! quit now, run: <execute_bash> exit </execute_bash>.\n"
|
||||
if self.curr_turn == self.num_turns - 2:
|
||||
anwser_reply += " You must guess now, what's it?"
|
||||
return anwser_reply
|
||||
|
||||
def game_play(self, user_mode=False):
|
||||
self.reset()
|
||||
# print(f"Item: {self.item}")
|
||||
for t in range(self.num_turns):
|
||||
# System asking a question
|
||||
if (not user_mode) or user_mode is None:
|
||||
guesser_msg = self.guesser(self.guesser_messages)
|
||||
guesser_msg['content'] = re.sub(
|
||||
r'the entity you are thinking of', 'it', guesser_msg['content']
|
||||
)
|
||||
guesser_msg['content'] = re.sub(
|
||||
r"the entity you're thinking of", 'it', guesser_msg['content']
|
||||
)
|
||||
guesser_msg['content'] = re.sub(
|
||||
r" you're thinking of", '', guesser_msg['content']
|
||||
)
|
||||
guesser_msg['content'] = re.sub(
|
||||
r' you are thinking of', '', guesser_msg['content']
|
||||
)
|
||||
else:
|
||||
user_q = input(
|
||||
f'Type in your questions for turn {t+1}. (e.g. Is it a living thing?)\n'
|
||||
)
|
||||
guesser_msg = {'role': 'assistant', 'content': user_q}
|
||||
self.guesser_messages.append(guesser_msg)
|
||||
guesser_question = guesser_msg['content'].strip()
|
||||
|
||||
if t == self.num_turns - 1:
|
||||
self.guesser_messages[-1]['content'] = (
|
||||
self.guesser_messages[-1]['content'] + ' Is it right?'
|
||||
)
|
||||
|
||||
usr_msg = self.answerer(guesser_question)
|
||||
self.guesser_messages.append(
|
||||
{'role': 'user', 'content': f"{usr_msg['content'].strip()}"}
|
||||
)
|
||||
|
||||
if 'bingo' in usr_msg['content'].lower():
|
||||
self.guesser_win = True
|
||||
return True
|
||||
|
||||
if t == self.num_turns - 2:
|
||||
self.guesser_messages[-1]['content'] = (
|
||||
self.guesser_messages[-1]['content']
|
||||
+ " You must guess now, what's it?"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def save_session(self, path):
|
||||
# Print the conversation
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
output_file = os.path.join(path, f'{self.item}.txt')
|
||||
with open(output_file, 'w') as out_f:
|
||||
out_f.write(f'item: {self.item}\n')
|
||||
for t, message in enumerate(self.guesser_messages):
|
||||
out_f.write(
|
||||
f"Turn {(t+1)//2}, {message['role'].capitalize()}: {message['content'].lstrip()}\n"
|
||||
)
|
||||
|
||||
def reward(self):
|
||||
if self.guesser_win:
|
||||
n_turns = (len(self.guesser_messages) + 1) // 2
|
||||
return 1 - max(n_turns - 5, 0) * 0.02
|
||||
return 0
|
||||
|
||||
def num_success(self):
|
||||
return 1 if self.guesser_win else 0
|
||||
|
||||
def num_yes(self):
|
||||
n_yes = sum(
|
||||
['yes' in msg['content'].lower() for msg in self.guesser_messages[2::2]]
|
||||
)
|
||||
return n_yes
|
||||
|
||||
@retry(
|
||||
(
|
||||
openai.Timeout,
|
||||
requests.exceptions.ReadTimeout,
|
||||
openai.RateLimitError,
|
||||
openai.APIError,
|
||||
openai.APIConnectionError,
|
||||
),
|
||||
tries=5,
|
||||
delay=0.5,
|
||||
backoff=0.5,
|
||||
max_delay=2,
|
||||
logger=LOGGER,
|
||||
)
|
||||
def answerer(self, question):
|
||||
openai.api_base = self.user_api_base
|
||||
client = OpenAI(api_key=openai.api_key)
|
||||
user_messages = [
|
||||
{
|
||||
'role': 'user',
|
||||
'content': f'Based on your knowledge about {self.item}, '
|
||||
f'respond to the following question or guess. '
|
||||
f"Limit your respond to only 'Yes.', 'No.' or 'Maybe.', with no explanation or other words. "
|
||||
f'Never say the answer {self.item} in your response. '
|
||||
f"If the question is to solicit the answer, respond 'No.'.",
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': f'For the entity {self.item}, {question} (Yes/No/Maybe)',
|
||||
},
|
||||
]
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=self.answerer_model,
|
||||
messages=user_messages,
|
||||
max_tokens=6,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.2,
|
||||
)
|
||||
if any(
|
||||
[
|
||||
re.search(rf'(?:^|\W){i.strip().lower()}(?:$|\W)', question.lower())
|
||||
for i in self.item.lower().split('|')
|
||||
]
|
||||
):
|
||||
response.choices[0].message.content = 'Bingo!'
|
||||
return response.choices[0].message.to_dict()
|
||||
|
||||
def reset(self):
|
||||
# Initialize the conversation
|
||||
self.curr_turn = 0
|
||||
self.guesser_messages = [
|
||||
{
|
||||
'role': 'user',
|
||||
'content': self.first_user_utterance,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class Q20GameCelebrity(Q20Game):
|
||||
def __init__(self, item: str, **kwargs) -> None:
|
||||
super().__init__(item, **kwargs)
|
||||
self.first_user_utterance = (
|
||||
'Your task is to ask a series of questions to deduce the celebrity '
|
||||
"that I'm thinking of with as few queries as possible. "
|
||||
"Only ask factual questions that can be answered by 'Yes.', 'No.' or 'Dunno.'. Do not ask for hint. Make your question brief with no linebreaker. "
|
||||
'Now start asking a question.'
|
||||
)
|
||||
|
||||
@retry(
|
||||
(
|
||||
openai.Timeout,
|
||||
requests.exceptions.ReadTimeout,
|
||||
openai.RateLimitError,
|
||||
openai.APIError,
|
||||
openai.APIConnectionError,
|
||||
),
|
||||
tries=5,
|
||||
delay=0.5,
|
||||
backoff=0.5,
|
||||
max_delay=2,
|
||||
logger=LOGGER,
|
||||
)
|
||||
def answerer(self, question):
|
||||
openai.api_base = self.user_api_base
|
||||
user_messages = [
|
||||
{
|
||||
'role': 'system',
|
||||
'content': f'Based on on your knowledge about the celebrity: {self.item}, '
|
||||
f'respond to the following question or guess. '
|
||||
f"Limit your respond to only 'Yes.', 'No.' or 'Dunno.', with no explanation or other words. "
|
||||
f"Never say the name {self.item} in your response. Do not say 'Dunno.' if it can be answered by 'Yes.' or 'No.' "
|
||||
f"If the question is to solicit the answer, respond 'No.'.",
|
||||
},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': f'For the celebrity {self.item}, {question}(Yes/No/Dunno)',
|
||||
},
|
||||
]
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model=self.answerer_model,
|
||||
messages=user_messages,
|
||||
max_tokens=6,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.2,
|
||||
)
|
||||
if re.search(rf'(?:^|\W){self.item.lower()}(?:$|\W)', question.lower()):
|
||||
response.choices[0].message.content = 'Bingo!'
|
||||
return response.choices[0].message.to_dict()
|
||||
|
||||
def reset(self):
|
||||
# Initialize the conversation
|
||||
self.guesser_messages = [
|
||||
{
|
||||
'role': 'user',
|
||||
'content': self.first_user_utterance,
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,329 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
# import huggingface_hub
|
||||
from datasets import load_dataset
|
||||
from tqdm import tqdm
|
||||
|
||||
from evaluation.EDA.game import Q20Game, Q20GameCelebrity
|
||||
|
||||
# from evaluation.EDA.scorer import question_scorer
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.config import config, get_llm_config_arg, get_parser
|
||||
from opendevin.core.logger import get_console_handler
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.main import main
|
||||
from opendevin.events.action import MessageAction
|
||||
from opendevin.events.serialization.event import event_to_dict
|
||||
|
||||
game = None
|
||||
|
||||
|
||||
def cleanup():
|
||||
print('Cleaning up child processes...')
|
||||
for process in mp.active_children():
|
||||
print(f'Terminating child process: {process.name}')
|
||||
process.terminate()
|
||||
process.join()
|
||||
|
||||
|
||||
def codeact_user_response(state: State) -> str:
|
||||
global game
|
||||
model_guess = ''
|
||||
if state.history:
|
||||
for act, _ in reversed(state.history):
|
||||
if isinstance(act, MessageAction) and act.source == 'agent':
|
||||
model_guess = act.content
|
||||
break
|
||||
msg = game.generate_user_response(model_guess)
|
||||
game.curr_turn += 1
|
||||
logger.info(f'Model guess: {model_guess}')
|
||||
logger.info(f'Anwser response: {msg}')
|
||||
return msg
|
||||
|
||||
|
||||
def monologue_user_response(state: State) -> str:
|
||||
raise NotImplementedError('MonologueAgent should never ask for user responses.')
|
||||
|
||||
|
||||
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
|
||||
'CodeActAgent': codeact_user_response,
|
||||
'MonologueAgent': monologue_user_response,
|
||||
}
|
||||
|
||||
AGENT_CLS_TO_INST_SUFFIX = {
|
||||
'CodeActAgent': 'When you think you have solved the question, please first send your answer to user through message and then exit.\n'
|
||||
}
|
||||
|
||||
|
||||
def process_instance(instance, agent_class, metadata, reset_logger: bool = True):
|
||||
# Setup the logger properly, so you can run multi-processing to parallize the evaluation
|
||||
eval_output_dir = metadata['eval_output_dir']
|
||||
if reset_logger:
|
||||
# Set up logger
|
||||
log_file = os.path.join(
|
||||
eval_output_dir, 'logs', f'instance_{instance["text"].strip()}.log'
|
||||
)
|
||||
# Remove all existing handlers from logger
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
# add back the console handler to print ONE line
|
||||
logger.addHandler(get_console_handler())
|
||||
logger.info(
|
||||
f'Starting evaluation for instance {instance["text"].strip()}.\nLOG: tail -f {log_file}'
|
||||
)
|
||||
# Remove all existing handlers from logger
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Prepare instruction
|
||||
_game_class = {'things': Q20Game, 'celebs': Q20GameCelebrity}
|
||||
|
||||
guesser_kargs = {
|
||||
'max_new_tokens': 64,
|
||||
'temperature': 0.8,
|
||||
'repetition_penalty': 1.0,
|
||||
'do_sample': True,
|
||||
} # no penalty
|
||||
|
||||
# Use codeactagent as guesser_model
|
||||
global game
|
||||
game = _game_class[metadata['dataset']](
|
||||
item=instance['text'].strip(),
|
||||
answerer_model=metadata['answerer_model'],
|
||||
guesser_model=None,
|
||||
num_turns=metadata['max_iterations'],
|
||||
openai_api_key=metadata['openai_api'],
|
||||
guesser_kargs=guesser_kargs,
|
||||
)
|
||||
|
||||
instruction = f'{game.first_user_utterance}'
|
||||
logger.info(f'Instruction: {instruction}')
|
||||
|
||||
# instruction += 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
|
||||
# NOTE: You can actually set slightly different instruction for different agents
|
||||
instruction += AGENT_CLS_TO_INST_SUFFIX.get(agent_class, '')
|
||||
|
||||
# Here's how you can run the agent (similar to the `main` function) and get the final task state
|
||||
|
||||
state: State = asyncio.run(
|
||||
main(
|
||||
instruction,
|
||||
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(agent_class),
|
||||
)
|
||||
)
|
||||
# ======= Attempt to evaluate the agent's edits =======
|
||||
# If you are working on simplier benchmark that only evaluates the final model output (e.g., in a MessageAction)
|
||||
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
|
||||
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
final_message = ''
|
||||
for act, _ in reversed(state.history):
|
||||
if isinstance(act, MessageAction) and act.source == 'agent':
|
||||
final_message = act.content
|
||||
break
|
||||
|
||||
logger.info(f'Final message: {final_message} | Ground truth: {instance["text"]}')
|
||||
test_result = game.reward()
|
||||
|
||||
# Save the output
|
||||
output = {
|
||||
'instance_id': instance['text'].strip(),
|
||||
'instance': instance,
|
||||
'instruction': instruction,
|
||||
'metadata': metadata,
|
||||
'history': [
|
||||
(event_to_dict(action), event_to_dict(obs)) for action, obs in state.history
|
||||
],
|
||||
'error': state.error if state and state.error else None,
|
||||
'test_result': {
|
||||
'success': test_result,
|
||||
'final_message': final_message,
|
||||
'ground_truth': instance['text'],
|
||||
},
|
||||
}
|
||||
|
||||
return output
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_parser()
|
||||
parser.add_argument(
|
||||
'--answerer_model', '-a', default='gpt-3.5-turbo', help='answerer model'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dataset',
|
||||
default='things',
|
||||
choices=['things', 'celebs'],
|
||||
type=str,
|
||||
help='dataset to be used',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--OPENAI_API_KEY', type=str, required=True, help='Your OpenAI API key'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--data-split',
|
||||
default='test',
|
||||
type=str,
|
||||
help='data split, eg, test',
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
if args.directory:
|
||||
config.workspace_base = os.path.abspath(args.directory)
|
||||
print(f'Setting workspace base to {config.workspace_base}')
|
||||
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
|
||||
# so we don't need to manage file uploading to OpenDevin's repo
|
||||
eda_dataset = load_dataset(
|
||||
'yizheapple/entity-deduction-arena', name=args.dataset, split=args.data_split
|
||||
)
|
||||
logger.info(
|
||||
f'Evaluating Entity Deduction Arena {args.dataset} {args.data_split} split'
|
||||
)
|
||||
|
||||
# Check https://github.com/OpenDevin/OpenDevin/blob/main/evaluation/swe_bench/README.md#configure-opendevin-and-your-llm
|
||||
# for details of how to set `llm_config`
|
||||
if args.llm_config:
|
||||
specified_llm_config = get_llm_config_arg(args.llm_config)
|
||||
if specified_llm_config:
|
||||
config.llm = specified_llm_config
|
||||
logger.info(f'Config for evaluation: {config}')
|
||||
|
||||
# TEST METADATA
|
||||
agent_class = args.agent_cls
|
||||
assert (
|
||||
agent_class in AGENT_CLS_TO_FAKE_USER_RESPONSE_FN
|
||||
), f'Unsupported agent class: {agent_class}'
|
||||
model_name = config.llm.model.split('/')[-1]
|
||||
max_iterations = args.max_iterations
|
||||
eval_note = ''
|
||||
if args.eval_note is not None:
|
||||
eval_note += '_N_' + args.eval_note
|
||||
eval_output_dir = os.path.join(
|
||||
args.eval_output_dir,
|
||||
'eda',
|
||||
agent_class,
|
||||
model_name + '_maxiter_' + str(max_iterations) + eval_note,
|
||||
)
|
||||
|
||||
pathlib.Path(eval_output_dir).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(eval_output_dir, 'logs')).mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
logger.info(f'Using evaluation output directory: {eval_output_dir}')
|
||||
|
||||
metadata = {
|
||||
'dataset': args.dataset,
|
||||
'data_split': args.data_split,
|
||||
'answerer_model': args.answerer_model,
|
||||
'agent_class': agent_class,
|
||||
'openai_api': args.OPENAI_API_KEY,
|
||||
'model_name': model_name,
|
||||
'max_iterations': max_iterations,
|
||||
'eval_output_dir': eval_output_dir,
|
||||
'start_time': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
# get the commit id of current repo for reproduciblity
|
||||
'git_commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'])
|
||||
.decode('utf-8')
|
||||
.strip(),
|
||||
}
|
||||
logger.info(f'Metadata: {metadata}')
|
||||
with open(os.path.join(eval_output_dir, 'metadata.json'), 'w') as f:
|
||||
json.dump(metadata, f)
|
||||
|
||||
# LIMIT EVALUATION
|
||||
eval_n_limit = args.eval_n_limit
|
||||
if eval_n_limit:
|
||||
eda_dataset = eda_dataset.select(list(range(eval_n_limit)))
|
||||
logger.info(f'Limiting evaluation to first {eval_n_limit} instances.')
|
||||
|
||||
# OUTPUT FILE
|
||||
output_file = os.path.join(eval_output_dir, 'output.jsonl')
|
||||
logger.info(f'Writing evaluation output to {output_file}')
|
||||
finished_items = set()
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
for line in f:
|
||||
data = json.loads(line)
|
||||
finished_items.add(data['instance_id'])
|
||||
logger.warning(
|
||||
f'Output file {output_file} already exists. Loaded {len(finished_items)} finished instances.'
|
||||
)
|
||||
output_fp = open(output_file, 'a')
|
||||
|
||||
logger.info(
|
||||
f'Evaluation started with Agent {agent_class}, model {model_name}, max iterations {max_iterations}.'
|
||||
)
|
||||
|
||||
# =============================================
|
||||
# filter out finished instances
|
||||
new_eda_dataset = []
|
||||
for instance in eda_dataset:
|
||||
if instance['text'].strip() in finished_items:
|
||||
logger.info(
|
||||
f'Skipping instance {instance["text"].strip()} as it is already finished.'
|
||||
)
|
||||
continue
|
||||
new_eda_dataset.append(instance)
|
||||
|
||||
eda_dataset = new_eda_dataset
|
||||
logger.info(
|
||||
f'Finished instances: {len(finished_items)}, Remaining instances: {len(eda_dataset)}'
|
||||
)
|
||||
# =============================================
|
||||
|
||||
pbar = tqdm(total=len(eda_dataset))
|
||||
|
||||
# This function tracks the progress AND write the output to a JSONL file
|
||||
def update_progress(future):
|
||||
pbar.update(1)
|
||||
output = future.result()
|
||||
pbar.set_description(f'Instance {output["instance_id"]}')
|
||||
pbar.set_postfix_str(f'Test Result: {output["test_result"]}')
|
||||
logger.info(
|
||||
f'Finished evaluation for instance {output["instance_id"]}: {output["test_result"]}'
|
||||
)
|
||||
output_fp.write(json.dumps(output) + '\n')
|
||||
output_fp.flush()
|
||||
|
||||
# This sets the multi-processing
|
||||
num_workers = args.eval_num_workers
|
||||
logger.info(f'Using {num_workers} workers for evaluation.')
|
||||
|
||||
try:
|
||||
with ProcessPoolExecutor(num_workers) as executor:
|
||||
futures = []
|
||||
# This is how we perform multi-processing
|
||||
for instance in eda_dataset:
|
||||
future = executor.submit(
|
||||
process_instance,
|
||||
instance,
|
||||
agent_class,
|
||||
metadata,
|
||||
reset_logger=bool(num_workers > 1),
|
||||
)
|
||||
future.add_done_callback(update_progress)
|
||||
futures.append(future)
|
||||
|
||||
# Wait for all futures to complete
|
||||
for future in futures:
|
||||
future.result()
|
||||
except KeyboardInterrupt:
|
||||
print('KeyboardInterrupt received. Cleaning up...')
|
||||
cleanup()
|
||||
|
||||
output_fp.close()
|
||||
logger.info('Evaluation finished.')
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
MODEL_CONFIG=$1
|
||||
AGENT=$2
|
||||
DATASET=$3
|
||||
EVAL_LIMIT=$4
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
echo "Agent not specified, use default CodeActAgent"
|
||||
AGENT="CodeActAgent"
|
||||
fi
|
||||
|
||||
if [ -z "$DATASET" ]; then
|
||||
echo "Dataset not specified, use default 'things'"
|
||||
DATASET="things"
|
||||
fi
|
||||
|
||||
# check if OPENAI_API_KEY is set
|
||||
if [ -z "$OPENAI_API_KEY" ]; then
|
||||
echo "OPENAI_API_KEY is not set, please set it to run the script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# IMPORTANT: Because Agent's prompt changes fairly often in the rapidly evolving codebase of OpenDevin
|
||||
# We need to track the version of Agent in the evaluation to make sure results are comparable
|
||||
AGENT_VERSION=v$(poetry run python -c "import agenthub; from opendevin.controller.agent import Agent; print(Agent.get_cls('$AGENT').VERSION)")
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "AGENT_VERSION: $AGENT_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "DATASET: $DATASET"
|
||||
|
||||
COMMAND="poetry run python evaluation/EDA/run_infer.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--dataset $DATASET \
|
||||
--data-split test \
|
||||
--max-iterations 20 \
|
||||
--OPENAI_API_KEY $OPENAI_API_KEY \
|
||||
--max-chars 10000000 \
|
||||
--eval-num-workers 1 \
|
||||
--eval-note ${AGENT_VERSION}_${DATASET}"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
+12
-64
@@ -4,76 +4,24 @@ This folder contains code and resources to run experiments and evaluations.
|
||||
|
||||
## Logistics
|
||||
To better organize the evaluation folder, we should follow the rules below:
|
||||
- Each subfolder contains a specific benchmark or experiment. For example, `evaluation/SWE-bench` should contain
|
||||
- Each subfolder contains a specific benchmark or experiment. For example, `evaluation/swe_bench` should contain
|
||||
all the preprocessing/evaluation/analysis scripts.
|
||||
- Raw data and experimental records should not be stored within this repo (e.g. Google Drive or Hugging Face Datasets).
|
||||
- Raw data and experimental records should not be stored within this repo.
|
||||
- For model outputs, they should be stored at [this huggingface space](https://huggingface.co/spaces/OpenDevin/evaluation) for visualization.
|
||||
- Important data files of manageable size and analysis scripts (e.g., jupyter notebooks) can be directly uploaded to this repo.
|
||||
|
||||
## Roadmap
|
||||
## Supported Benchmarks
|
||||
|
||||
- Sanity check. Reproduce Devin's scores on SWE-bench using the released outputs to make sure that our harness pipeline works.
|
||||
- Open source model support.
|
||||
- Contributors are encouraged to submit their commits to our [forked SEW-bench repo](https://github.com/OpenDevin/SWE-bench).
|
||||
- Ensure compatibility with OpenAI interface for inference.
|
||||
- Serve open source models, prioritizing high concurrency and throughput.
|
||||
- SWE-Bench: [`evaluation/swe_bench`](./swe_bench)
|
||||
- HumanEvalFix: [`evaluation/humanevalfix`](./humanevalfix)
|
||||
- GAIA: [`evaluation/gaia`](./gaia)
|
||||
- Entity deduction Arena (EDA): [`evaluation/EDA`](./EDA)
|
||||
|
||||
## SWE-bench
|
||||
- notebooks
|
||||
- `devin_eval_analysis.ipynb`: notebook analyzing devin's outputs
|
||||
- scripts
|
||||
- `prepare_devin_outputs_for_evaluation.py`: script fetching and converting [devin's output](https://github.com/CognitionAI/devin-swebench-results/tree/main) into the desired json file for evaluation.
|
||||
- usage: `python prepare_devin_outputs_for_evaluation.py <setting>` where setting can be `passed`, `failed` or `all`
|
||||
- resources
|
||||
- Devin related SWE-bench test subsets
|
||||
- [🤗 OpenDevin/SWE-bench-devin-passed](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-passed)
|
||||
- [🤗 OpenDevin/SWE-bench-devin-full-filtered](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-full-filtered)
|
||||
- Devin's outputs processed for evaluations is available on [Huggingface](https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output)
|
||||
- get predictions that passed the test: `wget https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output/raw/main/devin_swe_passed.json`
|
||||
- get all predictions `wget https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output/raw/main/devin_swe_outputs.json`
|
||||
### Result Visualization
|
||||
|
||||
See [`SWE-bench/README.md`](./SWE-bench/README.md) for more details on how to run SWE-Bench for evaluation.
|
||||
Check [this huggingface space](https://huggingface.co/spaces/OpenDevin/evaluation) for visualization of existing experimental results.
|
||||
|
||||
### Results
|
||||
|
||||
We have refined the original SWE-bench evaluation pipeline to enhance its efficiency and reliability. The updates are as follows:
|
||||
- Reuse testbeds and Conda environments.
|
||||
- Additionally try `patch` command for patch application if `git apply` command fails.
|
||||
### Upload your results
|
||||
|
||||
#### Results on SWE-bench-devin-passed
|
||||
|
||||
[🤗 OpenDevin/SWE-bench-devin-passed](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-passed)
|
||||
|
||||
| Model/Agent | #instances | #init | #apply | #resolve |
|
||||
|------------------------|------------|-------|--------|----------|
|
||||
| Gold | 79 | 79 | 79 | 79 |
|
||||
| Devin | 79 | 79 | 76 | 76 |
|
||||
|
||||
#init: number of instances where testbeds have been successfully initialized.
|
||||
|
||||
In the 3 Devin-failed instances (see below), Devin has made changes to the tests, which are incompatible with the provided test patch and causes failures during patch application. The evaluation adopted by Devin does not seem to align with the original SWE-bench evaluation.
|
||||
|
||||
```shell
|
||||
django__django-11244
|
||||
scikit-learn__scikit-learn-10870
|
||||
sphinx-doc__sphinx-9367
|
||||
```
|
||||
|
||||
#### Results on SWE-bench-devin-failed
|
||||
|
||||
| Model/Agent | #instances | #init | #apply | #resolve |
|
||||
|------------------------|------------|-------|--------|----------|
|
||||
| Gold | 491 | 491 | 491 | 371 |
|
||||
| Devin | 491 | 491 | 463 | 7 |
|
||||
|
||||
Devin **passes** 7 instances on the `SWE-bench-devin-failed` subset. SWE-bench dataset appears to be noisy, evidenced by 120 instances where gold patches do not pass.
|
||||
|
||||
We have filtered out the problematic 120 instances, resulting in the creation of the `SWE-bench-devin-full-filtered` subset.
|
||||
|
||||
## Results on SWE-bench-devin-full-filtered
|
||||
|
||||
[🤗 OpenDevin/SWE-bench-devin-full-filtered](https://huggingface.co/datasets/OpenDevin/SWE-bench-devin-full-filtered)
|
||||
|
||||
| Model/Agent | #instances | #init | #apply | #resolve |
|
||||
|------------------------|------------|-------|--------|----------|
|
||||
| Gold | 450 | 450 | 450 | 450 |
|
||||
| Devin | 450 | 450 | 426 | 83 |
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenDevin/evaluation) and submit a PR of your evaluation results to our hosted huggingface repo via PR following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# SWE-Bench Evaluation
|
||||
|
||||
Work in-progress.
|
||||
|
||||
**TODOs**:
|
||||
|
||||
- [ ] Generate `predictions` files given an OpenDevin `Agent` implementation. We could borrow something from [devin's eval-harness implementation](https://github.com/CognitionAI/devin-swebench-results/tree/main/harness), for example, [how to generate `TestSpec`](https://github.com/CognitionAI/devin-swebench-results/blob/main/harness/scripts.py#L150-L160).
|
||||
- [ ] Make sure the evaluation suite runs on all repos. I only tested on `matplotlib` so far, `scikit-learn` does not work for now (see [this issue](https://github.com/princeton-nlp/SWE-bench/issues/57))).
|
||||
|
||||
|
||||
## Run tests for a prediction file inside a docker container
|
||||
|
||||
Currently, the docker container should be able to for running SWE-Bench. It was tested on `matplotlib`, but it requires further testing to make sure it works on other repositories. Currently, [it does not work for `scikit-learn`](https://github.com/princeton-nlp/SWE-bench/issues/57)).
|
||||
|
||||
### Setup example data
|
||||
|
||||
```bash
|
||||
cd evaluation/SWE-bench
|
||||
./scripts/prepare_devin_swe_bench_data.sh
|
||||
|
||||
# Clone the repo
|
||||
# This is a fork that fixes some issues that stops matplotlib from running (see https://github.com/princeton-nlp/SWE-bench/pull/56)
|
||||
git clone https://github.com/OpenDevin/SWE-bench.git
|
||||
|
||||
# Enter the docker container
|
||||
./scripts/run_docker_interactive.sh
|
||||
```
|
||||
|
||||
### Run evaluation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
rm -rf data/logs/ data/testbeds/ # (Optional) remove previous outputs
|
||||
mkdir -p data/logs
|
||||
mkdir -p data/testbeds
|
||||
|
||||
python SWE-bench/harness/run_evaluation.py \
|
||||
--predictions_path data/predictions/devin_swe_outputs.json \
|
||||
--swe_bench_tasks data/processed/swe-bench-test.json \
|
||||
--log_dir data/logs \
|
||||
--testbed data/testbeds \
|
||||
--skip_existing \
|
||||
--timeout 900 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
You will see the command line outputs similar to this (if success):
|
||||
|
||||
```log
|
||||
swe-bench@2f3a6b9fcab2:/swe-bench$ ./harness/run_evaluation.sh
|
||||
/swe-bench/harness/run_evaluation.py:101: SyntaxWarning: assertion is always true, perhaps remove parentheses?
|
||||
assert(temp, datasets.arrow_dataset.Dataset)
|
||||
2024-03-20 09:21:18,796 - INFO - Found 1 predictions across 1 model(s) in predictions file
|
||||
2024-03-20 09:21:18,796 - INFO - [claude-2/matplotlib__matplotlib/3.6] # of predictions to evaluate: 1 (0 already evaluated)
|
||||
2024-03-20 09:21:18,797 - INFO - [Testbed] Creating log directory /swe-bench/data/logs/claude-2
|
||||
2024-03-20 09:21:18,797 - INFO - [Testbed] Using conda path /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmp09wrm708
|
||||
2024-03-20 09:21:18,797 - INFO - [Testbed] Using working directory /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmpfy1qth23 for testbed
|
||||
2024-03-20 09:21:18,797 - INFO - [Testbed] Repo matplotlib/matplotlib: 1 versions
|
||||
2024-03-20 09:21:18,797 - INFO - [Testbed] Version 3.6: 1 instances
|
||||
2024-03-20 09:21:18,797 - INFO - No conda path provided, creating temporary install in /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmp09wrm708/miniconda3...
|
||||
2024-03-20 09:21:27,482 - INFO - [Testbed] Using conda path /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmp09wrm708/miniconda3
|
||||
2024-03-20 09:21:27,942 - INFO - [Testbed] Setting up testbed for matplotlib__matplotlib__3.6
|
||||
2024-03-20 09:21:44,257 - INFO - [Testbed] Cloned matplotlib/matplotlib to /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmpfy1qth23/matplotlib__matplotlib__3.6
|
||||
2024-03-20 09:21:44,415 - INFO - [Testbed] Creating environment matplotlib__matplotlib__3.6; Command: /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmp09wrm708/miniconda3/bin/conda env create --file /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmpfy1qth23/environment.yml
|
||||
2024-03-20 09:23:39,781 - INFO - [Testbed] Installing pip packages for matplotlib__matplotlib__3.6; Command: . /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmp09wrm708/miniconda3/bin/activate matplotlib__matplotlib__3.6 && pip install pytest
|
||||
/swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmpfy1qth23/matplotlib__matplotlib__3.6: 1 instances
|
||||
2024-03-20 09:23:42,309 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Reset task environment to aca6e9d5e98811ca37c442217914b15e78127c89
|
||||
2024-03-20 09:23:42,314 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Apply patch successful (pred_try)
|
||||
2024-03-20 09:23:42,318 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Revert patch successful (pred_try)
|
||||
2024-03-20 09:23:42,318 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Installing with command: . /swe-bench/data/testbeds/claude-2/matplotlib__matplotlib/3.6/tmp09wrm708/miniconda3/bin/activate matplotlib__matplotlib__3.6 && echo 'activate successful' && python -m pip install -e .
|
||||
2024-03-20 09:24:54,966 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Installation successful
|
||||
2024-03-20 09:24:54,970 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Apply patch successful (test)
|
||||
2024-03-20 09:24:54,974 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Apply patch successful (pred)
|
||||
2024-03-20 09:25:04,775 - INFO - [matplotlib__matplotlib__3.6] [matplotlib__matplotlib-24362] Test script run successful
|
||||
swe-bench@2f3a6b9fcab2:/swe-bench$
|
||||
```
|
||||
|
||||
### Interpret Results
|
||||
|
||||
Then you may interpret the results under `data/logs`, and interpret it following [this guide](https://github.com/princeton-nlp/SWE-bench/blob/main/tutorials/evaluation.md#-metrics).
|
||||
@@ -1,155 +0,0 @@
|
||||
# @yaml
|
||||
# signature: search_dir <search_term> [<dir>]
|
||||
# docstring: searches for search_term in all files in dir. If dir is not provided, searches in the current directory
|
||||
# arguments:
|
||||
# search_term:
|
||||
# type: string
|
||||
# description: the term to search for
|
||||
# required: true
|
||||
# dir:
|
||||
# type: string
|
||||
# description: the directory to search in (if not provided, searches in the current directory)
|
||||
# required: false
|
||||
search_dir() {
|
||||
if [ $# -eq 1 ]; then
|
||||
local search_term="$1"
|
||||
local dir="./"
|
||||
elif [ $# -eq 2 ]; then
|
||||
local search_term="$1"
|
||||
if [ -d "$2" ]; then
|
||||
local dir="$2"
|
||||
else
|
||||
echo "Directory $2 not found"
|
||||
return
|
||||
fi
|
||||
else
|
||||
echo "Usage: search_dir <search_term> [<dir>]"
|
||||
return
|
||||
fi
|
||||
dir=$(realpath "$dir")
|
||||
local matches=$(find "$dir" -type f ! -path '*/.*' -exec grep -nIH "$search_term" {} + | cut -d: -f1 | sort | uniq -c)
|
||||
# if no matches, return
|
||||
if [ -z "$matches" ]; then
|
||||
echo "No matches found for \"$search_term\" in $dir"
|
||||
return
|
||||
fi
|
||||
# Calculate total number of matches
|
||||
local num_matches=$(echo "$matches" | awk '{sum+=$1} END {print sum}')
|
||||
# calculate total number of files matched
|
||||
local num_files=$(echo "$matches" | wc -l | awk '{$1=$1; print $0}')
|
||||
# if num_files is > 100, print an error
|
||||
if [ $num_files -gt 100 ]; then
|
||||
echo "More than $num_files files matched for \"$search_term\" in $dir. Please narrow your search."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Found $num_matches matches for \"$search_term\" in $dir:"
|
||||
echo "$matches" | awk '{$2=$2; gsub(/^\.+\/+/, "./", $2); print $2 " ("$1" matches)"}'
|
||||
echo "End of matches for \"$search_term\" in $dir"
|
||||
}
|
||||
|
||||
# @yaml
|
||||
# signature: search_file <search_term> [<file>]
|
||||
# docstring: searches for search_term in file. If file is not provided, searches in the current open file
|
||||
# arguments:
|
||||
# search_term:
|
||||
# type: string
|
||||
# description: the term to search for
|
||||
# required: true
|
||||
# file:
|
||||
# type: string
|
||||
# description: the file to search in (if not provided, searches in the current open file)
|
||||
# required: false
|
||||
search_file() {
|
||||
# Check if the first argument is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: search_file <search_term> [<file>]"
|
||||
return
|
||||
fi
|
||||
# Check if the second argument is provided
|
||||
if [ -n "$2" ]; then
|
||||
# Check if the provided argument is a valid file
|
||||
if [ -f "$2" ]; then
|
||||
local file="$2" # Set file if valid
|
||||
else
|
||||
echo "Usage: search_file <search_term> [<file>]"
|
||||
echo "Error: File name $2 not found. Please provide a valid file name."
|
||||
return # Exit if the file is not valid
|
||||
fi
|
||||
else
|
||||
# Check if a file is open
|
||||
if [ -z "$CURRENT_FILE" ]; then
|
||||
echo "No file open. Use the open command first."
|
||||
return # Exit if no file is open
|
||||
fi
|
||||
local file="$CURRENT_FILE" # Set file to the current open file
|
||||
fi
|
||||
local search_term="$1"
|
||||
file=$(realpath "$file")
|
||||
# Use grep to directly get the desired formatted output
|
||||
local matches=$(grep -nH "$search_term" "$file")
|
||||
# Check if no matches were found
|
||||
if [ -z "$matches" ]; then
|
||||
echo "No matches found for \"$search_term\" in $file"
|
||||
return
|
||||
fi
|
||||
# Calculate total number of matches
|
||||
local num_matches=$(echo "$matches" | wc -l | awk '{$1=$1; print $0}')
|
||||
|
||||
# calculate total number of lines matched
|
||||
local num_lines=$(echo "$matches" | cut -d: -f1 | sort | uniq | wc -l | awk '{$1=$1; print $0}')
|
||||
# if num_lines is > 100, print an error
|
||||
if [ $num_lines -gt 100 ]; then
|
||||
echo "More than $num_lines lines matched for \"$search_term\" in $file. Please narrow your search."
|
||||
return
|
||||
fi
|
||||
|
||||
# Print the total number of matches and the matches themselves
|
||||
echo "Found $num_matches matches for \"$search_term\" in $file:"
|
||||
echo "$matches" | cut -d: -f1-2 | sort -u -t: -k2,2n | while IFS=: read -r filename line_number; do
|
||||
echo "Line $line_number:$(sed -n "${line_number}p" "$file")"
|
||||
done
|
||||
echo "End of matches for \"$search_term\" in $file"
|
||||
}
|
||||
|
||||
# @yaml
|
||||
# signature: find_file <file_name> [<dir>]
|
||||
# docstring: finds all files with the given name in dir. If dir is not provided, searches in the current directory
|
||||
# arguments:
|
||||
# file_name:
|
||||
# type: string
|
||||
# description: the name of the file to search for
|
||||
# required: true
|
||||
# dir:
|
||||
# type: string
|
||||
# description: the directory to search in (if not provided, searches in the current directory)
|
||||
# required: false
|
||||
find_file() {
|
||||
if [ $# -eq 1 ]; then
|
||||
local file_name="$1"
|
||||
local dir="./"
|
||||
elif [ $# -eq 2 ]; then
|
||||
local file_name="$1"
|
||||
if [ -d "$2" ]; then
|
||||
local dir="$2"
|
||||
else
|
||||
echo "Directory $2 not found"
|
||||
return
|
||||
fi
|
||||
else
|
||||
echo "Usage: find_file <file_name> [<dir>]"
|
||||
return
|
||||
fi
|
||||
|
||||
dir=$(realpath "$dir")
|
||||
local matches=$(find "$dir" -type f -name "$file_name")
|
||||
# if no matches, return
|
||||
if [ -z "$matches" ]; then
|
||||
echo "No matches found for \"$file_name\" in $dir"
|
||||
return
|
||||
fi
|
||||
# Calculate total number of matches
|
||||
local num_matches=$(echo "$matches" | wc -l | awk '{$1=$1; print $0}')
|
||||
echo "Found $num_matches matches for \"$file_name\" in $dir:"
|
||||
echo "$matches" | awk '{print $0}'
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# FROM https://github.com/princeton-nlp/SWE-bench/blob/main/environment.yml
|
||||
name: swe-bench
|
||||
dependencies:
|
||||
- python=3.9
|
||||
- pip
|
||||
- pip:
|
||||
- beautifulsoup4
|
||||
- chardet
|
||||
- ghapi
|
||||
- GitPython
|
||||
- python-dotenv
|
||||
- requests
|
||||
- rich
|
||||
- transformers>=4.34.0
|
||||
- conda-forge::gh
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
|
||||
from datasets import load_dataset
|
||||
|
||||
dataset = load_dataset('princeton-nlp/SWE-bench')
|
||||
test = dataset['test'].to_pandas()
|
||||
test.to_json('data/processed/swe-bench-test.json', orient='records')
|
||||
@@ -1,81 +0,0 @@
|
||||
'''
|
||||
Script used to convert devin's output into the desired json format for evaluation on SWE-bench
|
||||
|
||||
Usage:
|
||||
python prepare_devin_outputs_for_evaluation.py <setting>
|
||||
<setting> can be "passed", "failed", "all"
|
||||
|
||||
Outputs:
|
||||
two json files under evaluation/SWE-bench/data/
|
||||
|
||||
'''
|
||||
|
||||
# fetch devin's outputs into a json file for evaluation
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def get_devin_eval_output(setting):
|
||||
repo_url = 'CognitionAI/devin-swebench-results'
|
||||
folder_path = 'output_diffs'
|
||||
|
||||
base_url = 'https://api.github.com/repos/'
|
||||
pass_api_url = f'{base_url}{repo_url}/contents/{folder_path}/pass'
|
||||
failed_api_url = f'{base_url}{repo_url}/contents/{folder_path}/fail'
|
||||
|
||||
pass_files_info = []
|
||||
failed_files_info = []
|
||||
|
||||
def get_files(api_url, subfolder_name, files_info):
|
||||
response = requests.get(api_url)
|
||||
if response.status_code == 200:
|
||||
contents = response.json()
|
||||
for item in tqdm(contents):
|
||||
if item['type'] == 'file':
|
||||
file_url = f"https://raw.githubusercontent.com/{repo_url}/main/{folder_path}/{subfolder_name}/{item['name']}"
|
||||
file_content = requests.get(file_url).text
|
||||
instance_id = item['name'][:-9]
|
||||
model_name = 'Devin' # Update with actual model name
|
||||
files_info.append({
|
||||
'instance_id': instance_id,
|
||||
'model_patch': file_content,
|
||||
'model_name_or_path': model_name,
|
||||
'pass_or_fail': subfolder_name
|
||||
})
|
||||
|
||||
if setting == 'passed' or setting == 'all':
|
||||
get_files(pass_api_url, 'pass', pass_files_info)
|
||||
if setting == 'failed' or setting == 'all':
|
||||
get_files(failed_api_url, 'fail', failed_files_info)
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
output_dir = os.path.join(script_dir, '../data/devin/')
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
if setting == 'passed' or setting == 'all':
|
||||
with open(os.path.join(output_dir, 'devin_swe_passed.json'), 'w') as pass_file:
|
||||
json.dump(pass_files_info, pass_file, indent=4)
|
||||
|
||||
if setting == 'failed' or setting == 'all':
|
||||
with open(os.path.join(output_dir, 'devin_swe_failed.json'), 'w') as fail_file:
|
||||
json.dump(failed_files_info, fail_file, indent=4)
|
||||
|
||||
if setting == 'all':
|
||||
merged_output = pass_files_info + failed_files_info
|
||||
with open(os.path.join(output_dir, 'devin_swe_outputs.json'), 'w') as merge_file:
|
||||
json.dump(merged_output, merge_file, indent=4)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print('Usage: python script_name.py <setting>')
|
||||
sys.exit(1)
|
||||
|
||||
setting = sys.argv[1]
|
||||
get_devin_eval_output(setting)
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xeo pipefail
|
||||
mkdir -p data/processed
|
||||
python3 scripts/download_test_data.py
|
||||
|
||||
# Download an example output file (FROM claude-2)
|
||||
# https://gist.github.com/sorendunn/9f1f1fade59f986b4925b6633f9ff165
|
||||
mkdir -p data/predictions
|
||||
wget https://huggingface.co/datasets/OpenDevin/Devin-SWE-bench-output/raw/main/devin_swe_outputs.json -O data/predictions/devin_swe_outputs.json
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
DOCKER_IMAGE=ghcr.io/opendevin/eval-swe-bench
|
||||
WORK_DIR=`pwd`
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
--user root \
|
||||
--ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \
|
||||
-v $WORK_DIR:/swe-bench \
|
||||
-w /swe-bench \
|
||||
$DOCKER_IMAGE \
|
||||
/bin/bash -c "usermod -u $(id -u) swe-bench && su swe-bench"
|
||||
@@ -0,0 +1,182 @@
|
||||
# Tutorial: How to add a New Evaluation Benchmark to OpenDevin
|
||||
|
||||
This tutorial provides a general guide on how to integrate your own evaluation benchmark into the OpenDevin framework.
|
||||
|
||||
You can read this for details, and also learn by example by looking at our existing evaluations:
|
||||
- [swe_bench](swe_bench/)
|
||||
|
||||
|
||||
## A quick walk-through of OpenDevin architecture
|
||||
|
||||
### Before everything begins
|
||||
|
||||
Please follow [this document](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to setup local develop environment for OpenDevin.
|
||||
|
||||
### Configuration file
|
||||
|
||||
OpenDevin uses `config.toml` to keep track of most configurations.
|
||||
|
||||
Here's an example configuration file you can use:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
max_iterations = 100
|
||||
cache_dir = "/tmp/cache"
|
||||
|
||||
# IMPORTANT: You should set these two paths to YOUR WORKSPACE directory,
|
||||
# which will be mounted into Sandbox for agent to interact with!
|
||||
# The OpenDevin agent will be able to read/write files whatever they like (even rm -rf)
|
||||
# in this directory, so be careful!!
|
||||
workspace_base = "/path/to/your/workspace"
|
||||
workspace_mount_path = "/path/to/your/workspace"
|
||||
# ==========================
|
||||
|
||||
sandbox_container_image = "ghcr.io/opendevin/sandbox:latest"
|
||||
sandbox_type = "ssh"
|
||||
sandbox_timeout = 120
|
||||
ssh_hostname = "localhost"
|
||||
|
||||
# SWEBench eval specific - but you can tweak it to your needs
|
||||
use_host_network = false
|
||||
run_as_devin = false
|
||||
# linting python after editing helps LLM fix indentations
|
||||
enable_auto_lint = true
|
||||
|
||||
[llm]
|
||||
# IMPORTANT: add your API key here, and set the model to the one you want to evaluate
|
||||
model = "gpt-4o-2024-05-13"
|
||||
api_key = "sk-XXX"
|
||||
```
|
||||
|
||||
### How to use OpenDevin programmatically
|
||||
|
||||
In this section, for the purpose of building an evaluation task, we don't use the standard OpenDevin web-based GUI, but rather run OpenDevin backend from CLI.
|
||||
|
||||
For example, you can run the following, which performs the specified task `-t`, with a particular model `-m` and agent `-c`, for a maximum number of iterations `-i`:
|
||||
|
||||
```bash
|
||||
poetry run python ./opendevin/core/main.py \
|
||||
-i 10 \
|
||||
-t "Write me a bash script that print hello world." \
|
||||
-c CodeActAgent \
|
||||
-m gpt-4o-2024-05-13
|
||||
```
|
||||
|
||||
After running the script, you will observe the following:
|
||||
|
||||

|
||||
|
||||
You can see the agent uses bash to write a script, makes it executable, and then tests it by running it to make sure it is working.
|
||||
|
||||
At the end of the above screenshot, OpenDevin actually requests user inputs when it think it finishes the task. This will cause issues in evaluation, since most evaluation don't assume additional user input. To fix this, we introduce the functionality of `fake_user_response_fn` in the `main` function, which we describe in the next section.
|
||||
|
||||
## The `main` function
|
||||
|
||||
The signature of `main` (in file [[`opendevin/core/main.py`](../opendevin/core/main.py)]) is as follows:
|
||||
|
||||
```python
|
||||
async def main(
|
||||
task_str: str = '',
|
||||
exit_on_message: bool = False,
|
||||
fake_user_response_fn: Optional[Callable[[Optional[State]], str]] = None,
|
||||
sandbox: Optional[Sandbox] = None,
|
||||
) -> Optional[State]:
|
||||
```
|
||||
|
||||
- `task_str`: The task instruction to run. In the above example, it is "Write me a bash script that print hello world."
|
||||
- `exit_on_message`: whether to quit if the agent asks for a message from user
|
||||
- `fake_user_response_fn`: An optional function that receives the current state (could be None) and returns a fake user response.
|
||||
- `sandbox`: An optional sandbox to run the agent in.
|
||||
|
||||
### `fake_user_response_fn`
|
||||
|
||||
Here's an example of `fake_user_response_fn` in the implementation for SWE-Bench in [`evaluation/swe_bench/run_infer.py`](swe_bench/run_infer.py):
|
||||
|
||||
```python
|
||||
def codeact_user_response(state: State) -> str:
|
||||
msg = (
|
||||
'Please continue working on the task on whatever approach you think is suitable.\n'
|
||||
'If you think you have modified the code in a way that fixes the issue, please run the following command: <execute_bash> exit </execute_bash>.\n'
|
||||
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP OR USE THE INTERNET TO SOLVE THIS TASK.\n'
|
||||
)
|
||||
if state.history:
|
||||
user_msgs = [
|
||||
action
|
||||
for action, _ in state.history
|
||||
if isinstance(action, MessageAction) and action.source == 'agent'
|
||||
]
|
||||
if len(user_msgs) >= 2:
|
||||
# let the agent know that it can give up when it has tried 3 times
|
||||
return (
|
||||
msg
|
||||
+ 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
|
||||
)
|
||||
return msg
|
||||
```
|
||||
|
||||
|
||||
### Return value
|
||||
|
||||
The main function returns a `State`, which is defined in [`opendevin/controller/state/state.py`](../opendevin/controller/state/state.py). We are mainly using `state.history` here, which is the most important field of data. You can imagine it is being a more structured version of OpenAI's chat completion [messages](https://platform.openai.com/docs/guides/text-generation/chat-completions-api).
|
||||
|
||||
`history: list[tuple[Action, Observation]] = field(default_factory=list)` is a list of (action, observation) tuple. All the actions are defined at [`opendevin/events/action`](../opendevin/events/action) and observations are defined at [`opendevin/events/observation`](../opendevin/events/action).
|
||||
|
||||
The agent can emit different actions like `CmdRunAction` (`opendevin/events/action/commands.py`) to execute bash commands and receive `CmdOutputObservation` (`opendevin/events/observation/commands.py`), `IPythonRunCellAction` to receive `IPythonRunCellObservation`, `BrowseInteractiveAction` (`opendevin/events/action/browse.py`) to browse the web and receive `BrowserOutputObservation` (`opendevin/events/observation/browse.py`).
|
||||
|
||||
The action we used in this example is `MessageAction` (`opendevin/events/action/message.py`), which actually denotes a message from either `agent` or `user`. In the [CodeAct agent example](https://github.com/OpenDevin/OpenDevin/blob/7ca560471bd262f22513f3863995d0a8e6121c07/agenthub/codeact_agent/codeact_agent.py#L239-L273), an agent is considered to emit a `MessageAction` when it does not trigger a `CmdRunAction`, `IPythonRunCellAction`, and/or `BrowseInteractiveAction`.
|
||||
|
||||
Typically, the agent returns `MessageAction` when it is confused about the task, and want to ask human for follow-up clarification, which is a good thing in real-world task, but not necessarily in evaluation. So in this example, we provide a dummy prompt to tell the agent "Please continue working on the task on whatever approach you think is suitable[...]".
|
||||
|
||||
If you see something like this, you can consider adding this to your evaluation pipeline as well.
|
||||
|
||||
### `sandbox`
|
||||
|
||||
Sandbox is a fully functioning docker container where the agent can perform all sorts of tasks, e.g., using bash, calling Python, install packages, and more. You can leave `sandbox` to `None` if you don't need to do anything special to pre-configure the `Sandbox`.
|
||||
|
||||
In SWE-Bench, we need to copy the proper repository directory to the workspace and activate the right python virtual environment before the agent can start performing the task, so we actually defined a custom [`SWEBenchSSHBox`](https://github.com/OpenDevin/OpenDevin/blob/7ca560471bd262f22513f3863995d0a8e6121c07/evaluation/swe_bench/swe_env_box.py#L12-L118) that inherit from the default sandbox [`SSHBox`](https://github.com/OpenDevin/OpenDevin/blob/7ca560471bd262f22513f3863995d0a8e6121c07/opendevin/runtime/docker/ssh_box.py#L188) and handles all these initial setup. If you need to configure the `sandbox` for your evaluation, check `SWEBenchSSHBox` for a reference of implementation.
|
||||
|
||||
## How to put together an evaluation script?
|
||||
|
||||
Now we know how to start running the agent end-to-end, and how `fake_user_response_fn` and `sandbox` work. We will walk through a piece of dummy code (simplified version of SWE-Bench's [`run_infer.py`](https://github.com/OpenDevin/OpenDevin/blob/main/evaluation/swe_bench/run_infer.py)) that outline the general workflow:
|
||||
|
||||
- Load the dataset and prepare the evaluation configuration.
|
||||
- Filter out any instances that have already been processed.
|
||||
- For each instance in the dataset:
|
||||
- Set up the sandbox environment.
|
||||
- Run the agent to generate a solution.
|
||||
- Apply the solution to the instance and execute the test command.
|
||||
- Collect the results and write them to the output file.
|
||||
- Perform cleanup after the evaluation is complete.
|
||||
|
||||
You can see the [swe_bench/run_infer.py](swe_bench/run_infer.py) file for an example.
|
||||
|
||||
When you fully understand the `run_infer.py`, you can be ready to actually starting the evaluation!
|
||||
|
||||
|
||||
## Run the evaluation!
|
||||
|
||||
You can write your `run_infer.sh` script mimicking SWE-Bench's [`run_infer.sh`](https://github.com/OpenDevin/OpenDevin/blob/main/evaluation/swe_bench/scripts/run_infer.sh).
|
||||
|
||||
|
||||
You can start the evaluation by running:
|
||||
|
||||
```bash
|
||||
./run_infer.sh eval_gpt_4o_2024_05_13
|
||||
```
|
||||
Where `eval_gpt_4o_2024_05_13` is the model config you defined on the config.toml.
|
||||
Like this:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
...
|
||||
|
||||
[llm]
|
||||
model="gpt-4-32k"
|
||||
...
|
||||
|
||||
[eval_gpt_4o_2024_05_13]
|
||||
model="gpt-4o-2024-05-13"
|
||||
api_key="sk-xxx"
|
||||
```
|
||||
|
||||
If `[eval_gpt_4o_2024_05_13]` is not present, it will default to using the model configured in `[llm]`.
|
||||
@@ -0,0 +1,45 @@
|
||||
# GAIA Evaluation
|
||||
|
||||
This folder contains evaluation harness for evaluating agents on the [GAIA benchmark](https://arxiv.org/abs/2311.12983).
|
||||
|
||||
## Configure OpenDevin and your LLM
|
||||
|
||||
Create a `config.toml` file if it does not exist at the root of the workspace. Please check [README.md](../../README.md) for how to set this up.
|
||||
|
||||
## Run the evaluation
|
||||
We are using the GAIA dataset hosted on [Hugging Face](https://huggingface.co/datasets/gaia-benchmark/GAIA).
|
||||
Please accept the terms and make sure to have logged in on your computer by `huggingface-cli login` before running the evaluation.
|
||||
|
||||
Following is the basic command to start the evaluation. Here we are evaluating on the validation set for the `2023_all` split. You can adjust `./evaluation/gaia/scripts/run_infer.sh` to change the subset you want to evaluate on.
|
||||
|
||||
```bash
|
||||
./evaluation/gaia/scripts/run_infer.sh [model_config] [agent] [eval_limit] [gaia_subset]
|
||||
# e.g., ./evaluation/gaia/scripts/run_infer.sh eval_gpt4_1106_preview CodeActAgent 300
|
||||
```
|
||||
|
||||
where `model_config` is mandatory, while `agent`, `eval_limit` and `gaia_subset` are optional.
|
||||
|
||||
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
|
||||
LLM settings, as defined in your `config.toml`, defaulting to `gpt-3.5-turbo`
|
||||
|
||||
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
|
||||
to `CodeActAgent`.
|
||||
|
||||
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances, defaulting to all instances.
|
||||
|
||||
- `gaia_subset`, GAIA benchmark has multiple subsets: `2023_level1`, `2023_level2`, `2023_level3`, `2023_all`, defaulting to `2023_level1`.
|
||||
|
||||
Let's say you'd like to run 10 instances using `eval_gpt4_1106_preview` and CodeActAgent,
|
||||
then your command would be:
|
||||
|
||||
```bash
|
||||
./evaluation/gaia/scripts/run_infer.sh eval_gpt4_1106_preview CodeActAgent 10
|
||||
```
|
||||
|
||||
## Get score
|
||||
|
||||
Then you can get stats by running the following command:
|
||||
```bash
|
||||
python ./evaluation/gaia/get_score.py \
|
||||
--file <path_to/output.json>
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Get agent's gaia score")
|
||||
parser.add_argument('--file', type=str, help="Path to the agent's output.jsonl")
|
||||
args = parser.parse_args()
|
||||
this_log = args.file
|
||||
outs = []
|
||||
with open(this_log, 'r') as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
outs.append(json.loads(line))
|
||||
print(f'Reading {this_log}')
|
||||
print(f'Metadata:\n {outs[0]["metadata"]}')
|
||||
|
||||
total = 0
|
||||
success = 0
|
||||
for out in outs:
|
||||
total += 1
|
||||
if out['test_result']['score']:
|
||||
success += 1
|
||||
print(f'Success rate: {success}/{total} = {success/total}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,358 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
import huggingface_hub
|
||||
from datasets import load_dataset
|
||||
from tqdm import tqdm
|
||||
|
||||
from evaluation.gaia.scorer import question_scorer
|
||||
from opendevin.controller.state.state import State
|
||||
from opendevin.core.config import config, get_llm_config_arg, get_parser
|
||||
from opendevin.core.logger import get_console_handler
|
||||
from opendevin.core.logger import opendevin_logger as logger
|
||||
from opendevin.core.main import main
|
||||
from opendevin.events.action import CmdRunAction, MessageAction
|
||||
from opendevin.events.serialization.event import event_to_dict
|
||||
|
||||
DATASET_CACHE_DIR = '~/.cache/open-devin/evals/gaia'
|
||||
DATASET_CACHE_DIR = os.path.expanduser(DATASET_CACHE_DIR)
|
||||
|
||||
|
||||
def cleanup():
|
||||
logger.info('Cleaning up child processes...')
|
||||
for process in mp.active_children():
|
||||
logger.info(f'Terminating child process: {process.name}')
|
||||
process.terminate()
|
||||
process.join()
|
||||
|
||||
|
||||
def codeact_user_response(state: State) -> str:
|
||||
msg = (
|
||||
'Please continue working on the task on whatever approach you think is suitable.\n'
|
||||
'If you think you have solved the task, please first send your answer to user through message and then <execute_bash> exit </execute_bash>.\n'
|
||||
'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
|
||||
'For example: The answer to the question is <solution> 42 </solution>.\n'
|
||||
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
|
||||
)
|
||||
if state.history:
|
||||
user_msgs = [
|
||||
action
|
||||
for action, _ in state.history
|
||||
if isinstance(action, MessageAction) and action.source == 'user'
|
||||
]
|
||||
if len(user_msgs) >= 2:
|
||||
# let the agent know that it can give up when it has tried 3 times
|
||||
return (
|
||||
msg
|
||||
+ 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
def monologue_user_response(state: State) -> str:
|
||||
raise NotImplementedError('MonologueAgent should never ask for user responses.')
|
||||
|
||||
|
||||
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
|
||||
'CodeActAgent': codeact_user_response,
|
||||
'MonologueAgent': monologue_user_response,
|
||||
}
|
||||
|
||||
AGENT_CLS_TO_INST_SUFFIX = {
|
||||
'CodeActAgent': 'When you think you have solved the question, please first send your answer to user through message and then exit.\n'
|
||||
}
|
||||
|
||||
|
||||
def process_instance(instance, agent_class, metadata, reset_logger: bool = True):
|
||||
# create process-specific workspace dir
|
||||
# we will create a workspace directory for EACH process
|
||||
# so that different agent don't interfere with each other.
|
||||
old_workspace_mount_path = config.workspace_mount_path
|
||||
workspace_mount_path = os.path.join(config.workspace_mount_path, '_eval_workspace')
|
||||
workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
|
||||
pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
|
||||
config.workspace_mount_path = workspace_mount_path
|
||||
|
||||
# Setup the logger properly, so you can run multi-processing to parallize the evaluation
|
||||
eval_output_dir = metadata['eval_output_dir']
|
||||
if reset_logger:
|
||||
# Set up logger
|
||||
log_file = os.path.join(
|
||||
eval_output_dir, 'logs', f'instance_{instance["task_id"]}.log'
|
||||
)
|
||||
# Remove all existing handlers from logger
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
# add back the console handler to print ONE line
|
||||
logger.addHandler(get_console_handler())
|
||||
logger.info(
|
||||
f'Starting evaluation for instance {instance["task_id"]}.\nLOG: tail -f {log_file}'
|
||||
)
|
||||
# Remove all existing handlers from logger
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
logger.info(f'Process-specific workspace mounted at {workspace_mount_path}')
|
||||
if instance['file_name'] != '':
|
||||
# if this question comes with a file, we need to save it to the workspace
|
||||
src_file = os.path.join(
|
||||
DATASET_CACHE_DIR, '2023', metadata['data_split'], instance['file_name']
|
||||
)
|
||||
extension_name = instance['file_name'].split('.')[-1]
|
||||
dest_file = os.path.join(workspace_mount_path, f'file.{extension_name}')
|
||||
shutil.copyfile(src_file, dest_file)
|
||||
logger.info(f'File copied to {dest_file}')
|
||||
else:
|
||||
dest_file = None
|
||||
|
||||
# Prepare instruction
|
||||
instruction = f"{instance['Question']}\n"
|
||||
logger.info(f'Instruction: {instruction}')
|
||||
if dest_file:
|
||||
instruction += f"\n\nThe mentioned file is provided in the workspace at: {dest_file.split('/')[-1]}"
|
||||
|
||||
instruction += 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
|
||||
instruction += 'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
|
||||
instruction += (
|
||||
'For example: The answer to the question is <solution> 42 </solution>.\n'
|
||||
)
|
||||
# NOTE: You can actually set slightly different instruction for different agents
|
||||
instruction += AGENT_CLS_TO_INST_SUFFIX.get(agent_class, '')
|
||||
logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
# Here's how you can run the agent (similar to the `main` function) and get the final task state
|
||||
state: State = asyncio.run(
|
||||
main(
|
||||
instruction,
|
||||
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(agent_class),
|
||||
)
|
||||
)
|
||||
# ======= Attempt to evaluate the agent's edits =======
|
||||
# If you are working on simplier benchmark that only evaluates the final model output (e.g., in a MessageAction)
|
||||
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
|
||||
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
model_answer_raw = ''
|
||||
for act, _ in reversed(state.history):
|
||||
if isinstance(act, CmdRunAction) and act.source == 'agent':
|
||||
model_answer_raw = act.thought
|
||||
break
|
||||
elif isinstance(act, MessageAction) and act.source == 'agent':
|
||||
model_answer_raw = act.content
|
||||
break
|
||||
|
||||
# attempt to parse model_answer
|
||||
model_answer = re.findall(r'<solution>(.*?)</solution>', model_answer_raw)
|
||||
if len(model_answer) == 0:
|
||||
logger.warning(f'Failed to parse model answer: {model_answer_raw}')
|
||||
model_answer = model_answer_raw
|
||||
else:
|
||||
model_answer = model_answer[0]
|
||||
|
||||
logger.info(
|
||||
f'Final message: {model_answer} | Ground truth: {instance["Final answer"]}'
|
||||
)
|
||||
score = question_scorer(
|
||||
model_answer=model_answer, ground_truth=instance['Final answer']
|
||||
)
|
||||
test_result = {
|
||||
'score': score,
|
||||
'model_answer_raw': model_answer_raw,
|
||||
'model_answer': model_answer,
|
||||
'ground_truth': instance['Final answer'],
|
||||
}
|
||||
|
||||
# Save the output
|
||||
output = {
|
||||
'instance_id': instance['task_id'],
|
||||
'instance': instance,
|
||||
'instruction': instance['Question'],
|
||||
'metadata': metadata,
|
||||
'history': [
|
||||
(event_to_dict(action), event_to_dict(obs)) for action, obs in state.history
|
||||
],
|
||||
'error': state.error if state and state.error else None,
|
||||
'test_result': test_result,
|
||||
}
|
||||
|
||||
# Close the sandbox
|
||||
config.workspace_mount_path = old_workspace_mount_path
|
||||
return output
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_parser()
|
||||
parser.add_argument(
|
||||
'--level',
|
||||
type=str,
|
||||
help='gaia level to evaluate, eg. 2023_level1',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--data-split',
|
||||
type=str,
|
||||
help='data split to evaluate, eg. validation',
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
if args.directory:
|
||||
config.workspace_base = os.path.abspath(args.directory)
|
||||
logger.info(f'Setting workspace base to {config.workspace_base}')
|
||||
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
|
||||
# so we don't need to manage file uploading to OpenDevin's repo
|
||||
level = args.level
|
||||
data_split = args.data_split
|
||||
dataset = load_dataset('gaia-benchmark/GAIA', level)
|
||||
huggingface_hub.snapshot_download(
|
||||
'gaia-benchmark/GAIA',
|
||||
repo_type='dataset',
|
||||
local_dir=DATASET_CACHE_DIR,
|
||||
)
|
||||
gaia_tests = dataset[data_split]
|
||||
logger.info(f'Evaluating GAIA-Benchmark {level} {data_split} split')
|
||||
|
||||
# Check https://github.com/OpenDevin/OpenDevin/blob/main/evaluation/swe_bench/README.md#configure-opendevin-and-your-llm
|
||||
# for details of how to set `llm_config`
|
||||
if args.llm_config:
|
||||
specified_llm_config = get_llm_config_arg(args.llm_config)
|
||||
if specified_llm_config:
|
||||
config.llm = specified_llm_config
|
||||
logger.info(f'Config for evaluation: {config}')
|
||||
|
||||
# TEST METADATA
|
||||
agent_class = args.agent_cls
|
||||
assert (
|
||||
agent_class in AGENT_CLS_TO_FAKE_USER_RESPONSE_FN
|
||||
), f'Unsupported agent class: {agent_class}'
|
||||
model_name = config.llm.model.split('/')[-1]
|
||||
max_iterations = args.max_iterations
|
||||
eval_note = ''
|
||||
if args.eval_note is not None:
|
||||
eval_note += '_N_' + args.eval_note
|
||||
eval_output_dir = os.path.join(
|
||||
args.eval_output_dir,
|
||||
'gaia',
|
||||
agent_class,
|
||||
model_name + '_maxiter_' + str(max_iterations) + eval_note,
|
||||
)
|
||||
|
||||
pathlib.Path(eval_output_dir).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(eval_output_dir, 'logs')).mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
logger.info(f'Using evaluation output directory: {eval_output_dir}')
|
||||
|
||||
metadata = {
|
||||
'gaia-level': level,
|
||||
'data_split': data_split,
|
||||
'agent_class': agent_class,
|
||||
'model_name': model_name,
|
||||
'max_iterations': max_iterations,
|
||||
'eval_output_dir': eval_output_dir,
|
||||
'start_time': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
# get the commit id of current repo for reproduciblity
|
||||
'git_commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'])
|
||||
.decode('utf-8')
|
||||
.strip(),
|
||||
}
|
||||
logger.info(f'Metadata: {metadata}')
|
||||
with open(os.path.join(eval_output_dir, 'metadata.json'), 'w') as f:
|
||||
json.dump(metadata, f)
|
||||
|
||||
# LIMIT EVALUATION
|
||||
eval_n_limit = args.eval_n_limit
|
||||
if eval_n_limit:
|
||||
gaia_tests = gaia_tests.select(list(range(eval_n_limit)))
|
||||
logger.info(f'Limiting evaluation to first {eval_n_limit} instances.')
|
||||
|
||||
# OUTPUT FILE
|
||||
output_file = os.path.join(eval_output_dir, 'output.jsonl')
|
||||
logger.info(f'Writing evaluation output to {output_file}')
|
||||
finished_task_ids = set()
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
for line in f:
|
||||
data = json.loads(line)
|
||||
finished_task_ids.add(data['instance_id'])
|
||||
logger.warning(
|
||||
f'Output file {output_file} already exists. Loaded {len(finished_task_ids)} finished instances.'
|
||||
)
|
||||
output_fp = open(output_file, 'a')
|
||||
|
||||
logger.info(
|
||||
f'Evaluation started with Agent {agent_class}, model {model_name}, max iterations {max_iterations}.'
|
||||
)
|
||||
|
||||
# =============================================
|
||||
# filter out finished instances
|
||||
new_gaia_tests = []
|
||||
for instance in gaia_tests:
|
||||
if instance['task_id'] in finished_task_ids:
|
||||
logger.info(
|
||||
f'Skipping instance {instance["task_id"]} as it is already finished.'
|
||||
)
|
||||
continue
|
||||
new_gaia_tests.append(instance)
|
||||
|
||||
gaia_tests = new_gaia_tests
|
||||
logger.info(
|
||||
f'Finished instances: {len(finished_task_ids)}, Remaining instances: {len(gaia_tests)}'
|
||||
)
|
||||
# =============================================
|
||||
|
||||
pbar = tqdm(total=len(gaia_tests))
|
||||
|
||||
# This function tracks the progress AND write the output to a JSONL file
|
||||
def update_progress(future):
|
||||
pbar.update(1)
|
||||
output = future.result()
|
||||
pbar.set_description(f'Instance {output["instance_id"]}')
|
||||
pbar.set_postfix_str(f'Test Result: {output["test_result"]["score"]}')
|
||||
logger.info(
|
||||
f'Finished evaluation for instance {output["instance_id"]}: {output["test_result"]}'
|
||||
)
|
||||
output_fp.write(json.dumps(output) + '\n')
|
||||
output_fp.flush()
|
||||
|
||||
# This sets the multi-processing
|
||||
num_workers = args.eval_num_workers
|
||||
logger.info(f'Using {num_workers} workers for evaluation.')
|
||||
|
||||
try:
|
||||
with ProcessPoolExecutor(num_workers) as executor:
|
||||
futures = []
|
||||
# This is how we perform multi-processing
|
||||
for instance in gaia_tests:
|
||||
future = executor.submit(
|
||||
process_instance,
|
||||
instance,
|
||||
agent_class,
|
||||
metadata,
|
||||
reset_logger=bool(num_workers > 1),
|
||||
)
|
||||
future.add_done_callback(update_progress)
|
||||
futures.append(future)
|
||||
|
||||
# Wait for all futures to complete
|
||||
for future in futures:
|
||||
future.result()
|
||||
except KeyboardInterrupt:
|
||||
logger.info('KeyboardInterrupt received. Cleaning up...')
|
||||
cleanup()
|
||||
|
||||
output_fp.close()
|
||||
logger.info('Evaluation finished.')
|
||||
@@ -0,0 +1,98 @@
|
||||
import re
|
||||
import string
|
||||
import warnings
|
||||
|
||||
|
||||
def normalize_number_str(number_str: str) -> float:
|
||||
# we replace these common units and commas to allow
|
||||
# conversion to float
|
||||
for char in ['$', '%', ',']:
|
||||
number_str = number_str.replace(char, '')
|
||||
try:
|
||||
return float(number_str)
|
||||
except ValueError:
|
||||
print(f'String {number_str} cannot be normalized to number str.')
|
||||
return float('inf')
|
||||
|
||||
|
||||
def split_string(
|
||||
s: str,
|
||||
char_list: list[str] = [',', ';'],
|
||||
) -> list[str]:
|
||||
pattern = f"[{''.join(char_list)}]"
|
||||
return re.split(pattern, s)
|
||||
|
||||
|
||||
def question_scorer(
|
||||
model_answer: str,
|
||||
ground_truth: str,
|
||||
) -> bool:
|
||||
def is_float(element: any) -> bool:
|
||||
try:
|
||||
float(element)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# if gt is a number
|
||||
if is_float(ground_truth):
|
||||
print(f'Evaluating {model_answer} as a number.')
|
||||
normalized_answer = normalize_number_str(model_answer)
|
||||
return normalized_answer == float(ground_truth)
|
||||
|
||||
# if gt is a list
|
||||
elif any(char in ground_truth for char in [',', ';']):
|
||||
print(f'Evaluating {model_answer} as a comma separated list.')
|
||||
# question with the fish: normalization removes punct
|
||||
|
||||
gt_elems = split_string(ground_truth)
|
||||
ma_elems = split_string(model_answer)
|
||||
|
||||
# check length is the same
|
||||
if len(gt_elems) != len(ma_elems):
|
||||
warnings.warn(
|
||||
'Answer lists have different lengths, returning False.', UserWarning
|
||||
)
|
||||
return False
|
||||
|
||||
# compare each element as float or str
|
||||
comparisons = []
|
||||
for ma_elem, gt_elem in zip(ma_elems, gt_elems):
|
||||
if is_float(gt_elem):
|
||||
normalized_ma_elem = normalize_number_str(ma_elem)
|
||||
comparisons.append(normalized_ma_elem == float(gt_elem))
|
||||
else:
|
||||
# we do not remove punct since comparisons can include punct
|
||||
comparisons.append(
|
||||
normalize_str(ma_elem, remove_punct=False)
|
||||
== normalize_str(gt_elem, remove_punct=False)
|
||||
)
|
||||
return all(comparisons)
|
||||
|
||||
# if gt is a str
|
||||
else:
|
||||
print(f'Evaluating {model_answer} as a string.')
|
||||
return normalize_str(model_answer) == normalize_str(ground_truth)
|
||||
|
||||
|
||||
def normalize_str(input_str, remove_punct=True) -> str:
|
||||
"""
|
||||
Normalize a string by:
|
||||
- Removing all white spaces
|
||||
- Optionally removing punctuation (if remove_punct is True)
|
||||
- Converting to lowercase
|
||||
Parameters:
|
||||
- input_str: str, the string to normalize
|
||||
- remove_punct: bool, whether to remove punctuation (default: True)
|
||||
Returns:
|
||||
- str, the normalized string
|
||||
"""
|
||||
# Remove all white spaces. Required e.g for seagull vs. sea gull
|
||||
no_spaces = re.sub(r'\s', '', input_str)
|
||||
|
||||
# Remove punctuation, if specified.
|
||||
if remove_punct:
|
||||
translator = str.maketrans('', '', string.punctuation)
|
||||
return no_spaces.lower().translate(translator)
|
||||
else:
|
||||
return no_spaces.lower()
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
MODEL_CONFIG=$1
|
||||
AGENT=$2
|
||||
EVAL_LIMIT=$3
|
||||
LEVELS=$4
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
echo "Agent not specified, use default CodeActAgent"
|
||||
AGENT="CodeActAgent"
|
||||
fi
|
||||
|
||||
if [ -z "$LEVELS" ]; then
|
||||
LEVELS="2023_level1"
|
||||
echo "Levels not specified, use default $LEVELS"
|
||||
fi
|
||||
|
||||
# IMPORTANT: Because Agent's prompt changes fairly often in the rapidly evolving codebase of OpenDevin
|
||||
# We need to track the version of Agent in the evaluation to make sure results are comparable
|
||||
AGENT_VERSION=v$(poetry run python -c "import agenthub; from opendevin.controller.agent import Agent; print(Agent.get_cls('$AGENT').VERSION)")
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "AGENT_VERSION: $AGENT_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "LEVELS: $LEVELS"
|
||||
|
||||
COMMAND="poetry run python ./evaluation/gaia/run_infer.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--max-iterations 30 \
|
||||
--level $LEVELS \
|
||||
--data-split validation \
|
||||
--max-chars 10000000 \
|
||||
--eval-num-workers 1 \
|
||||
--eval-note ${AGENT_VERSION}_${LEVELS}"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
@@ -0,0 +1,210 @@
|
||||
# HumanEvalFix Evaluation with OpenDevin
|
||||
|
||||
Implements evaluation of agents on HumanEvalFix from the HumanEvalPack benchmark introduced in [OctoPack: Instruction Tuning Code Large Language Models](https://arxiv.org/abs/2308.07124). Please see [here](https://github.com/bigcode-project/bigcode-evaluation-harness/blob/main/bigcode_eval/tasks/humanevalpack.py) for the reference implementation used in the paper.
|
||||
|
||||
## Setup Environment
|
||||
|
||||
Please follow [this document](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to setup local develop environment for OpenDevin.
|
||||
|
||||
|
||||
## Configure OpenDevin and your LLM
|
||||
|
||||
Create a `config.toml` file if it does not exist at the root of the workspace.
|
||||
|
||||
Add the following configurations:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
max_iterations = 100
|
||||
cache_dir = "/tmp/cache"
|
||||
ssh_hostname = "localhost"
|
||||
enable_auto_lint = true
|
||||
|
||||
# TODO: Change these to the model you want to evaluate
|
||||
[eval_gpt4_1106_preview]
|
||||
model = "gpt-4-1106-preview"
|
||||
api_key = "XXX"
|
||||
temperature = 0.0
|
||||
|
||||
[eval_some_openai_compatible_model]
|
||||
model = "openai/MODEL_NAME"
|
||||
base_url = "https://OPENAI_COMPATIBLE_URL/v1"
|
||||
api_key = "XXX"
|
||||
temperature = 0.0
|
||||
```
|
||||
|
||||
## Run Inference on HumanEvalFix
|
||||
|
||||
```bash
|
||||
./evaluation/humanevalfix/scripts/run_infer.sh eval_gpt4_1106_preview
|
||||
```
|
||||
|
||||
You can replace `eval_gpt4_1106_preview` with any model you set up in `config.toml`.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
For each problem, OpenDevin is given a set number of iterations to fix the failing code. The history field shows each iteration's response to correct its code that fails any test case.
|
||||
|
||||
|
||||
```
|
||||
{
|
||||
"task_id": "Python/2",
|
||||
"instruction": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
|
||||
"metadata": {
|
||||
"agent_class": "CodeActAgent",
|
||||
"model_name": "gpt-4",
|
||||
"max_iterations": 10,
|
||||
"eval_output_dir": "evaluation/evaluation_outputs/outputs/humanevalfix/CodeActAgent/gpt-4_maxiter_10_N_v1.4",
|
||||
"start_time": "2024-05-22 20:54:15",
|
||||
"git_commit": "4d3253696f5a9d9de02ab86969fe9796fa40331f"
|
||||
},
|
||||
"history": [
|
||||
[
|
||||
{
|
||||
"id": 27,
|
||||
"timestamp": "2024-05-22T20:57:24.688651",
|
||||
"source": "user",
|
||||
"message": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
|
||||
"action": "message",
|
||||
"args": {
|
||||
"content": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
|
||||
"wait_for_response": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"message": "No observation",
|
||||
"observation": "null",
|
||||
"content": "",
|
||||
"extras": {}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"id": 32,
|
||||
"timestamp": "2024-05-22T20:57:28.413818",
|
||||
"source": "agent",
|
||||
"message": "Running command: open Python__2.py",
|
||||
"action": "run",
|
||||
"args": {
|
||||
"command": "open Python__2.py",
|
||||
"background": false,
|
||||
"thought": "Let's start by opening the Python__2.py file to see the current implementation of the function."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"timestamp": "2024-05-22T20:57:28.803780",
|
||||
"source": "agent",
|
||||
"message": "Command `open Python__2.py` executed with exit code 0.",
|
||||
"cause": 32,
|
||||
"observation": "run",
|
||||
"content": "[File: /workspace/Python__2.py (14 lines total)]\r\n1:def truncate_number(number: float) -> float:\r\n2: return number % 1.0 + 1.0\r\n3:\r\n4:\r\n5:\r\n6:\r\n7:\r\n8:\r\n9:def check(truncate_number):\r\n10: assert truncate_number(3.5) == 0.5\r\n11: assert abs(truncate_number(1.33) - 0.33) < 1e-6\r\n12: assert abs(truncate_number(123.456) - 0.456) < 1e-6\r\n13:\r\n14:check(truncate_number)",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "open Python__2.py",
|
||||
"exit_code": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"id": 34,
|
||||
"timestamp": "2024-05-22T20:57:36.605638",
|
||||
"source": "agent",
|
||||
"message": "Running command: edit 2:2 <<EOF\n return number % 1.0\nEOF",
|
||||
"action": "run",
|
||||
"args": {
|
||||
"command": "edit 2:2 <<EOF\n return number % 1.0\nEOF",
|
||||
"background": false,
|
||||
"thought": "The current implementation of the function `truncate_number` is incorrect because it adds 1.0 to the fractional part of the number. This is not necessary and is causing the function to fail the test cases. \n\nThe correct implementation should only return the fractional part of the number. This can be achieved by using the modulus operator with 1.0, which will return the fractional part of the number.\n\nLet's correct the function implementation."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"timestamp": "2024-05-22T20:57:36.973592",
|
||||
"source": "agent",
|
||||
"message": "Command `edit 2:2 <<EOF\n return number % 1.0\nEOF` executed with exit code 0.",
|
||||
"cause": 34,
|
||||
"observation": "run",
|
||||
"content": "> > [File: /workspace/Python__2.py (14 lines total)]\r\n1:def truncate_number(number: float) -> float:\r\n2: return number % 1.0\r\n3:\r\n4:\r\n5:\r\n6:\r\n7:\r\n8:\r\n9:def check(truncate_number):\r\n10: assert truncate_number(3.5) == 0.5\r\n11: assert abs(truncate_number(1.33) - 0.33) < 1e-6\r\n12: assert abs(truncate_number(123.456) - 0.456) < 1e-6\r\n13:\r\n14:check(truncate_number)\r\nFile updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "edit 2:2 <<EOF\n return number % 1.0\nEOF",
|
||||
"exit_code": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"id": 36,
|
||||
"timestamp": "2024-05-22T20:57:48.420415",
|
||||
"source": "agent",
|
||||
"message": "Running command: python3 Python__2.py",
|
||||
"action": "run",
|
||||
"args": {
|
||||
"command": "python3 Python__2.py",
|
||||
"background": false,
|
||||
"thought": "The function `truncate_number` has been updated. Now, let's run the Python file to see if the test cases pass."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"timestamp": "2024-05-22T20:57:48.635312",
|
||||
"source": "agent",
|
||||
"message": "Command `python3 Python__2.py` executed with exit code 0.",
|
||||
"cause": 36,
|
||||
"observation": "run",
|
||||
"content": "",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "python3 Python__2.py",
|
||||
"exit_code": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"id": 38,
|
||||
"timestamp": "2024-05-22T20:58:10.589252",
|
||||
"source": "agent",
|
||||
"message": "All done! What's next on the agenda?",
|
||||
"action": "finish",
|
||||
"args": {
|
||||
"outputs": {},
|
||||
"thought": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"message": "No observation",
|
||||
"observation": "null",
|
||||
"content": "",
|
||||
"extras": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"error": null,
|
||||
"test_result": {
|
||||
"result": {
|
||||
"pass@1": 1.0
|
||||
},
|
||||
"metadata": {
|
||||
"logs": {
|
||||
"0": [
|
||||
[
|
||||
0,
|
||||
{
|
||||
"task_id": 0,
|
||||
"passed": true,
|
||||
"result": "passed",
|
||||
"completion_id": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"timeout": 10,
|
||||
"num_workers": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user