diff --git a/.github/workflows/clean-up.yml b/.github/workflows/clean-up.yml
new file mode 100644
index 0000000000..a00dad7282
--- /dev/null
+++ b/.github/workflows/clean-up.yml
@@ -0,0 +1,68 @@
+# Workflow that cleans up outdated and old workflows to prevent out of disk issues
+name: Delete old workflow runs
+
+on:
+ workflow_dispatch:
+ inputs:
+ days:
+ description: 'Days-worth of runs to keep for each workflow'
+ required: true
+ default: '30'
+ minimum_runs:
+ description: 'Minimum runs to keep for each workflow'
+ required: true
+ default: '10'
+ delete_workflow_pattern:
+ description: 'Name or filename of the workflow (if not set, all workflows are targeted)'
+ required: false
+ delete_workflow_by_state_pattern:
+ description: 'Filter workflows by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually'
+ required: true
+ default: "ALL"
+ type: choice
+ options:
+ - "ALL"
+ - active
+ - deleted
+ - disabled_inactivity
+ - disabled_manually
+ delete_run_by_conclusion_pattern:
+ description: 'Remove runs based on conclusion: action_required, cancelled, failure, skipped, success'
+ required: true
+ default: 'ALL'
+ type: choice
+ options:
+ - 'ALL'
+ - 'Unsuccessful: action_required,cancelled,failure,skipped'
+ - action_required
+ - cancelled
+ - failure
+ - skipped
+ - success
+ dry_run:
+ description: 'Logs simulated changes, no deletions are performed'
+ required: false
+
+jobs:
+ del_runs:
+ runs-on: ubuntu-latest
+ permissions:
+ actions: write
+ contents: read
+ steps:
+ - name: Delete workflow runs
+ uses: Mattraks/delete-workflow-runs@v2
+ with:
+ token: ${{ github.token }}
+ repository: ${{ github.repository }}
+ retain_days: ${{ github.event.inputs.days }}
+ keep_minimum_runs: ${{ github.event.inputs.minimum_runs }}
+ delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }}
+ delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }}
+ delete_run_by_conclusion_pattern: >-
+ ${{
+ startsWith(github.event.inputs.delete_run_by_conclusion_pattern, 'Unsuccessful:')
+ && 'action_required,cancelled,failure,skipped'
+ || github.event.inputs.delete_run_by_conclusion_pattern
+ }}
+ dry_run: ${{ github.event.inputs.dry_run }}
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 939e72bcb1..b4ed558a0c 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -1,3 +1,4 @@
+# Workflow that builds and deploys the documentation website
name: Deploy Docs to GitHub Pages
on:
@@ -5,10 +6,13 @@ on:
branches:
- main
pull_request:
+ paths:
+ - 'docs/**'
branches:
- main
jobs:
+ # Build the documentation website
build:
name: Build Docusaurus
runs-on: ubuntu-latest
@@ -25,23 +29,23 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
-
+ python-version: '3.11'
- name: Generate Python Docs
run: rm -rf docs/modules/python && pip install pydoc-markdown && pydoc-markdown
- name: Install dependencies
run: cd docs && npm ci
- name: Build website
run: cd docs && npm run build
-
- name: Upload Build Artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: docs/build
+ # Deploy the documentation website
deploy:
name: Deploy to GitHub Pages
+ runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.repository == 'OpenDevin/OpenDevin'
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
@@ -52,7 +56,6 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
diff --git a/.github/workflows/dummy-agent-test.yml b/.github/workflows/dummy-agent-test.yml
index b13eaeb184..6cf4d5900c 100644
--- a/.github/workflows/dummy-agent-test.yml
+++ b/.github/workflows/dummy-agent-test.yml
@@ -1,3 +1,4 @@
+# Workflow that uses the DummyAgent to run a simple task
name: Run E2E test with dummy agent
concurrency:
@@ -10,9 +11,6 @@ on:
- main
pull_request:
-env:
- PERSIST_SANDBOX : "false"
-
jobs:
test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml
index c5379e0c6b..770c181a7f 100644
--- a/.github/workflows/ghcr.yml
+++ b/.github/workflows/ghcr.yml
@@ -1,3 +1,4 @@
+# Workflow that builds, tests and then pushes the docker images to the ghcr.io repository
name: Build Publish and Test Runtime Image
concurrency:
@@ -19,25 +20,21 @@ on:
default: ''
jobs:
+ # Builds the OpenDevin Docker images
ghcr_build:
runs-on: ubuntu-latest
-
outputs:
tags: ${{ steps.capture-tags.outputs.tags }}
-
permissions:
contents: read
packages: write
-
strategy:
matrix:
- image: ["sandbox", "opendevin"]
- platform: ["amd64", "arm64"]
-
+ image: ['opendevin']
+ platform: ['amd64', 'arm64']
steps:
- name: Checkout
uses: actions/checkout@v4
-
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
@@ -52,51 +49,43 @@ jobs:
large-packages: true
docker-images: false
swap-storage: true
-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
-
- name: Build and export image
id: build
run: ./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} ${{ matrix.platform }}
-
- name: Capture tags
id: capture-tags
run: |
tags=$(cat tags.txt)
echo "tags=$tags"
echo "tags=$tags" >> $GITHUB_OUTPUT
-
- name: Upload Docker image as artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
path: /tmp/${{ matrix.image }}_image_${{ matrix.platform }}.tar
+ retention-days: 14
+ # Builds the runtime Docker images
ghcr_build_runtime:
runs-on: ubuntu-latest
-
outputs:
tags: ${{ steps.capture-tags.outputs.tags }}
-
permissions:
contents: read
packages: write
-
strategy:
matrix:
- image: ["od_runtime"]
- base_image: ["ubuntu:22.04"]
- platform: ["amd64", "arm64"]
-
+ image: ['od_runtime']
+ base_image: ['ubuntu:22.04']
+ platform: ['amd64', 'arm64']
steps:
- name: Checkout
uses: actions/checkout@v4
-
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
@@ -111,67 +100,65 @@ jobs:
large-packages: true
docker-images: false
swap-storage: true
-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
-
- name: Install poetry via pipx
run: pipx install poetry
-
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
- cache: "poetry"
-
+ python-version: '3.11'
+ cache: 'poetry'
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
-
- name: Create source distribution and Dockerfile
run: poetry run python3 opendevin/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime
-
- name: Build and export image
id: build
- run: ./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} ${{ matrix.platform }}
-
+ run: |
+ if [ -f 'containers/runtime/Dockerfile' ]; then
+ echo 'Dockerfile detected, building runtime image...'
+ ./containers/build.sh ${{ matrix.image }} ${{ github.repository_owner }} ${{ matrix.platform }}
+ else
+ echo 'No Dockerfile detected which means an exact image is already built. Pulling the image and saving it to a tar file...'
+ source containers/runtime/config.sh
+ echo "$DOCKER_IMAGE_TAG $DOCKER_IMAGE_HASH_TAG" >> tags.txt
+ echo "Pulling image $DOCKER_IMAGE/$DOCKER_IMAGE_HASH_TAG to /tmp/${{ matrix.image }}_image_${{ matrix.platform }}.tar"
+ docker pull $DOCKER_IMAGE:$DOCKER_IMAGE_HASH_TAG
+ docker save $DOCKER_IMAGE:$DOCKER_IMAGE_HASH_TAG -o /tmp/${{ matrix.image }}_image_${{ matrix.platform }}.tar
+ fi
- name: Capture tags
id: capture-tags
run: |
tags=$(cat tags.txt)
echo "tags=$tags"
echo "tags=$tags" >> $GITHUB_OUTPUT
-
- name: Upload Docker image as artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
path: /tmp/${{ matrix.image }}_image_${{ matrix.platform }}.tar
+ retention-days: 14
+ # Run unit tests with the EventStream and Server runtime Docker images
test_runtime:
name: Test Runtime
runs-on: ubuntu-latest
needs: [ghcr_build_runtime, ghcr_build]
- env:
- PERSIST_SANDBOX: "false"
-
strategy:
matrix:
- runtime_type: ["eventstream", "server"]
-
+ runtime_type: ['eventstream']
steps:
- uses: actions/checkout@v4
-
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# when set to "true" but frees about 6 GB
tool-cache: true
-
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
@@ -179,33 +166,27 @@ jobs:
haskell: true
large-packages: true
swap-storage: true
-
- 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"
-
+ python-version: '3.11'
+ cache: 'poetry'
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
-
- name: Download Runtime Docker image
if: matrix.runtime_type == 'eventstream'
uses: actions/download-artifact@v4
with:
name: od_runtime-docker-image-amd64
path: /tmp/
-
- name: Download Sandbox Docker image
if: matrix.runtime_type == 'server'
uses: actions/download-artifact@v4
with:
name: sandbox-docker-image-amd64
path: /tmp/
-
- name: Load Runtime image and run runtime tests
run: |
# Load the Docker image and capture the output
@@ -222,50 +203,47 @@ jobs:
echo "Loaded Docker image: $image_name"
TEST_RUNTIME=${{ matrix.runtime_type }} SANDBOX_USER_ID=$(id -u) SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true poetry run pytest --cov=agenthub --cov=opendevin --cov-report=xml -s ./tests/unit/test_runtime.py
-
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- integration_tests_on_linux:
- name: Integration Tests on Linux
+ # Run integration tests with the eventstream runtime Docker image
+ runtime_integration_tests_on_linux:
+ name: Runtime Integration Tests on Linux
runs-on: ubuntu-latest
- needs: ghcr_build
- env:
- PERSIST_SANDBOX: "false"
+ needs: [ghcr_build_runtime]
strategy:
fail-fast: false
matrix:
- python-version: ["3.11"]
- sandbox: ["ssh", "local"]
+ python-version: ['3.11']
+ # server is tested in a separate workflow
+ runtime_type: ['eventstream']
steps:
- uses: actions/checkout@v4
-
- name: Install poetry via pipx
run: pipx install poetry
-
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
-
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
-
- - name: Download sandbox Docker image
+ - name: Download Runtime Docker image
uses: actions/download-artifact@v4
with:
- name: sandbox-docker-image-amd64
+ name: od_runtime-docker-image-amd64
path: /tmp/
-
- - name: Load sandbox image and run integration tests
- env:
- SANDBOX_BOX_TYPE: ${{ matrix.sandbox }}
+ - name: Load runtime image and run integration tests
run: |
# Load the Docker image and capture the output
- output=$(docker load -i /tmp/sandbox_image_amd64.tar)
+ if [ "${{ matrix.runtime_type }}" == "eventstream" ]; then
+ output=$(docker load -i /tmp/od_runtime_image_amd64.tar)
+ else
+ echo "No Runtime Docker image to load"
+ exit 1
+ fi
# Extract the first image name from the output
image_name=$(echo "$output" | grep -oP 'Loaded image: \K.*' | head -n 1)
@@ -273,49 +251,40 @@ jobs:
# Print the full name of the image
echo "Loaded Docker image: $image_name"
- SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true TEST_ONLY=true ./tests/integration/regenerate.sh
-
+ TEST_RUNTIME=${{ matrix.runtime_type }} SANDBOX_USER_ID=$(id -u) SANDBOX_CONTAINER_IMAGE=$image_name TEST_IN_CI=true TEST_ONLY=true ./tests/integration/regenerate.sh
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-
+ # Push the OpenDevin and sandbox Docker images to the ghcr.io repository
ghcr_push:
runs-on: ubuntu-latest
- # don't push if integration tests or sandbox tests fail
- needs: [ghcr_build, test_runtime, integration_tests_on_linux]
+ needs: [ghcr_build]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
-
env:
tags: ${{ needs.ghcr_build.outputs.tags }}
-
permissions:
contents: read
packages: write
-
strategy:
matrix:
- image: ["sandbox", "opendevin"]
- platform: ["amd64", "arm64"]
-
+ image: ['opendevin']
+ platform: ['amd64', 'arm64']
steps:
- name: Checkout code
uses: actions/checkout@v4
-
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
- name: Download Docker images
uses: actions/download-artifact@v4
with:
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
path: /tmp/${{ matrix.platform }}
-
- name: Load images and push to registry
run: |
mv /tmp/${{ matrix.platform }}/${{ matrix.image }}_image_${{ matrix.platform }}.tar .
@@ -330,28 +299,23 @@ jobs:
docker push $image_name:${tag}_${{ matrix.platform }}
done
+ # Push the runtime Docker images to the ghcr.io repository
ghcr_push_runtime:
runs-on: ubuntu-latest
- # don't push if runtime tests fail
- needs: [ghcr_build_runtime, test_runtime, integration_tests_on_linux]
+ needs: [ghcr_build_runtime, test_runtime, runtime_integration_tests_on_linux]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
-
env:
- tags: ${{ needs.ghcr_build_runtime.outputs.tags }}
-
+ RUNTIME_TAGS: ${{ needs.ghcr_build_runtime.outputs.tags }}
permissions:
contents: read
packages: write
-
strategy:
matrix:
- image: ["od_runtime"]
- platform: ["amd64", "arm64"]
-
+ image: ['od_runtime']
+ platform: ['amd64', 'arm64']
steps:
- name: Checkout code
uses: actions/checkout@v4
-
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
@@ -362,25 +326,21 @@ jobs:
large-packages: true
docker-images: false
swap-storage: true
-
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
- name: Download Docker images
uses: actions/download-artifact@v4
with:
name: ${{ matrix.image }}-docker-image-${{ matrix.platform }}
path: /tmp/${{ matrix.platform }}
-
- name: List downloaded files
run: |
ls -la /tmp/${{ matrix.platform }}
file /tmp/${{ matrix.platform }}/*
-
- name: Load images and push to registry
run: |
mv /tmp/${{ matrix.platform }}/${{ matrix.image }}_image_${{ matrix.platform }}.tar ./${{ matrix.image }}_image_${{ matrix.platform }}.tar
@@ -389,46 +349,40 @@ jobs:
exit 1
fi
echo "loaded image = $loaded_image"
- tags=$(echo ${tags} | tr ' ' '\n')
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
echo "image name = $image_name"
- for tag in $tags; do
+ echo "$RUNTIME_TAGS" | tr ' ' '\n' | while read -r tag; do
echo "tag = $tag"
- if [ -n "$image_name" ]; then
+ if [ -n "$image_name" ] && [ -n "$tag" ]; then
docker tag $loaded_image $image_name:${tag}_${{ matrix.platform }}
docker push $image_name:${tag}_${{ matrix.platform }}
else
- echo "Skipping tag and push due to empty image_name"
+ echo "Skipping tag and push due to empty image_name or tag"
fi
done
+ # Creates and pushes the OpenDevin and sandbox Docker image manifests
create_manifest:
runs-on: ubuntu-latest
needs: [ghcr_build, ghcr_push]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
-
env:
tags: ${{ needs.ghcr_build.outputs.tags }}
-
strategy:
matrix:
- image: ["sandbox", "opendevin"]
-
+ image: ['opendevin']
permissions:
contents: read
packages: write
-
steps:
- name: Checkout code
uses: actions/checkout@v4
-
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
- name: Create and push multi-platform manifest
run: |
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
@@ -441,33 +395,28 @@ jobs:
$image_name:${tag}_arm64
done
+ # Creates and pushes the runtime Docker image manifest
create_manifest_runtime:
runs-on: ubuntu-latest
needs: [ghcr_build_runtime, ghcr_push_runtime]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
-
env:
tags: ${{ needs.ghcr_build_runtime.outputs.tags }}
-
strategy:
matrix:
- image: ["od_runtime"]
-
+ image: ['od_runtime']
permissions:
contents: read
packages: write
-
steps:
- name: Checkout code
uses: actions/checkout@v4
-
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
- name: Create and push multi-platform manifest
run: |
image_name=$(echo "ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}" | tr '[:upper:]' '[:lower:]')
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 7233bdb25a..98d905c6c3 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,3 +1,4 @@
+# Workflow that runs lint on the frontend and python code
name: Lint
concurrency:
@@ -11,27 +12,26 @@ on:
pull_request:
jobs:
+ # Run lint on the frontend code
lint-frontend:
name: Lint frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
-
- name: Install Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
-
- name: Install dependencies
run: |
cd frontend
npm install --frozen-lockfile
-
- name: Lint
run: |
cd frontend
npm run lint
+ # Run lint on the python code
lint-python:
name: Lint python
runs-on: ubuntu-latest
diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml
index 8a6c295397..6d7771e570 100644
--- a/.github/workflows/review-pr.yml
+++ b/.github/workflows/review-pr.yml
@@ -1,3 +1,4 @@
+# Workflow that uses OpenDevin to review a pull request. PR must be labeled 'review-this'
name: Use OpenDevin to Review Pull Request
on:
@@ -22,16 +23,13 @@ jobs:
run: |
sudo apt-get install -y git gh
git config --global --add safe.directory $PWD
-
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }} # check out the target branch
-
- name: Download Diff
run: |
curl -O "${{ github.event.pull_request.diff_url }}" -L
-
- name: Write Task File
run: |
echo "Your coworker wants to apply a pull request to this project." > task.txt
@@ -45,19 +43,16 @@ jobs:
echo "${{ github.event.pull_request.body }}" >> task.txt
echo "" >> task.txt
echo "Diff file is: ${{ github.event.pull_request.number }}.diff" >> task.txt
-
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
export PATH="/github/home/.local/bin:$PATH"
poetry install --without evaluation,llama-index
poetry run playwright install --with-deps chromium
-
- name: Run OpenDevin
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_MODEL: ${{ vars.LLM_MODEL }}
- SANDBOX_BOX_TYPE: ssh
run: |
# Append path to launch poetry
export PATH="/github/home/.local/bin:$PATH"
@@ -67,7 +62,6 @@ jobs:
export WORKSPACE_BASE=$GITHUB_WORKSPACE
echo -e "/exit\n" | poetry run python opendevin/core/main.py -i 50 -f task.txt
rm task.txt
-
- name: Check if review file is non-empty
id: check_file
run: |
@@ -76,7 +70,6 @@ jobs:
echo "non_empty=true" >> $GITHUB_OUTPUT
fi
shell: bash
-
- name: Create PR review if file is non-empty
env:
GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml
index eb58070b29..0badedf957 100644
--- a/.github/workflows/run-unit-tests.yml
+++ b/.github/workflows/run-unit-tests.yml
@@ -1,3 +1,4 @@
+# Workflow that runs frontend and python unit tests
name: Run Unit Tests
concurrency:
@@ -15,63 +16,52 @@ on:
- 'evaluation/**'
pull_request:
-env:
- PERSIST_SANDBOX : "false"
jobs:
+ # Run frontend unit tests
fe-test:
runs-on: ubuntu-latest
-
strategy:
matrix:
node-version: [20]
-
steps:
- name: Checkout
uses: actions/checkout@v4
-
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
-
- name: Install dependencies
working-directory: ./frontend
run: npm ci
-
- name: Run tests and collect coverage
working-directory: ./frontend
run: npm run test:coverage
-
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ # Run python unit tests on macOS
test-on-macos:
name: Test on macOS
runs-on: macos-12
env:
- INSTALL_DOCKER: "1" # Set to '0' to skip Docker installation
+ INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation
strategy:
matrix:
- python-version: ["3.11"]
-
+ python-version: ['3.11']
steps:
- uses: actions/checkout@v4
-
- name: Install poetry via pipx
run: pipx install poetry
-
- name: Set up Python ${{ matrix.python-version }}
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,llama-index
-
- name: Install & Start Docker
if: env.INSTALL_DOCKER == '1'
run: |
@@ -120,47 +110,39 @@ jobs:
# For testcontainers to find the Colima socket
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
-
- name: Build Environment
run: make build
-
- name: Run Tests
run: poetry run pytest --forked --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox.py and not test_runtime.py"
-
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ # Run python unit tests on Linux
test-on-linux:
name: Test on Linux
runs-on: ubuntu-latest
env:
- INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
+ INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
strategy:
matrix:
- python-version: ["3.11"]
-
+ python-version: ['3.11']
steps:
- uses: actions/checkout@v4
-
- name: Install poetry via pipx
run: pipx install poetry
-
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- cache: "poetry"
-
+ cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation,llama-index
-
- name: Build Environment
run: make build
-
- name: Run Tests
run: poetry run pytest --forked --cov=agenthub --cov=opendevin --cov-report=xml ./tests/unit -k "not test_sandbox.py and not test_runtime.py"
-
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
diff --git a/.github/workflows/solve-issue.yml b/.github/workflows/solve-issue.yml
index 5d86e52b5a..8e075761ab 100644
--- a/.github/workflows/solve-issue.yml
+++ b/.github/workflows/solve-issue.yml
@@ -1,3 +1,4 @@
+# Workflow that uses OpenDevin to resolve a GitHub issue. Issue must be labeled 'solve-this'
name: Use OpenDevin to Resolve GitHub Issue
on:
@@ -17,14 +18,11 @@ jobs:
image: ghcr.io/opendevin/opendevin
volumes:
- /var/run/docker.sock:/var/run/docker.sock
-
steps:
- name: install git, github cli
run: apt-get install -y git gh
-
- name: Checkout Repository
uses: actions/checkout@v4
-
- name: Write Task File
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
@@ -35,22 +33,18 @@ jobs:
echo "" >> task.txt
echo "BODY:" >> task.txt
echo "${ISSUE_BODY}" >> task.txt
-
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
export PATH="/github/home/.local/bin:$PATH"
poetry install --without evaluation,llama-index
poetry run playwright install --with-deps chromium
-
-
- name: Run OpenDevin
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- SANDBOX_BOX_TYPE: ssh
run: |
# Append path to launch poetry
export PATH="/github/home/.local/bin:$PATH"
@@ -58,7 +52,6 @@ jobs:
export PYTHONPATH=$(pwd):$PYTHONPATH
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE poetry run python ./opendevin/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
rm task.txt
-
- name: Setup Git, Create Branch, and Commit Changes
run: |
# Setup Git configuration
@@ -84,7 +77,6 @@ jobs:
# Push changes
git push --set-upstream origin $BRANCH_NAME
-
- name: Fetch Default Branch
env:
GH_TOKEN: ${{ github.token }}
@@ -93,7 +85,6 @@ jobs:
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
echo "Default branch is $DEFAULT_BRANCH"
echo "DEFAULT_BRANCH=$DEFAULT_BRANCH" >> $GITHUB_ENV
-
- name: Generate PR
env:
GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index b7e48311e4..6897fc79ad 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -1,4 +1,6 @@
+# Workflow that marks issues and PRs with no activity for 30 days with "Stale" and closes them after 7 more days of no activity
name: 'Close stale issues'
+
on:
schedule:
- cron: '30 1 * * *'
@@ -9,21 +11,9 @@ jobs:
steps:
- uses: actions/stale@v9
with:
- # Aggressively close issues that have been explicitly labeled `age-out`
- any-of-labels: age-out
- stale-issue-message: 'This issue is stale because it has been open for 7 days with no activity. Remove stale label or comment or this will be closed in 1 day.'
- close-issue-message: 'This issue was closed because it has been stalled for over 7 days with no activity.'
- stale-pr-message: 'This PR is stale because it has been open for 7 days with no activity. Remove stale label or comment or this will be closed in 1 days.'
- close-pr-message: 'This PR was closed because it has been stalled for over 7 days with no activity.'
- days-before-stale: 7
- days-before-close: 1
-
- - uses: actions/stale@v9
- with:
- # Be more lenient with other issues
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
- close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
- close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-stale: 30
+ close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
+ close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-close: 7
diff --git a/.github/workflows/update-pyproject-version.yml b/.github/workflows/update-pyproject-version.yml
deleted file mode 100644
index 24fa5429c5..0000000000
--- a/.github/workflows/update-pyproject-version.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Update pyproject.toml Version and Tags
-
-on:
- release:
- types:
- - published
-
-jobs:
- update-pyproject-and-tags:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0 # Fetch all history for all branches and tags
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.11"
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install toml
-
- - name: Get release tag
- id: get_release_tag
- run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
-
- - name: Update pyproject.toml with release tag
- run: |
- python -c "
- import toml
- with open('pyproject.toml', 'r') as f:
- data = toml.load(f)
- data['tool']['poetry']['version'] = '${{ env.RELEASE_TAG }}'
- with open('pyproject.toml', 'w') as f:
- toml.dump(data, f)
- "
-
- - name: Commit and push pyproject.toml changes
- uses: stefanzweifel/git-auto-commit-action@v4
- with:
- commit_message: "Update pyproject.toml version to ${{ env.RELEASE_TAG }}"
- branch: main
- file_pattern: pyproject.toml
diff --git a/.gitignore b/.gitignore
index fbed821549..cac17cdfa3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -169,6 +169,10 @@ evaluation/outputs
evaluation/swe_bench/eval_workspace*
evaluation/SWE-bench/data
evaluation/webarena/scripts/webarena_env.sh
+evaluation/bird/data
+evaluation/gaia/data
+evaluation/gorilla/data
+evaluation/toolqa/data
# frontend
diff --git a/Makefile b/Makefile
index 8472d234a1..c4f145c15d 100644
--- a/Makefile
+++ b/Makefile
@@ -23,9 +23,6 @@ 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-pre-commit-hooks
@@ -124,11 +121,6 @@ check-poetry:
exit 1; \
fi
-pull-docker-image:
- @echo "$(YELLOW)Pulling Docker image...$(RESET)"
- @docker pull $(DOCKER_IMAGE)
- @echo "$(GREEN)Docker image pulled successfully.$(RESET)"
-
install-python-dependencies:
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
@if [ -z "${TZ}" ]; then \
@@ -246,16 +238,6 @@ setup-config-prompts:
workspace_dir=$${workspace_dir:-$(DEFAULT_WORKSPACE_DIR)}; \
echo "workspace_base=\"$$workspace_dir\"" >> $(CONFIG_FILE).tmp
- @read -p "Do you want to persist the sandbox container? [true/false] [default: false]: " persist_sandbox; \
- persist_sandbox=$${persist_sandbox:-false}; \
- if [ "$$persist_sandbox" = "true" ]; then \
- read -p "Enter a password for the sandbox container: " ssh_password; \
- echo "ssh_password=\"$$ssh_password\"" >> $(CONFIG_FILE).tmp; \
- echo "persist_sandbox=$$persist_sandbox" >> $(CONFIG_FILE).tmp; \
- else \
- echo "persist_sandbox=$$persist_sandbox" >> $(CONFIG_FILE).tmp; \
- fi
-
@echo "" >> $(CONFIG_FILE).tmp
@echo "[llm]" >> $(CONFIG_FILE).tmp
@@ -316,4 +298,4 @@ help:
@echo " $(GREEN)help$(RESET) - Display this help message, providing information on available targets."
# Phony targets
-.PHONY: build check-dependencies check-python check-npm check-docker check-poetry pull-docker-image install-python-dependencies install-frontend-dependencies install-pre-commit-hooks lint start-backend start-frontend run run-wsl setup-config setup-config-prompts help
+.PHONY: build check-dependencies check-python check-npm check-docker check-poetry install-python-dependencies install-frontend-dependencies install-pre-commit-hooks lint start-backend start-frontend run run-wsl setup-config setup-config-prompts help
diff --git a/README.md b/README.md
index e2232ef19d..41161e9a44 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@
-
+
@@ -66,7 +66,7 @@ docker run -it \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name opendevin-app-$(date +%Y%m%d%H%M%S) \
- ghcr.io/opendevin/opendevin
+ ghcr.io/opendevin/opendevin:0.8
```
> [!NOTE]
@@ -111,7 +111,7 @@ For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
Whether you're a developer, a researcher, or simply enthusiastic about OpenDevin, we'd love to have you in our community.
Let's make software engineering better together!
-- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA) - Here we talk about research, architecture, and future development.
+- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw) - Here we talk about research, architecture, and future development.
- [Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
## 📈 Progress
@@ -141,12 +141,12 @@ Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more informati
```
@misc{opendevin,
- title={{OpenDevin: An Open Platform for AI Software Developers as Generalist Agents}},
+ title={{OpenDevin: An Open Platform for AI Software Developers as Generalist Agents}},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
year={2024},
eprint={2407.16741},
archivePrefix={arXiv},
primaryClass={cs.SE},
- url={https://arxiv.org/abs/2407.16741},
+ url={https://arxiv.org/abs/2407.16741},
}
```
diff --git a/agenthub/browsing_agent/browsing_agent.py b/agenthub/browsing_agent/browsing_agent.py
index 91a59cd282..448e8b1392 100644
--- a/agenthub/browsing_agent/browsing_agent.py
+++ b/agenthub/browsing_agent/browsing_agent.py
@@ -7,6 +7,7 @@ from agenthub.browsing_agent.response_parser import BrowsingResponseParser
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.logger import opendevin_logger as logger
+from opendevin.core.message import Message, TextContent
from opendevin.events.action import (
Action,
AgentFinishAction,
@@ -136,7 +137,7 @@ class BrowsingAgent(Agent):
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
- messages = []
+ messages: list[Message] = []
prev_actions = []
cur_axtree_txt = ''
error_prefix = ''
@@ -191,20 +192,23 @@ class BrowsingAgent(Agent):
)
return MessageAction('Error encountered when browsing.')
- if (goal := state.get_current_user_intent()) is None:
+ goal, _ = state.get_current_user_intent()
+
+ if goal is None:
goal = state.inputs['task']
+
system_msg = get_system_message(
goal,
self.action_space.describe(with_long_description=False, with_examples=True),
)
- messages.append({'role': 'system', 'content': system_msg})
+ messages.append(Message(role='system', content=[TextContent(text=system_msg)]))
prompt = get_prompt(error_prefix, cur_axtree_txt, prev_action_str)
- messages.append({'role': 'user', 'content': prompt})
+ messages.append(Message(role='user', content=[TextContent(text=prompt)]))
logger.debug(prompt)
response = self.llm.completion(
- messages=messages,
+ messages=[message.model_dump() for message in messages],
temperature=0.0,
stop=[')```', ')\n```'],
)
diff --git a/agenthub/codeact_agent/codeact_agent.py b/agenthub/codeact_agent/codeact_agent.py
index 48e4baecbb..ac03ec79b3 100644
--- a/agenthub/codeact_agent/codeact_agent.py
+++ b/agenthub/codeact_agent/codeact_agent.py
@@ -8,6 +8,7 @@ from agenthub.codeact_agent.prompt import (
)
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
+from opendevin.core.message import ImageContent, Message, TextContent
from opendevin.events.action import (
Action,
AgentDelegateAction,
@@ -131,7 +132,7 @@ class CodeActAgent(Agent):
return action.thought
return ''
- def get_action_message(self, action: Action) -> dict[str, str] | None:
+ def get_action_message(self, action: Action) -> Message | None:
if (
isinstance(action, AgentDelegateAction)
or isinstance(action, CmdRunAction)
@@ -139,39 +140,41 @@ class CodeActAgent(Agent):
or isinstance(action, MessageAction)
or (isinstance(action, AgentFinishAction) and action.source == 'agent')
):
- return {
- 'role': 'user' if action.source == 'user' else 'assistant',
- 'content': self.action_to_str(action),
- }
+ content = [TextContent(text=self.action_to_str(action))]
+
+ if isinstance(action, MessageAction) and action.images_urls:
+ content.append(ImageContent(image_urls=action.images_urls))
+
+ return Message(
+ role='user' if action.source == 'user' else 'assistant', content=content
+ )
return None
- def get_observation_message(self, obs: Observation) -> dict[str, str] | None:
+ def get_observation_message(self, obs: Observation) -> Message | None:
max_message_chars = self.llm.config.max_message_chars
if isinstance(obs, CmdOutputObservation):
- content = 'OBSERVATION:\n' + truncate_content(
- obs.content, max_message_chars
- )
- content += (
+ text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
+ text += (
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
)
- return {'role': 'user', 'content': content}
+ return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
- content = 'OBSERVATION:\n' + obs.content
+ text = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
- splitted = content.split('\n')
+ splitted = text.split('\n')
for i, line in enumerate(splitted):
if ' already displayed to user'
)
- content = '\n'.join(splitted)
- content = truncate_content(content, max_message_chars)
- return {'role': 'user', 'content': content}
+ text = '\n'.join(splitted)
+ text = truncate_content(text, max_message_chars)
+ return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, AgentDelegateObservation):
- content = 'OBSERVATION:\n' + truncate_content(
+ text = 'OBSERVATION:\n' + truncate_content(
str(obs.outputs), max_message_chars
)
- return {'role': 'user', 'content': content}
+ return Message(role='user', content=[TextContent(text=text)])
return None
def reset(self) -> None:
@@ -198,10 +201,10 @@ class CodeActAgent(Agent):
return AgentFinishAction()
# prepare what we want to send to the LLM
- messages: list[dict[str, str]] = self._get_messages(state)
+ messages = self._get_messages(state)
response = self.llm.completion(
- messages=messages,
+ messages=[message.model_dump() for message in messages],
stop=[
'',
'',
@@ -211,11 +214,11 @@ class CodeActAgent(Agent):
)
return self.action_parser.parse(response)
- def _get_messages(self, state: State) -> list[dict[str, str]]:
+ def _get_messages(self, state: State) -> list[Message]:
system_message: str = get_system_message(state.prompt_context)
messages = [
- {'role': 'system', 'content': system_message},
- {'role': 'user', 'content': self.in_context_example},
+ Message(role='system', content=[TextContent(text=system_message)]),
+ Message(role='user', content=[TextContent(text=self.in_context_example)]),
]
for event in state.history.get_events():
@@ -229,18 +232,44 @@ class CodeActAgent(Agent):
# add regular message
if message:
- messages.append(message)
+ # handle error if the message is the SAME role as the previous message
+ # litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
+ # there should not have two consecutive messages from the same role
+ if messages and messages[-1].role == message.role:
+ messages[-1].content.extend(message.content)
+ else:
+ messages.append(message)
# the latest user message is important:
# we want to remind the agent of the environment constraints
latest_user_message = next(
- (m for m in reversed(messages) if m['role'] == 'user'), None
+ (
+ m
+ for m in reversed(messages)
+ if m.role == 'user'
+ and any(isinstance(c, TextContent) for c in m.content)
+ ),
+ None,
)
- # add a reminder to the prompt
+ # Get the last user text inside content
if latest_user_message:
- latest_user_message['content'] += (
- f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with '
+ latest_user_message_text = next(
+ (
+ t
+ for t in reversed(latest_user_message.content)
+ if isinstance(t, TextContent)
+ )
)
+ # add a reminder to the prompt
+ reminder_text = f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with .'
+
+ if latest_user_message_text:
+ latest_user_message_text.text = (
+ latest_user_message_text.text + reminder_text
+ )
+ else:
+ latest_user_message_text = TextContent(text=reminder_text)
+ latest_user_message.content.append(latest_user_message_text)
return messages
diff --git a/agenthub/codeact_swe_agent/codeact_swe_agent.py b/agenthub/codeact_swe_agent/codeact_swe_agent.py
index d150af8405..b1191ff672 100644
--- a/agenthub/codeact_swe_agent/codeact_swe_agent.py
+++ b/agenthub/codeact_swe_agent/codeact_swe_agent.py
@@ -7,6 +7,7 @@ from agenthub.codeact_swe_agent.prompt import (
from agenthub.codeact_swe_agent.response_parser import CodeActSWEResponseParser
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
+from opendevin.core.message import ImageContent, Message, TextContent
from opendevin.events.action import (
Action,
AgentFinishAction,
@@ -84,40 +85,43 @@ class CodeActSWEAgent(Agent):
return action.content
return ''
- def get_action_message(self, action: Action) -> dict[str, str] | None:
+ def get_action_message(self, action: Action) -> Message | None:
if (
isinstance(action, CmdRunAction)
or isinstance(action, IPythonRunCellAction)
or isinstance(action, MessageAction)
):
- return {
- 'role': 'user' if action.source == 'user' else 'assistant',
- 'content': self.action_to_str(action),
- }
+ content = [TextContent(text=self.action_to_str(action))]
+
+ if isinstance(action, MessageAction) and action.images_urls:
+ content.append(ImageContent(image_urls=action.images_urls))
+
+ return Message(
+ role='user' if action.source == 'user' else 'assistant', content=content
+ )
+
return None
- def get_observation_message(self, obs: Observation) -> dict[str, str] | None:
+ def get_observation_message(self, obs: Observation) -> Message | None:
max_message_chars = self.llm.config.max_message_chars
if isinstance(obs, CmdOutputObservation):
- content = 'OBSERVATION:\n' + truncate_content(
- obs.content, max_message_chars
- )
- content += (
+ text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
+ text += (
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
)
- return {'role': 'user', 'content': content}
+ return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
- content = 'OBSERVATION:\n' + obs.content
+ text = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
- splitted = content.split('\n')
+ splitted = text.split('\n')
for i, line in enumerate(splitted):
if ' already displayed to user'
)
- content = '\n'.join(splitted)
- content = truncate_content(content, max_message_chars)
- return {'role': 'user', 'content': content}
+ text = '\n'.join(splitted)
+ text = truncate_content(text, max_message_chars)
+ return Message(role='user', content=[TextContent(text=text)])
return None
def reset(self) -> None:
@@ -143,10 +147,10 @@ class CodeActSWEAgent(Agent):
return AgentFinishAction()
# prepare what we want to send to the LLM
- messages: list[dict[str, str]] = self._get_messages(state)
+ messages: list[Message] = self._get_messages(state)
response = self.llm.completion(
- messages=messages,
+ messages=[message.model_dump() for message in messages],
stop=[
'',
'',
@@ -156,10 +160,10 @@ class CodeActSWEAgent(Agent):
return self.response_parser.parse(response)
- def _get_messages(self, state: State) -> list[dict[str, str]]:
- messages = [
- {'role': 'system', 'content': self.system_message},
- {'role': 'user', 'content': self.in_context_example},
+ def _get_messages(self, state: State) -> list[Message]:
+ messages: list[Message] = [
+ Message(role='system', content=[TextContent(text=self.system_message)]),
+ Message(role='user', content=[TextContent(text=self.in_context_example)]),
]
for event in state.history.get_events():
@@ -173,18 +177,38 @@ class CodeActSWEAgent(Agent):
# add regular message
if message:
- messages.append(message)
+ # handle error if the message is the SAME role as the previous message
+ # litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
+ # there should not have two consecutive messages from the same role
+ if messages and messages[-1].role == message.role:
+ messages[-1].content.extend(message.content)
+ else:
+ messages.append(message)
# the latest user message is important:
# we want to remind the agent of the environment constraints
latest_user_message = next(
- (m for m in reversed(messages) if m['role'] == 'user'), None
+ (m for m in reversed(messages) if m.role == 'user'), None
)
- # add a reminder to the prompt
+ # Get the last user text inside content
if latest_user_message:
- latest_user_message['content'] += (
- f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
+ latest_user_message_text = next(
+ (
+ t
+ for t in reversed(latest_user_message.content)
+ if isinstance(t, TextContent)
+ )
)
+ # add a reminder to the prompt
+ reminder_text = f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with .'
+
+ if latest_user_message_text:
+ latest_user_message_text.text = (
+ latest_user_message_text.text + reminder_text
+ )
+ else:
+ latest_user_message_text = TextContent(text=reminder_text)
+ latest_user_message.content.append(latest_user_message_text)
return messages
diff --git a/agenthub/delegator_agent/agent.py b/agenthub/delegator_agent/agent.py
index 5a07cfc68c..1e49603bf3 100644
--- a/agenthub/delegator_agent/agent.py
+++ b/agenthub/delegator_agent/agent.py
@@ -34,7 +34,7 @@ class DelegatorAgent(Agent):
"""
if self.current_delegate == '':
self.current_delegate = 'study'
- task = state.get_current_user_intent()
+ task, _ = state.get_current_user_intent()
return AgentDelegateAction(
agent='StudyRepoForTaskAgent', inputs={'task': task}
)
@@ -45,7 +45,7 @@ class DelegatorAgent(Agent):
if not isinstance(last_observation, AgentDelegateObservation):
raise Exception('Last observation is not an AgentDelegateObservation')
- goal = state.get_current_user_intent()
+ goal, _ = state.get_current_user_intent()
if self.current_delegate == 'study':
self.current_delegate = 'coder'
return AgentDelegateAction(
diff --git a/agenthub/dummy_agent/agent.py b/agenthub/dummy_agent/agent.py
index 6563aef415..f0fa8de9b4 100644
--- a/agenthub/dummy_agent/agent.py
+++ b/agenthub/dummy_agent/agent.py
@@ -208,9 +208,3 @@ class DummyAgent(Agent):
f' Unable to perform interactive browsing: {action.browser_actions}'
)
return MessageAction(content=message)
-
- async def get_working_directory(self, state: State) -> str:
- # Implement this method to return the current working directory
- # This might involve accessing state information or making an async call
- # For now, we'll return a placeholder value
- return './workspace'
diff --git a/agenthub/micro/agent.py b/agenthub/micro/agent.py
index 941bc9e903..0d305f70ec 100644
--- a/agenthub/micro/agent.py
+++ b/agenthub/micro/agent.py
@@ -2,6 +2,7 @@ from jinja2 import BaseLoader, Environment
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
+from opendevin.core.message import ImageContent, Message, TextContent
from opendevin.core.utils import json
from opendevin.events.action import Action
from opendevin.events.serialization.action import action_from_dict
@@ -62,16 +63,20 @@ class MicroAgent(Agent):
del self.delegates[self.agent_definition['name']]
def step(self, state: State) -> Action:
+ last_user_message, last_image_urls = state.get_current_user_intent()
prompt = self.prompt_template.render(
state=state,
instructions=instructions,
to_json=to_json,
history_to_json=self.history_to_json,
delegates=self.delegates,
- latest_user_message=state.get_current_user_intent(),
+ latest_user_message=last_user_message,
)
- messages = [{'content': prompt, 'role': 'user'}]
- resp = self.llm.completion(messages=messages)
+ content = [TextContent(text=prompt)]
+ if last_image_urls:
+ content.append(ImageContent(image_urls=last_image_urls))
+ message = Message(role='user', content=content)
+ resp = self.llm.completion(messages=[message.model_dump()])
action_resp = resp['choices'][0]['message']['content']
action = parse_response(action_resp)
return action
diff --git a/agenthub/micro/commit_writer/README.md b/agenthub/micro/commit_writer/README.md
index 927bc67da2..f82484b91d 100644
--- a/agenthub/micro/commit_writer/README.md
+++ b/agenthub/micro/commit_writer/README.md
@@ -3,7 +3,7 @@
CommitWriterAgent can help write git commit message. Example:
```bash
-WORKSPACE_MOUNT_PATH="`PWD`" SANDBOX_BOX_TYPE="ssh" \
+WORKSPACE_MOUNT_PATH="`PWD`" \
poetry run python opendevin/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
```
diff --git a/agenthub/planner_agent/agent.py b/agenthub/planner_agent/agent.py
index f3225e6e0e..04e38c0b95 100644
--- a/agenthub/planner_agent/agent.py
+++ b/agenthub/planner_agent/agent.py
@@ -1,11 +1,12 @@
from agenthub.planner_agent.response_parser import PlannerResponseParser
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
+from opendevin.core.message import ImageContent, Message, TextContent
from opendevin.events.action import Action, AgentFinishAction
from opendevin.llm.llm import LLM
from opendevin.runtime.tools import RuntimeTool
-from .prompt import get_prompt
+from .prompt import get_prompt_and_images
class PlannerAgent(Agent):
@@ -42,7 +43,13 @@ class PlannerAgent(Agent):
'abandoned',
]:
return AgentFinishAction()
- prompt = get_prompt(state, self.llm.config.max_message_chars)
- messages = [{'content': prompt, 'role': 'user'}]
- resp = self.llm.completion(messages=messages)
+
+ prompt, image_urls = get_prompt_and_images(
+ state, self.llm.config.max_message_chars
+ )
+ content = [TextContent(text=prompt)]
+ if image_urls:
+ content.append(ImageContent(image_urls=image_urls))
+ message = Message(role='user', content=content)
+ resp = self.llm.completion(messages=[message.model_dump()])
return self.response_parser.parse(resp)
diff --git a/agenthub/planner_agent/prompt.py b/agenthub/planner_agent/prompt.py
index 9289ac045b..ad26b20cac 100644
--- a/agenthub/planner_agent/prompt.py
+++ b/agenthub/planner_agent/prompt.py
@@ -115,7 +115,9 @@ def get_hint(latest_action_id: str) -> str:
return hints.get(latest_action_id, '')
-def get_prompt(state: State, max_message_chars: int) -> str:
+def get_prompt_and_images(
+ state: State, max_message_chars: int
+) -> tuple[str, list[str]]:
"""Gets the prompt for the planner agent.
Formatted with the most recent action-observation pairs, current task, and hint based on last action
@@ -161,16 +163,16 @@ def get_prompt(state: State, max_message_chars: int) -> str:
logger.info('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
# the last relevant user message (the task)
- task = state.get_current_user_intent()
+ message, image_urls = state.get_current_user_intent()
# finally, fill in the prompt
return prompt % {
- 'task': task,
+ 'task': message,
'plan': plan_str,
'history': history_str,
'hint': hint,
'plan_status': plan_status,
- }
+ }, image_urls
def parse_response(response: str) -> Action:
diff --git a/config.template.toml b/config.template.toml
index 1ce48dd517..1307852d3d 100644
--- a/config.template.toml
+++ b/config.template.toml
@@ -55,24 +55,11 @@ workspace_base = "./workspace"
# Path to rewrite the workspace mount path to
#workspace_mount_rewrite = ""
-# Persist the sandbox
-persist_sandbox = false
-
# Run as devin
#run_as_devin = true
# Runtime environment
-#runtime = "server"
-
-# SSH hostname for the sandbox
-#ssh_hostname = "localhost"
-
-# SSH password for the sandbox
-#ssh_password = ""
-
-# SSH port for the sandbox
-#ssh_port = 63710
-
+#runtime = "eventstream"
# Name of the default agent
#default_agent = "CodeActAgent"
@@ -183,9 +170,6 @@ llm_config = 'gpt3'
# Sandbox timeout in seconds
#timeout = 120
-# Sandbox type (ssh, e2b, local)
-#box_type = "ssh"
-
# Sandbox user ID
#user_id = 1000
diff --git a/containers/app/Dockerfile b/containers/app/Dockerfile
index d4aa2b38af..36ed2fbf06 100644
--- a/containers/app/Dockerfile
+++ b/containers/app/Dockerfile
@@ -32,11 +32,13 @@ FROM python:3.12.3-slim AS runtime
WORKDIR /app
+ARG OPEN_DEVIN_BUILD_VERSION #re-declare for this section
+
ENV RUN_AS_DEVIN=true
# A random number--we need this to be different from the user's UID on the host machine
ENV OPENDEVIN_USER_ID=42420
+ENV SANDBOX_API_HOSTNAME=host.docker.internal
ENV USE_HOST_NETWORK=false
-ENV SSH_HOSTNAME=host.docker.internal
ENV WORKSPACE_BASE=/opt/workspace_base
ENV OPEN_DEVIN_BUILD_VERSION=$OPEN_DEVIN_BUILD_VERSION
RUN mkdir -p $WORKSPACE_BASE
@@ -44,8 +46,10 @@ RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \
&& apt-get install -y curl ssh sudo
-RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs # Default is 1000, but OSX is often 501
-RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs # Default is 60000, but we've seen up to 200000
+# Default is 1000, but OSX is often 501
+RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs
+# Default is 60000, but we've seen up to 200000
+RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs
RUN groupadd app
RUN useradd -l -m -u $OPENDEVIN_USER_ID -s /bin/bash opendevin && \
@@ -66,6 +70,9 @@ RUN playwright install --with-deps chromium
COPY --chown=opendevin:app --chmod=770 ./opendevin ./opendevin
COPY --chown=opendevin:app --chmod=777 ./opendevin/runtime/plugins ./opendevin/runtime/plugins
COPY --chown=opendevin:app --chmod=770 ./agenthub ./agenthub
+COPY --chown=opendevin:app --chmod=770 ./pyproject.toml ./pyproject.toml
+COPY --chown=opendevin:app --chmod=770 ./poetry.lock ./poetry.lock
+COPY --chown=opendevin:app --chmod=770 ./README.md ./README.md
RUN python opendevin/core/download.py # No-op to download assets
RUN chown -R opendevin:app /app/logs && chmod -R 770 /app/logs # This gets created by the download.py script
diff --git a/containers/build.sh b/containers/build.sh
index ceeced985a..1518482344 100755
--- a/containers/build.sh
+++ b/containers/build.sh
@@ -53,6 +53,11 @@ fi
if [[ -n "$DOCKER_IMAGE_TAG" ]]; then
tags+=("$DOCKER_IMAGE_TAG")
fi
+# If $DOCKER_IMAGE_HASH_TAG is set, add it to the tags
+if [[ -n "$DOCKER_IMAGE_HASH_TAG" ]]; then
+ tags+=("$DOCKER_IMAGE_HASH_TAG")
+fi
+
DOCKER_REPOSITORY="$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE"
DOCKER_REPOSITORY=${DOCKER_REPOSITORY,,} # lowercase
diff --git a/containers/runtime/config.sh b/containers/runtime/config.sh
index 5ff2fab5a0..1a4b47f10d 100644
--- a/containers/runtime/config.sh
+++ b/containers/runtime/config.sh
@@ -4,5 +4,3 @@ DOCKER_BASE_DIR="./containers/runtime"
# These two variables will be appended by the runtime_build.py script
# DOCKER_IMAGE=
# DOCKER_IMAGE_TAG=
-DOCKER_IMAGE=od_runtime
-DOCKER_IMAGE_TAG=od_v0.8.1_image_ubuntu_tag_22.04
diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts
index 0389180426..1cd0a0d440 100644
--- a/docs/docusaurus.config.ts
+++ b/docs/docusaurus.config.ts
@@ -4,7 +4,7 @@ import { themes as prismThemes } from "prism-react-renderer";
const config: Config = {
title: "OpenDevin",
- tagline: "Code Less, Make More",
+ tagline: "An Open Platform for AI Software Developers as Generalist Agents",
favicon: "img/logo.png",
// Set the production url of your site here
@@ -32,6 +32,10 @@ const config: Config = {
},
},
+ markdown: {
+ mermaid: true,
+ },
+ themes: ['@docusaurus/theme-mermaid'],
presets: [
[
"classic",
@@ -77,7 +81,6 @@ const config: Config = {
position: "left",
label: "Codebase",
},
- { to: "/faq", label: "FAQ", position: "left" },
{
href: "https://github.com/OpenDevin/OpenDevin",
label: "GitHub",
diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/about.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/about.md
index b21435b522..aa6fa8d8e2 100644
--- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/about.md
+++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/about.md
@@ -31,7 +31,7 @@ Pour plus de détails, veuillez consulter [ce document](https://github.com/OpenD
Nous avons maintenant à la fois un espace de travail Slack pour la collaboration sur la construction d'OpenDevin et un serveur Discord pour discuter de tout ce qui est lié, par exemple, à ce projet, aux LLM, aux agents, etc.
-- [Espace de travail Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA)
+- [Espace de travail Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw)
- [Serveur Discord](https://discord.gg/ESHStjSjD4)
Si vous souhaitez contribuer, n'hésitez pas à rejoindre notre communauté. Simplifions l'ingénierie logicielle ensemble !
diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/custom_sandbox_guide.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/custom_sandbox_guide.md
index 4b77b87d2d..d7812087ff 100644
--- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/custom_sandbox_guide.md
+++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/custom_sandbox_guide.md
@@ -41,7 +41,6 @@ Créez un fichier ```config.toml``` dans le répertoire OpenDevin et entrez ces
```toml
[core]
workspace_base="./workspace"
-persist_sandbox=false
run_as_devin=true
sandbox_container_image="image_personnalisée"
```
@@ -92,7 +91,6 @@ Si vous voyez cette erreur dans la sortie de la console, il s'agit du fait que O
```toml
[core]
workspace_base="./workspace"
-persist_sandbox=false
run_as_devin=true
sandbox_container_image="image_personnalisée"
sandbox_user_id="1001"
@@ -104,4 +102,4 @@ Si vous voyez un message d'erreur indiquant que le port est utilisé ou indispon
## Discuter
-Pour d'autres problèmes ou questions rejoignez le [Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2jsrl32uf-fTeeFjNyNYxqSZt5NPY3fA) ou le [Discord](https://discord.gg/ESHStjSjD4) et demandez!
+Pour d'autres problèmes ou questions rejoignez le [Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw) ou le [Discord](https://discord.gg/ESHStjSjD4) et demandez!
diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/intro.mdx b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/intro.mdx
index 28a478842d..ed66081c48 100644
--- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/intro.mdx
+++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/intro.mdx
@@ -42,7 +42,7 @@ Explorez le code source d'OpenDevin sur [GitHub](https://github.com/OpenDevin/Op
/>
-
+
-
+
\
+ -t "" \
+ -c \
+ -l
+```
+
+For example:
+
+```bash
+poetry run python ./opendevin/core/main.py \
+ -i 10 \
+ -t "Write me a bash script that prints hello world." \
+ -c CodeActAgent \
+ -l llm
+```
+
+This command runs OpenDevin with:
+- A maximum of 10 iterations
+- The specified task description
+- Using the CodeActAgent
+- With the LLM configuration defined in the `llm` section of your `config.toml` file
+
+## How does OpenDevin work
+
+The main entry point for OpenDevin is in `opendevin/core/main.py`. Here's a simplified flow of how it works:
+
+1. Parse command-line arguments and load the configuration.
+2. Create a runtime environment using `create_runtime()`.
+3. Initialize the specified agent.
+4. Run the controller using `run_controller()`, which:
+ - Attaches the runtime to the agent
+ - Executes the agent's task
+ - Returns a final state when complete
+
+The `run_controller()` function is the core of OpenDevin's execution. It manages the interaction between the agent, the runtime, and the task, handling things like user input simulation and event processing.
+
+
+## Easiest way to get started: Exploring Existing Benchmarks
+
+We encourage you to review the various evaluation benchmarks available in the [`evaluation/` directory](https://github.com/OpenDevin/OpenDevin/blob/main/evaluation) of our repository.
+
+To integrate your own benchmark, we suggest starting with the one that most closely resembles your needs. This approach can significantly streamline your integration process, allowing you to build upon existing structures and adapt them to your specific requirements.
+
+## How to create an evaluation workflow
+
+To create an evaluation workflow for your benchmark, follow these steps:
+
+1. Create a configuration:
+ ```python
+ def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='your_container_image',
+ enable_auto_lint=True,
+ timeout=300,
+ ),
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+ ```
+
+2. Initialize the runtime and set up the evaluation environment:
+ ```python
+ async def initialize_runtime(runtime: Runtime, instance: pd.Series):
+ # Set up your evaluation environment here
+ # For example, setting environment variables, preparing files, etc.
+ pass
+ ```
+
+3. Create a function to process each instance:
+ ```python
+ async def process_instance(instance: pd.Series, metadata: EvalMetadata) -> EvalOutput:
+ config = get_config(instance, metadata)
+ runtime = await create_runtime(config, sid=instance.instance_id)
+ await initialize_runtime(runtime, instance)
+
+ instruction = get_instruction(instance, metadata)
+
+ state = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=your_user_response_function,
+ )
+
+ # Evaluate the agent's actions
+ evaluation_result = await evaluate_agent_actions(runtime, instance)
+
+ return EvalOutput(
+ instance_id=instance.instance_id,
+ instruction=instruction,
+ test_result=evaluation_result,
+ metadata=metadata,
+ history=state.history.compatibility_for_eval_history_pairs(),
+ metrics=state.metrics.get() if state.metrics else None,
+ error=state.last_error if state and state.last_error else None,
+ )
+ ```
+
+4. Run the evaluation:
+ ```python
+ metadata = make_metadata(llm_config, dataset_name, agent_class, max_iterations, eval_note, eval_output_dir)
+ output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
+ instances = prepare_dataset(your_dataset, output_file, eval_n_limit)
+
+ await run_evaluation(
+ instances,
+ metadata,
+ output_file,
+ num_workers,
+ process_instance
+ )
+ ```
+
+This workflow sets up the configuration, initializes the runtime environment, processes each instance by running the agent and evaluating its actions, and then collects the results into an `EvalOutput` object. The `run_evaluation` function handles parallelization and progress tracking.
+
+Remember to customize the `get_instruction`, `your_user_response_function`, and `evaluate_agent_actions` functions according to your specific benchmark requirements.
+
+By following this structure, you can create a robust evaluation workflow for your benchmark within the OpenDevin framework.
+
+Certainly! I'll add a section explaining the user_response_fn and include a description of the workflow and interaction. Here's an updated version of the guideline with the new section:
+
+
+## Understanding the `user_response_fn`
+
+The `user_response_fn` is a crucial component in OpenDevin's evaluation workflow. It simulates user interaction with the agent, allowing for automated responses during the evaluation process. This function is particularly useful when you want to provide consistent, predefined responses to the agent's queries or actions.
+
+
+### Workflow and Interaction
+
+The correct workflow for handling actions and the `user_response_fn` is as follows:
+
+1. Agent receives a task and starts processing
+2. Agent emits an Action
+3. If the Action is executable (e.g., CmdRunAction, IPythonRunCellAction):
+ - The Runtime processes the Action
+ - Runtime returns an Observation
+4. If the Action is not executable (typically a MessageAction):
+ - The `user_response_fn` is called
+ - It returns a simulated user response
+5. The agent receives either the Observation or the simulated response
+6. Steps 2-5 repeat until the task is completed or max iterations are reached
+
+Here's a more accurate visual representation:
+
+```
+ [Agent]
+ |
+ v
+ [Emit Action]
+ |
+ v
+ [Is Action Executable?]
+ / \
+ Yes No
+ | |
+ v v
+ [Runtime] [user_response_fn]
+ | |
+ v v
+ [Return Observation] [Simulated Response]
+ \ /
+ \ /
+ v v
+ [Agent receives feedback]
+ |
+ v
+ [Continue or Complete Task]
+```
+
+In this workflow:
+
+- Executable actions (like running commands or executing code) are handled directly by the Runtime.
+- Non-executable actions (typically when the agent wants to communicate or ask for clarification) are handled by the `user_response_fn`.
+- The agent then processes the feedback, whether it's an Observation from the Runtime or a simulated response from the `user_response_fn`.
+
+This approach allows for automated handling of both concrete actions and simulated user interactions, making it suitable for evaluation scenarios where you want to test the agent's ability to complete tasks with minimal human intervention.
+
+### Example Implementation
+
+Here's an example of a `user_response_fn` used in the SWE-Bench evaluation:
+
+```python
+def codeact_user_response(state: State | None) -> 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 exit .\n'
+ 'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
+ )
+
+ if state and state.history:
+ # check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
+ user_msgs = [
+ event
+ for event in state.history.get_events()
+ if isinstance(event, MessageAction) and event.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: exit .\n'
+ )
+ return msg
+```
+
+This function does the following:
+
+1. Provides a standard message encouraging the agent to continue working.
+2. Checks how many times the agent has attempted to communicate with the user.
+3. If the agent has made multiple attempts, it provides an option to give up.
+
+By using this function, you can ensure consistent behavior across multiple evaluation runs and prevent the agent from getting stuck waiting for human input.
diff --git a/docs/modules/usage/intro.mdx b/docs/modules/usage/intro.mdx
index 69dc6ea3c6..b571f80cfd 100644
--- a/docs/modules/usage/intro.mdx
+++ b/docs/modules/usage/intro.mdx
@@ -42,7 +42,7 @@ Explore the codebase of OpenDevin on [GitHub](https://github.com/OpenDevin/OpenD
/>
-
+
B[OpenDevin Backend]
+ B -->|Builds| C[OD Runtime Image]
+ C -->|Launches| D[Runtime Client]
+ D -->|Initializes| E[Browser]
+ D -->|Initializes| F[Bash Shell]
+ D -->|Initializes| G[Plugins]
+ G -->|Initializes| L[Jupyter Server]
+
+ B -->|Spawn| H[Agent]
+ B -->|Spawn| I[EventStream]
+ I <--->|Execute Action to
+ Get Observation
+ via REST API
+ | D
+
+ H -->|Generate Action| I
+ I -->|Obtain Observation| H
+
+ subgraph "Docker Container"
+ D
+ E
+ F
+ G
+ L
+ end
+```
+
+1. User Input: The user provides a custom base Docker image.
+
+2. Image Building: OpenDevin builds a new Docker image (the "OD runtime image") based on the user-provided image. This new image includes OpenDevin-specific code, primarily the "runtime client."
+
+3. Container Launch: When OpenDevin starts, it launches a Docker container using the OD runtime image.
+
+4. Client Initialization: The runtime client initializes inside the container, setting up necessary components like a bash shell and loading any specified plugins.
+
+5. Communication: The OpenDevin backend (`runtime.py`) communicates with the runtime client over RESTful API, sending actions and receiving observations.
+
+6. Action Execution: The runtime client receives actions from the backend, executes them in the sandboxed environment, and sends back observations.
+
+7. Observation Return: The client sends execution results back to the OpenDevin backend as observations.
+
+
+The role of the client is crucial:
+- It acts as an intermediary between the OpenDevin backend and the sandboxed environment.
+- It executes various types of actions (shell commands, file operations, Python code, etc.) safely within the container.
+- It manages the state of the sandboxed environment, including the current working directory and loaded plugins.
+- It formats and returns observations to the backend, ensuring a consistent interface for processing results.
+
+
+## Advanced: How OpenDevin builds and maintains OD Runtime images
+
+OpenDevin uses a sophisticated approach to build and manage runtime images. This process ensures efficiency, consistency, and flexibility in creating and maintaining Docker images for both production and development environments.
+
+Check out [relavant code](https://github.com/OpenDevin/OpenDevin/blob/main/opendevin/runtime/utils/runtime_build.py) if you are interested in more details.
+
+### Image Tagging System
+
+OpenDevin uses a dual-tagging system for its runtime images to balance reproducibility with flexibility:
+
+1. Hash-based tag: `{target_image_repo}:{target_image_hash_tag}`
+ Example: `od_runtime:abc123def456`
+
+ - This tag is based on the MD5 hash of the Docker build folder, which includes the source code (of runtime client and related dependencies) and Dockerfile.
+ - Identical hash tags guarantee that the images were built with exactly the same source code and Dockerfile.
+ - This ensures reproducibility: the same hash always means the same image contents.
+
+2. Generic tag: `{target_image_repo}:{target_image_tag}`
+ Example: `od_runtime:od_v0.8.3_ubuntu_tag_22.04`
+
+ - This tag follows the format: `od_runtime:od_v{OD_VERSION}_{BASE_IMAGE_NAME}_tag_{BASE_IMAGE_TAG}`
+ - It represents the latest build for a particular base image and OpenDevin version combination.
+ - This tag is updated whenever a new image is built from the same base image, even if the source code changes.
+
+The hash-based tag ensures exact reproducibility, while the generic tag provides a stable reference to the latest version of a particular configuration. This dual-tagging approach allows OpenDevin to efficiently manage both development and production environments.
+
+### Build Process
+
+1. Image Naming Convention:
+ - Hash-based tag: `{target_image_repo}:{target_image_hash_tag}`
+ Example: `od_runtime:abc123def456`
+ - Generic tag: `{target_image_repo}:{target_image_tag}`
+ Example: `od_runtime:od_v0.8.3_ubuntu_tag_22.04`
+
+2. Build Process:
+ - a. Convert the base image name to an OD runtime image name.
+ Example: `ubuntu:22.04` -> `od_runtime:od_v0.8.3_ubuntu_tag_22.04`
+ - b. Generate a build context (Dockerfile and OpenDevin source code) and calculate its hash.
+ - c. Check for an existing image with the calculated hash.
+ - d. If not found, check for a recent compatible image to use as a base.
+ - e. If no compatible image exists, build from scratch using the original base image.
+ - f. Tag the new image with both hash-based and generic tags.
+
+3. Image Reuse and Rebuilding Logic:
+ The system follows these steps to determine whether to build a new image or use an existing one from a user-provided (base) image (e.g., `ubuntu:22.04`):
+
+ a. If an image exists with the same hash (e.g., `od_runtime:abc123def456`), it will be reused as is.
+
+ b. If the exact hash is not found, the system will try to rebuild using the latest generic image (e.g., `od_runtime:od_v0.8.3_ubuntu_tag_22.04`) as a base. This saves time by leveraging existing dependencies.
+
+ c. If neither the hash-tagged nor the generic-tagged image is found, the system will build the image completely from scratch.
+
+4. Caching and Efficiency:
+ - The system attempts to reuse existing images when possible to save build time.
+ - If an exact match (by hash) is found, it's used without rebuilding.
+ - If a compatible image is found, it's used as a base for rebuilding, saving time on dependency installation.
+
+Here's a flowchart illustrating the build process:
+
+```mermaid
+flowchart TD
+ A[Start] --> B{Convert base image name}
+ B --> |ubuntu:22.04 -> od_runtime:od_v0.8.3_ubuntu_tag_22.04| C[Generate build context and hash]
+ C --> D{Check for existing image with hash}
+ D -->|Found od_runtime:abc123def456| E[Use existing image]
+ D -->|Not found| F{Check for od_runtime:od_v0.8.3_ubuntu_tag_22.04}
+ F -->|Found| G[Rebuild based on recent image]
+ F -->|Not found| H[Build from scratch]
+ G --> I[Tag with hash and generic tags]
+ H --> I
+ E --> J[End]
+ I --> J
+```
+
+This approach ensures that:
+
+1. Identical source code and Dockerfile always produce the same image (via hash-based tags).
+2. The system can quickly rebuild images when minor changes occur (by leveraging recent compatible images).
+3. The generic tag (e.g., `od_runtime:od_v0.8.3_ubuntu_tag_22.04`) always points to the latest build for a particular base image and OpenDevin version combination.
+
+By using this method, OpenDevin maintains an efficient and flexible system for building and managing runtime images, adapting to both development needs and production requirements.
+
+
+## Advanced: Runtime Plugin System
+
+The OpenDevin Runtime supports a plugin system that allows for extending functionality and customizing the runtime environment. Plugins are initialized when the runtime client starts up.
+
+Check [an example of Jupyter plugin here](https://github.com/OpenDevin/OpenDevin/blob/9c44d94cef32e6426ebd8deeeb52963153b2348a/opendevin/runtime/plugins/jupyter/__init__.py#L30-L63) if you want to implement your own plugin.
+
+*More details about the Plugin system are still under construction - contributions are welcomed!*
+
+Key aspects of the plugin system:
+
+1. Plugin Definition: Plugins are defined as Python classes that inherit from a base `Plugin` class.
+
+2. Plugin Registration: Available plugins are registered in an `ALL_PLUGINS` dictionary.
+
+3. Plugin Specification: Plugins are associate with `Agent.sandbox_plugins: list[PluginRequirement]`. Users can specify which plugins to load when initializing the runtime.
+
+4. Initialization: Plugins are initialized asynchronously when the runtime client starts.
+
+5. Usage: The runtime client can use initialized plugins to extend its capabilities (e.g., the JupyterPlugin for running IPython cells).
diff --git a/docs/package-lock.json b/docs/package-lock.json
index edfb570269..935c0614a6 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -11,6 +11,7 @@
"@docusaurus/core": "^3.4.0",
"@docusaurus/plugin-content-pages": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
+ "@docusaurus/theme-mermaid": "^3.4.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@@ -2083,6 +2084,11 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
+ "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -2579,6 +2585,27 @@
"react-dom": "^18.0.0"
}
},
+ "node_modules/@docusaurus/theme-mermaid": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmmirror.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz",
+ "integrity": "sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ==",
+ "dependencies": {
+ "@docusaurus/core": "3.4.0",
+ "@docusaurus/module-type-aliases": "3.4.0",
+ "@docusaurus/theme-common": "3.4.0",
+ "@docusaurus/types": "3.4.0",
+ "@docusaurus/utils-validation": "3.4.0",
+ "mermaid": "^10.4.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz",
@@ -3291,6 +3318,24 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.8.tgz",
+ "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz",
+ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw=="
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.3.tgz",
+ "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4976,6 +5021,14 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@@ -5320,6 +5373,463 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
+ "node_modules/cytoscape": {
+ "version": "3.30.1",
+ "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.30.1.tgz",
+ "integrity": "sha512-TRJc3HbBPkHd50u9YfJh2FxD1lDLZ+JXnJoyBn5LkncoeuT7fapO/Hq/Ed8TdFclaKshzInge2i30bg7VKeoPQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.10",
+ "resolved": "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
+ "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
+ "dependencies": {
+ "d3": "^7.8.2",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.12",
+ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.12.tgz",
+ "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
+ },
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -5474,6 +5984,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -5558,6 +6076,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -5626,6 +6152,11 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.1.6.tgz",
+ "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="
+ },
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
@@ -5690,6 +6221,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.748.tgz",
"integrity": "sha512-VWqjOlPZn70UZ8FTKUOkUvBLeTQ0xpty66qV0yJcAGY2/CthI4xyW9aEozRVtuwv3Kpf5xTesmJUcPwuJmgP4A=="
},
+ "node_modules/elkjs": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmmirror.com/elkjs/-/elkjs-0.9.3.tgz",
+ "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -7539,6 +8075,14 @@
"css-in-js-utils": "^3.1.0"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
@@ -7980,6 +8524,29 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/katex": {
+ "version": "0.16.11",
+ "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.11.tgz",
+ "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7988,6 +8555,11 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -8027,6 +8599,11 @@
"shell-quote": "^1.8.1"
}
},
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -8091,6 +8668,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -8593,6 +9175,473 @@
"node": ">= 8"
}
},
+ "node_modules/mermaid": {
+ "version": "10.9.1",
+ "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-10.9.1.tgz",
+ "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==",
+ "dependencies": {
+ "@braintree/sanitize-url": "^6.0.1",
+ "@types/d3-scale": "^4.0.3",
+ "@types/d3-scale-chromatic": "^3.0.0",
+ "cytoscape": "^3.28.1",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "d3": "^7.4.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.10",
+ "dayjs": "^1.11.7",
+ "dompurify": "^3.0.5",
+ "elkjs": "^0.9.0",
+ "katex": "^0.16.9",
+ "khroma": "^2.0.0",
+ "lodash-es": "^4.17.21",
+ "mdast-util-from-markdown": "^1.3.0",
+ "non-layered-tidy-tree-layout": "^2.0.2",
+ "stylis": "^4.1.3",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^9.0.0",
+ "web-worker": "^1.2.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/@types/mdast": {
+ "version": "3.0.15",
+ "resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.15.tgz",
+ "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==",
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "node_modules/mermaid/node_modules/@types/unist": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.10.tgz",
+ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+ },
+ "node_modules/mermaid/node_modules/mdast-util-from-markdown": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+ "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "mdast-util-to-string": "^3.1.0",
+ "micromark": "^3.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "uvu": "^0.5.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mermaid/node_modules/mdast-util-to-string": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+ "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/micromark/-/micromark-3.2.0.tgz",
+ "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-core-commonmark": "^1.0.1",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-combine-extensions": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-sanitize-uri": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-core-commonmark": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+ "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-factory-destination": "^1.0.0",
+ "micromark-factory-label": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-factory-title": "^1.0.0",
+ "micromark-factory-whitespace": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-classify-character": "^1.0.0",
+ "micromark-util-html-tag-name": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-destination": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+ "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-label": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+ "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-title": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+ "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-whitespace": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+ "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-chunked": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+ "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-classify-character": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+ "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-combine-extensions": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+ "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+ "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-decode-string": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+ "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+ "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/mermaid/node_modules/micromark-util-html-tag-name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+ "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/mermaid/node_modules/micromark-util-normalize-identifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+ "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-resolve-all": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+ "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-sanitize-uri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+ "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-subtokenize": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+ "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-types": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz",
+ "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/mermaid/node_modules/unist-util-stringify-position": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+ "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mermaid/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -10376,6 +11425,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
@@ -10486,6 +11543,11 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
+ "node_modules/non-layered-tidy-tree-layout": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
+ "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -12567,6 +13629,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+ },
"node_modules/rtl-css-js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
@@ -12619,6 +13686,22 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+ },
+ "node_modules/sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmmirror.com/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -13699,6 +14782,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ts-dedent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz",
+ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+ "engines": {
+ "node": ">=6.10"
+ }
+ },
"node_modules/ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
@@ -14195,6 +15286,31 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/uvu": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmmirror.com/uvu/-/uvu-0.5.6.tgz",
+ "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+ "dependencies": {
+ "dequal": "^2.0.0",
+ "diff": "^5.0.0",
+ "kleur": "^4.0.3",
+ "sade": "^1.7.3"
+ },
+ "bin": {
+ "uvu": "bin.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/uvu/node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@@ -14277,6 +15393,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/web-worker": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/web-worker/-/web-worker-1.3.0.tgz",
+ "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA=="
+ },
"node_modules/webpack": {
"version": "5.91.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
diff --git a/docs/package.json b/docs/package.json
index 8d3bcb970e..a529476c47 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -18,6 +18,7 @@
"@docusaurus/core": "^3.4.0",
"@docusaurus/plugin-content-pages": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
+ "@docusaurus/theme-mermaid": "^3.4.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
diff --git a/docs/src/components/CustomFooter.tsx b/docs/src/components/CustomFooter.tsx
index 36e12ac94a..23b36d6ec2 100644
--- a/docs/src/components/CustomFooter.tsx
+++ b/docs/src/components/CustomFooter.tsx
@@ -17,11 +17,9 @@ function CustomFooter() {
-
-
-
+
-
+
diff --git a/docs/src/components/Demo/Demo.tsx b/docs/src/components/Demo/Demo.tsx
index a7493e34ce..25621bca5f 100644
--- a/docs/src/components/Demo/Demo.tsx
+++ b/docs/src/components/Demo/Demo.tsx
@@ -6,7 +6,7 @@ export function Demo() {
return (
diff --git a/docs/src/components/Welcome/Welcome.tsx b/docs/src/components/Welcome/Welcome.tsx
deleted file mode 100644
index bea6849152..0000000000
--- a/docs/src/components/Welcome/Welcome.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import "../../css/welcome.css";
-import Translate from '@docusaurus/Translate';
-
-export function Welcome() {
- return (
-
-
-

-
-
- Welcome to OpenDevin, an open-source autonomous AI software engineer
- that is capable of executing
- complex engineering tasks and collaborating actively with users on
- software development projects.
-
-
-
-
- );
-}
diff --git a/docs/src/css/faq.css b/docs/src/css/faq.css
deleted file mode 100644
index 07bfe7b3e0..0000000000
--- a/docs/src/css/faq.css
+++ /dev/null
@@ -1,66 +0,0 @@
-/* faq.css */
-
-.faq-container {
- margin: auto;
- padding: 24px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-bottom: 24px;
- }
-
- .faq-title {
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 2rem;
- padding: 8px;
- text-transform: uppercase;
- font-weight: bold;
- }
-
- @media (min-width: 1024px) {
- .faq-title {
- font-size: 6rem;
- }
- }
-
- .faq-section {
- display: flex;
- flex-direction: column;
- gap: 8px;
- width: 100%;
- margin-bottom: 24px;
- }
-
- .faq-section-title {
- text-transform: uppercase;
- font-weight: bold;
- font-size: 2rem;
- letter-spacing: 0.1em;
- }
-
- .highlight {
- font-weight: 600;
- color: var(--logo);
- }
-
- .faq-steps ol {
- padding-left: 24px;
- }
-
- .command-box {
- display: flex;
- flex-direction: column;
- padding: 8px;
- background-color: #e0e0e0;
- border-radius: 0.375rem;
- height: 6vh;
- text-transform: uppercase;
- color: #4a5568;
- }
-
- .command-box + .command-box {
- height: 8vh;
- }
-
\ No newline at end of file
diff --git a/docs/src/css/footer.css b/docs/src/css/footer.css
index d172773679..173ac64067 100644
--- a/docs/src/css/footer.css
+++ b/docs/src/css/footer.css
@@ -3,12 +3,12 @@
.custom-footer {
background-color: dark;
color: white;
- height: 25vh;
+ height: 200px;
/* background: linear-gradient(to bottom, #1a1a1a, #1a1a1a); */
background: linear-gradient(to bottom, #1f2937, #000000);
}
-
+
.footer-content {
display: flex;
flex-direction: column;
@@ -17,56 +17,55 @@
padding: 8px;
height: 100%;
}
-
+
.footer-top {
display: flex;
gap: 8px;
align-items: center;
}
-
+
.footer-title {
font-weight: bold;
font-size: 1.125rem;
}
-
+
@media (min-width: 768px) {
.footer-title {
font-size: 1.875rem;
}
}
-
+
.footer-link a {
font-size: 0.875rem;
text-decoration: none;
color: gray;
transition: color 0.3s ease;
}
-
+
.footer-link a:hover {
color: white;
}
-
+
.footer-community {
text-transform: uppercase;
font-weight: 300;
}
-
+
.footer-icons {
display: flex;
gap: 24px;
font-size: 1.875rem;
}
-
+
.footer-icons a {
color:gray;
transition: color 0.3s ease;
}
-
+
.footer-icons a:hover {
color: white;
}
-
+
.footer-bottom {
text-transform: uppercase;
}
-
\ No newline at end of file
diff --git a/docs/src/css/homepageHeader.css b/docs/src/css/homepageHeader.css
index 8e1b543361..f8dd2003d7 100644
--- a/docs/src/css/homepageHeader.css
+++ b/docs/src/css/homepageHeader.css
@@ -1,36 +1,47 @@
/* homepageHeader.css */
.homepage-header {
- height: 100vh;
- color: white;
- background: linear-gradient(to top, #64748b, #000000);
- }
-
- .header-content {
- display: flex;
- flex-direction: column;
- gap: 8px;
- align-items: center;
- padding: 24px;
- font-weight: 300;
- width: 100%;
- }
-
+ height: 800px;
+ color: white;
+ background: linear-gradient(to top, #64748b, #000000);
+}
+
+.header-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 2rem;
+ font-weight: 300;
+ width: 100%;
+}
+
+.header-title {
+ font-size: 3rem;
+}
+
+@media (min-width: 768px) {
.header-title {
- font-size: 3rem;
+ font-size: 4rem;
}
-
- @media (min-width: 768px) {
- .header-title {
- font-size: 5rem;
- }
- }
-
- .header-subtitle {
- font-size: 1.25rem;
- }
-
- .header-buttons {
- margin-top: 24px;
- }
-
\ No newline at end of file
+}
+
+.header-subtitle {
+ font-size: 1.5rem;
+}
+
+.header-links {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 10px;
+ max-width: 680px;
+}
+
+.header-links a {
+ display: inline-block;
+ transition: transform 0.2s ease-in-out;
+}
+
+.header-links a:hover {
+ transform: translateY(-2px);
+}
diff --git a/docs/src/css/welcome.css b/docs/src/css/welcome.css
deleted file mode 100644
index 731cc87c19..0000000000
--- a/docs/src/css/welcome.css
+++ /dev/null
@@ -1,53 +0,0 @@
-/* welcome.css */
-
-.text-white {
- color: white;
- }
-
- .welcome-container {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- background: linear-gradient(to bottom, #64748b, #1f2937);
- }
-
- @media (min-width: 768px) {
- .welcome-container {
- flex-direction: row;
- background: linear-gradient(to bottom, #64748b, #1f2937);
- }
- }
-
- .welcome-logo {
- height: 45vh;
- width: 45vw;
- }
-
- @media (max-width: 640px) {
- .welcome-logo {
- height: 40vw;
- width: 40vw;
- }
- }
-
- @media (min-width: 768px) {
- .welcome-logo {
- height: auto;
- width: 350px;
- }
- }
-
- .welcome-text {
- padding: 24px;
- margin-bottom: 24px;
- font-weight: 300;
- font-size: 1.125rem;
- }
-
- @media (min-width: 768px) {
- .welcome-text {
- padding: 8px;
- font-size: 1.5rem;
- }
- }
diff --git a/docs/src/pages/faq.tsx b/docs/src/pages/faq.tsx
deleted file mode 100644
index c185e705f9..0000000000
--- a/docs/src/pages/faq.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import Layout from '@theme/Layout';
-import '../css/faq.css';
-import Translate, { translate } from '@docusaurus/Translate';
-
-export default function FAQ() {
- const githubLink = (
- GitHub
- );
- const discordLink = (
-
Discord
- );
- const slackLink = (
-
Slack
- );
-
- return (
-
-
-
- Frequently Asked Questions
-
-
-
- What is OpenDevin?
-
-
- OpenDevin{" "}
-
- is an autonomous software engineer that can solve software engineering
- and web-browsing tasks end-to-end. It can perform data science queries, such
- as "Find the number of pull requests to the OpenDevin repository in the last
- month," and software engineering tasks, such as "Please add tests to this
- file and verify that all the tests pass. If they don't fix the file."
-
-
-
-
- At the same time, OpenDevin is a platform and community for agent developers
- to test out and evaluate new agents.
-
-
-
-
-
- Support
-
-
-
- {`Please file a bug on {githubLink} if you notice a problem that likely affects others. If you're having trouble installing, or have general questions, reach out on {discordLink} or {slackLink}.`}
-
-
-
-
-
- How to fix a GitHub issue with OpenDevin?
-
-
-
- To fix an issue on GitHub using OpenDevin, send a prompt to OpenDevin asking it to follow
- steps like the following:
-
-
- - Read the issue https://github.com/OpenDevin/OpenDevin/issues/1611
- - Clone the repository and check out a new branch
- - Based on the instructions in the issue description, modify files to fix the issue
- - Push the resulting output to GitHub using the GITHUB_TOKEN environment variable
- - Tell me the link that I need to go to to send a pull request
-
-
- Before you run OpenDevin, you can do:
-
-
- export SANDBOX_ENV_GITHUB_TOKEN=XXX
-
-
- 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:
-
-
- Push the resulting output to my fork at https://github.com/USERNAME/OpenDevin/ using the GITHUB_TOKEN environment variable
-
-
- where USERNAME is your GitHub username.
-
-
-
-
-
- How is OpenDevin different from Devin?
-
-
- Devin
-
- is a commercial product by Cognition Inc., that served as the initial
- inspiration for OpenDevin. They both aim to do a good job at solving software
- engineering tasks, but OpenDevin you can download, use, and modify, while Devin
- you can only use through the Cognition site. In addition, OpenDevin has evolved
- beyond the initial inspiration, and now serves as a community-driven ecosystem for
- agent development in general, and we'd love to have you join and
-
- contribute!
-
-
-
-
- How is OpenDevin different from ChatGPT?
-
-
-
- ChatGPT you can access online, it does not interface with local files, and
- its ability to execute code is limited. So it can write code, but it is not
- easy to test or execute it.
-
-
-
-
-
- );
-}
diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx
index 7a2b9f0cc0..8f1605b0c6 100644
--- a/docs/src/pages/index.tsx
+++ b/docs/src/pages/index.tsx
@@ -4,12 +4,11 @@ import { HomepageHeader } from "../components/HomepageHeader/HomepageHeader";
import { Welcome } from "../components/Welcome/Welcome";
import { translate } from '@docusaurus/Translate';
-export function Header({ title, summary, description }): JSX.Element {
+export function Header({ title, summary }): JSX.Element {
return (
{title}
- {summary}
- {description}
+ {summary}
);
}
@@ -17,22 +16,15 @@ export function Header({ title, summary, description }): JSX.Element {
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
return (
- <>
-
+
- >
);
}
diff --git a/docs/static/img/teaser.mp4 b/docs/static/img/teaser.mp4
index 12ae85f595..6a3cc0821b 100644
Binary files a/docs/static/img/teaser.mp4 and b/docs/static/img/teaser.mp4 differ
diff --git a/evaluation/EDA/README.md b/evaluation/EDA/README.md
index 8ae5f7b843..06e453ec7f 100644
--- a/evaluation/EDA/README.md
+++ b/evaluation/EDA/README.md
@@ -2,9 +2,10 @@
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
+## Setup Environment and LLM Configuration
+
+Please follow instruction [here](../README.md#setup) to setup your local development environment and 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
diff --git a/evaluation/EDA/run_infer.py b/evaluation/EDA/run_infer.py
index 5cd66901d6..6d489c3053 100644
--- a/evaluation/EDA/run_infer.py
+++ b/evaluation/EDA/run_infer.py
@@ -1,30 +1,27 @@
import asyncio
-import logging
import os
import pandas as pd
-
-# import huggingface_hub
from datasets import load_dataset
from evaluation.EDA.game import Q20Game, Q20GameCelebrity
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
-
-# from evaluation.EDA.scorer import question_scorer
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
game = None
@@ -56,39 +53,44 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
-def process_instance(
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=False,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
+) -> EvalOutput:
+ config = get_config(metadata)
+ instance_id = instance['text'].strip()
+
# Setup the logger properly, so you can run multi-processing to parallelize 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)
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance_id}.')
# Prepare instruction
- _game_class = {'things': Q20Game, 'celebs': Q20GameCelebrity}
+ _game_class = {'eda-things': Q20Game, 'eda-celebs': Q20GameCelebrity}
guesser_kargs = {
'max_new_tokens': 64,
@@ -112,24 +114,16 @@ def process_instance(
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[agent.__class__.__name__]
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
# Here's how you can run the agent (similar to the `main` function) and get the final task state
+ runtime = await create_runtime(config, sid=instance['text'].strip())
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
- agent.__class__.__name__
- ],
- sid=instance['text'].strip(),
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
)
# ======= Attempt to evaluate the agent's edits =======
# If you are working on simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
@@ -150,21 +144,20 @@ def process_instance(
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
- output = {
- 'instance_id': instance['text'].strip(),
- 'instance': instance,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': {
+ output = EvalOutput(
+ instance_id=instance_id,
+ instance=instance.to_dict(),
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
'success': test_result,
'final_message': final_message,
'ground_truth': instance['text'],
},
- }
-
+ )
return output
@@ -191,12 +184,16 @@ if __name__ == '__main__':
)
args, _ = parser.parse_known_args()
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
-
eda_dataset = load_dataset(
'yizheapple/entity-deduction-arena', name=args.dataset, split=args.data_split
)
+ eda_dataset.rename(columns={'text': 'instance_id'}, inplace=True)
+
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
@@ -214,16 +211,15 @@ if __name__ == '__main__':
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
prepared_dataset = prepare_dataset(
- eda_dataset.to_pandas(), output_file, args.eval_n_limit, 'text'
+ eda_dataset.to_pandas(), output_file, args.eval_n_limit
)
- agent = Agent.get_cls(args.agent_cls)(llm=LLM(config.llm))
-
- run_evaluation(
- prepared_dataset,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- 'text',
+ asyncio.run(
+ run_evaluation(
+ prepared_dataset,
+ metadata,
+ output_file,
+ args.eval_num_workers,
+ process_instance,
+ )
)
diff --git a/evaluation/EDA/scripts/run_infer.sh b/evaluation/EDA/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/README.md b/evaluation/README.md
index 496b313ffb..7ede494b8e 100644
--- a/evaluation/README.md
+++ b/evaluation/README.md
@@ -12,15 +12,59 @@ all the preprocessing/evaluation/analysis scripts.
## Supported Benchmarks
+To learn more about how to integrate your benchmark into OpenDevin, check out [tutorial here](https://docs.all-hands.dev/modules/usage/evaluation_harness).
+
+### Software Engineering
+
- SWE-Bench: [`evaluation/swe_bench`](./swe_bench)
-- ML-Bench: [`evaluation/ml_bench`](./ml_bench)
- HumanEvalFix: [`evaluation/humanevalfix`](./humanevalfix)
-- GAIA: [`evaluation/gaia`](./gaia)
-- Entity deduction Arena (EDA): [`evaluation/EDA`](./EDA)
-- MINT: [`evaluation/mint`](./mint)
-- AgentBench: [`evaluation/agent_bench`](./agent_bench)
- BIRD: [`evaluation/bird`](./bird)
-- LogicReasoning: [`evaluation/logic_reasoning`](./logic_reasoning)
+- BioCoder: [`evaluation/ml_bench`](./ml_bench)
+- ML-Bench: [`evaluation/ml_bench`](./ml_bench)
+- APIBench: [`evaluation/gorilla`](./gorilla/)
+- ToolQA: [`evaluation/toolqa`](./toolqa/)
+
+### Web Browsing
+
+- WebArena: [`evaluation/webarena`](./webarena/)
+- MiniWob++: [`evaluation/miniwob`](./miniwob/)
+
+### Misc. Assistance
+
+- GAIA: [`evaluation/gaia`](./gaia)
+- GPQA: [`evaluation/gpqa`](./gpqa)
+- AgentBench: [`evaluation/agent_bench`](./agent_bench)
+- MINT: [`evaluation/mint`](./mint)
+- Entity deduction Arena (EDA): [`evaluation/EDA`](./EDA)
+- ProofWriter: [`evaluation/logic_reasoning`](./logic_reasoning)
+
+
+## Before everything begins: Setup Environment and LLM Configuration
+
+Please follow instruction [here](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to setup your local development environment and LLM.
+
+OpenDevin in development mode uses `config.toml` to keep track of most configurations.
+
+Here's an example configuration file you can use to define and use multiple LLMs:
+
+```toml
+[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"
+
+[llm.eval_gpt4_1106_preview_llm]
+model = "gpt-4-1106-preview"
+api_key = "XXX"
+temperature = 0.0
+
+[llm.eval_some_openai_compatible_model_llm]
+model = "openai/MODEL_NAME"
+base_url = "https://OPENAI_COMPATIBLE_URL/v1"
+api_key = "XXX"
+temperature = 0.0
+```
+
### Result Visualization
diff --git a/evaluation/TUTORIAL.md b/evaluation/TUTORIAL.md
deleted file mode 100644
index 5906d7ff58..0000000000
--- a/evaluation/TUTORIAL.md
+++ /dev/null
@@ -1,186 +0,0 @@
-# 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"
-# ==========================
-
-ssh_hostname = "localhost"
-
-run_as_devin = false
-
-[sandbox]
-# SWEBench eval specific - but you can tweak it to your needs
-use_host_network = false
-# linting python after editing helps LLM fix indentations
-enable_auto_lint = true
-
-
-box_type = "ssh"
-timeout = 120
-
-[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 config `-l` 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 \
- -l llm
-```
-
-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:
exit .\n'
- 'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP OR USE THE INTERNET TO SOLVE THIS TASK.\n'
- )
- # check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
- if state.history:
- user_msgs = [
- event
- for event in state.history.get_events()
- 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:
exit .\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]`.
diff --git a/evaluation/agent_bench/README.md b/evaluation/agent_bench/README.md
index 6da710e1c1..f656e17ba7 100644
--- a/evaluation/agent_bench/README.md
+++ b/evaluation/agent_bench/README.md
@@ -1,44 +1,10 @@
# AgentBench Evaluation
-This folder contains evaluation harness for evaluating agents on
-the [AgentBench: Evaluating LLMs as Agents](https://arxiv.org/abs/2308.03688).
+This folder contains evaluation harness for evaluating agents on the [AgentBench: Evaluating LLMs as Agents](https://arxiv.org/abs/2308.03688). We currently only support running on the `osbench` subset.
-## Configure OpenDevin and your LLM
+## Setup Environment and LLM Configuration
-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.
-
-Here is an example `config.toml` file:
-
-```toml
-[core]
-max_iterations = 100
-cache_dir = "/path/to/cache"
-
-workspace_base = "/path/to/workspace"
-workspace_mount_path = "/path/to/workspace"
-
-ssh_hostname = "localhost"
-
-# AgentBench specific
-run_as_devin = true
-
-[sandbox]
-use_host_network = false
-enable_auto_lint = true
-box_type = "ssh"
-timeout = 120
-
-[llm.eval_gpt35_turbo]
-model = "gpt-3.5-turbo"
-api_key = "sk-123"
-temperature = 0.0
-
-[llm.eval_gpt4o]
-model = "gpt-4o"
-api_key = "sk-123"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Start the evaluation
@@ -46,7 +12,18 @@ temperature = 0.0
./evaluation/agent_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit]
```
-Following is the basic command to start the evaluation. Here we are only evaluating the `osbench` for now.
+- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
+LLM settings, as defined in your `config.toml`.
+- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenDevin version you would
+like to evaluate. It could also be a release tag like `0.6.2`.
+- `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. By
+default, the script evaluates the entire SWE-bench_Lite test set (300 issues). Note:
+in order to use `eval_limit`, you must also set `agent`.
+
+
+Following is the basic command to start the evaluation.
You can update the arguments in the script `evaluation/agent_bench/scripts/run_infer.sh`, such as `--max-iterations`, `--eval-num-workers` and so on.
@@ -57,5 +34,5 @@ You can update the arguments in the script `evaluation/agent_bench/scripts/run_i
- `--eval-n-limit`: the number of examples to evaluate. For example, `100`.
```bash
-./evaluation/agent_bench/scripts/run_infer.sh eval_gpt35_turbo 0.6.2 CodeActAgent 1
+./evaluation/agent_bench/scripts/run_infer.sh eval_gpt35_turbo HEAD CodeActAgent 1
```
diff --git a/evaluation/agent_bench/helper.py b/evaluation/agent_bench/helper.py
index 3a11b5f0b2..ca17e4f97d 100644
--- a/evaluation/agent_bench/helper.py
+++ b/evaluation/agent_bench/helper.py
@@ -14,7 +14,7 @@ def try_parse_answer(act) -> str | None:
raw_ans = act.thought
else:
return None
- agent_answer = re.findall(r'
(.*?)', raw_ans)
+ agent_answer = re.findall(r'
(.*?)', raw_ans, re.DOTALL)
if not agent_answer:
return None
return agent_answer[0].strip()
diff --git a/evaluation/agent_bench/run_infer.py b/evaluation/agent_bench/run_infer.py
index 87d256e556..23514634b6 100644
--- a/evaluation/agent_bench/run_infer.py
+++ b/evaluation/agent_bench/run_infer.py
@@ -1,10 +1,9 @@
import asyncio
-import logging
import os
import re
-import shutil
+import tempfile
+from typing import Any
-import docker
import pandas as pd
from datasets import load_dataset
@@ -16,64 +15,175 @@ from evaluation.agent_bench.helper import (
)
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.events.action import CmdRunAction, MessageAction
-from opendevin.llm.llm import LLM
-from opendevin.runtime.docker.ssh_box import DockerSSHBox
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import AgentFinishAction, CmdRunAction, MessageAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
-def process_instance(
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ init_cmd = instance.init
+ if init_cmd is not None:
+ script_name = f'{instance.instance_id}_init.sh'
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ host_script_path = os.path.join(tmpdir, script_name)
+ create_sh_file(host_script_path, init_cmd)
+ await runtime.copy_to(
+ host_script_path,
+ '/workspace',
+ )
+
+ logger.info(f'Running init script: {script_name}')
+ action = CmdRunAction(command=f'chmod +x ./{script_name} && ./{script_name}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def complete_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ agent_answer = None
+ get_agent_result_cmd = instance.get_agent_result
+ if get_agent_result_cmd is not None:
+ script_name = 'get_agent_result.sh'
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ host_script_path = os.path.join(tmpdir, script_name)
+ create_sh_file(host_script_path, get_agent_result_cmd)
+ await runtime.copy_to(
+ host_script_path,
+ '/workspace',
+ )
+ logger.info(f'Running get agent result cmd: {script_name}')
+
+ action = CmdRunAction(
+ command=f'chmod +x ./{script_name} && ./{script_name}',
+ keep_prompt=False,
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+ agent_answer = obs.content
+ # IF the agent answer is not found, retrieve it from the history
+ # We wait until the controller finishes
+
+ final_ans = None
+ if instance.ground_truth is not None:
+ final_ans = instance.ground_truth
+ else:
+ get_ground_truth_cmd = instance.get_ground_truth
+ if get_ground_truth_cmd is not None:
+ script_name = 'get_ground_truth.sh'
+ with tempfile.TemporaryDirectory() as tmpdir:
+ host_script_path = os.path.join(tmpdir, script_name)
+ create_sh_file(host_script_path, get_ground_truth_cmd)
+ await runtime.copy_to(
+ host_script_path,
+ '/workspace',
+ )
+ logger.info(f'Running get ground truth cmd: {script_name}')
+
+ action = CmdRunAction(
+ command=f'chmod +x ./{script_name} && ./{script_name}',
+ keep_prompt=False,
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ final_ans = obs.content
+
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
+ return {
+ 'final_ans': final_ans,
+ 'agent_answer': agent_answer,
+ }
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
+) -> EvalOutput:
+ config = get_config(metadata)
- inst_id = instance.instance_id
- question = instance.description
- # create a directory for the instance's workspace
- instance_workspace = str(os.path.join(config.workspace_base, inst_id))
- container_inst_workspace = str(
- os.path.join(config.workspace_mount_path_in_sandbox, inst_id)
- )
- if os.path.exists(instance_workspace):
- shutil.rmtree(instance_workspace)
- os.makedirs(instance_workspace, exist_ok=True)
-
- # Set up the logger properly, so you can run multiprocessing to parallel the evaluation
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{inst_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 {inst_id}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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)
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance.instance_id}.')
# =============================================
# build instruction
@@ -86,104 +196,68 @@ def process_instance(
'Please encapsulate your final answer (answer ONLY) within
and .\n'
'For example: The answer to the question is
42 .\n'
'# Problem \n'
- f'{question}\n\n'
+ f'{instance.description}\n\n'
)
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 += INST_SUFFIXES[agent.__class__.__name__]
+ instruction += INST_SUFFIXES[metadata.agent_class]
# =============================================
# create sandbox and run the agent
# =============================================
- sandbox = DockerSSHBox(
- config=config.sandbox,
- persist_sandbox=False,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- )
- sandbox.execute(f'cd {inst_id}')
+ runtime: Runtime = await create_runtime(config, sid=instance.instance_id)
- init_cmd = instance.init
- if init_cmd is not None:
- scpt_name = f'{instance.instance_id}_init.sh'
- scpt_path = os.path.join(container_inst_workspace, scpt_name)
- host_scpt_path = os.path.join(instance_workspace, scpt_name)
- create_sh_file(host_scpt_path, init_cmd)
- logger.info(f'Running init script: {scpt_path}')
- _, init_res = sandbox.execute(scpt_path)
- logger.info(f'Init script result: {init_res}')
+ await initialize_runtime(runtime, instance=instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=FAKE_RESPONSES[agent.__class__.__name__],
- sandbox=sandbox,
- sid=inst_id,
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=FAKE_RESPONSES[metadata.agent_class],
)
-
if state is None:
raise ValueError('State should not be None.')
- # get the ground truth
- # OSBenchSSHBox.get_ground_truth(instance, state)
-
# =============================================
# result evaluation
# =============================================
- agent_answer = ''
- get_agent_result_cmd = instance.get_agent_result
- if get_agent_result_cmd is not None:
- scpt_name = f'{instance.instance_id}_get_agent_result.sh'
- scpt_path = os.path.join(container_inst_workspace, scpt_name)
- host_scpt_path = os.path.join(instance_workspace, scpt_name)
- create_sh_file(host_scpt_path, get_agent_result_cmd)
- logger.info(f'Running get agent result cmd: {scpt_path}')
- _, agent_answer = sandbox.execute(scpt_path)
- else:
+ return_val = await complete_runtime(runtime, instance)
+ agent_answer = return_val['agent_answer']
+ final_ans = return_val['final_ans']
+
+ # If the agent answer is not found, retrieve it from the history
+ if agent_answer is None:
+ agent_answer = ''
logger.info('Retrieving agent answer from history.')
raw_ans = ''
# retrieve the last agent message or thought
for event in state.history.get_events(reverse=True):
- if isinstance(event, MessageAction) and event.source == 'agent':
- raw_ans = event.content
- elif isinstance(event, CmdRunAction) and event.source == 'agent':
- raw_ans = event.thought
+ if event.source == 'agent':
+ if isinstance(event, AgentFinishAction):
+ raw_ans = event.thought
+ break
+ elif isinstance(event, MessageAction):
+ raw_ans = event.content
+ break
+ elif isinstance(event, CmdRunAction):
+ raw_ans = event.thought
+ break
# parse the answer for a solution tag
- agent_answer = re.findall(r'
(.*?)', raw_ans)
+ agent_answer = re.findall(r'
(.*?)', raw_ans, re.DOTALL)
if len(agent_answer) == 0:
logger.warning(f'Failed to parse model answer: {raw_ans}')
agent_answer = raw_ans
else:
agent_answer = agent_answer[0]
- final_ans = ''
- if instance.ground_truth is not None:
- final_ans = instance.ground_truth
- else:
- get_ground_truth_cmd = instance.get_ground_truth
- if get_ground_truth_cmd is not None:
- scpt_name = f'{instance.instance_id}_get_ground_truth.sh'
- scpt_path = os.path.join(container_inst_workspace, scpt_name)
- host_scpt_path = os.path.join(instance_workspace, scpt_name)
- create_sh_file(host_scpt_path, get_ground_truth_cmd)
- logger.info(f'Running get ground truth cmd: {scpt_path}')
- sandbox.execute(f'cd {container_inst_workspace}')
- _, final_ans = sandbox.execute(scpt_path)
-
comparison_method = instance.comparison_method
logger.info(
f'Final message: {agent_answer} | Ground truth: {final_ans} | Comparison method: {comparison_method}'
@@ -198,58 +272,49 @@ def process_instance(
metrics = state.metrics.get() if state.metrics else None
# Save the output
- output = {
- 'instance_id': inst_id,
- 'instance': instance.to_dict(),
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': {
+ output = EvalOutput(
+ instance_id=instance.instance_id,
+ instance=instance.to_dict(),
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
'agent_answer': agent_answer,
'final_answer': final_ans,
'check_method': comparison_method,
'result': test_result,
},
- }
-
- # clean up
- if os.path.exists(instance_workspace):
- shutil.rmtree(instance_workspace)
- # Close the sandbox
- try:
- sandbox.close()
- except docker.errors.NotFound as e:
- logger.error(f'Failed to close sandbox: {e}')
+ )
return output
if __name__ == '__main__':
- id_column = 'instance_id'
args = parse_arguments()
dataset = load_dataset('iFurySt/AgentBench')
agent_bench_tests = dataset['osbench'].to_pandas()
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ 'AgentBench-OS',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
+ instances = prepare_dataset(agent_bench_tests, output_file, args.eval_n_limit)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/agent_bench/scripts/run_infer.sh b/evaluation/agent_bench/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/biocoder/README.md b/evaluation/biocoder/README.md
index 6829e1aaa5..c549afc9c1 100644
--- a/evaluation/biocoder/README.md
+++ b/evaluation/biocoder/README.md
@@ -2,15 +2,12 @@
Implements evaluation of agents on BioCoder from the BioCoder benchmark introduced in [BioCoder: A Benchmark for Bioinformatics Code Generation with Large Language Models](https://arxiv.org/abs/2308.16458). 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
+## Setup Environment and LLM Configuration
-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. Please check [README.md](../../README.md) for how to set this up.
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## BioCoder Docker Image
+
In the opendevin branch of the Biocoder repository, we have slightly modified our original Docker image to work with the OpenDevin environment. In the Docker image are testing scripts (`/testing/start_test_opendevin.py` and aux files in `/testing_files/`) to assist with evaluation. Additionally, we have installed all dependencies, including OpenJDK, mamba (with Python 3.6), and many system libraries. Notably, we have **not** packaged all repositories into the image, so they are downloaded at runtime.
**Before first execution, pull our Docker image with the following command**
@@ -41,12 +38,12 @@ to `CodeActAgent`.
- `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 1 instance using `eval_gpt4_1106_eval_gpt4o_2024_05_13preview` and CodeActAgent
-with OpenDevin version 0.6.2, then your command would be:
+with current OpenDevin version, then your command would be:
## Examples
```bash
-./evaluation/biocoder/scripts/run_infer.sh eval_gpt4o_2024_05_13 0.6.2 CodeActAgent 1
+./evaluation/biocoder/scripts/run_infer.sh eval_gpt4o_2024_05_13 HEAD CodeActAgent 1
```
## Reference
diff --git a/evaluation/biocoder/biocoder_env_box.py b/evaluation/biocoder/biocoder_env_box.py
deleted file mode 100644
index f589535ca3..0000000000
--- a/evaluation/biocoder/biocoder_env_box.py
+++ /dev/null
@@ -1,387 +0,0 @@
-import json
-import os
-import re
-import sys
-from collections import defaultdict
-from dataclasses import dataclass
-
-from datasets import load_dataset
-
-from opendevin.core.config import load_app_config
-from opendevin.core.logger import opendevin_logger as logger
-from opendevin.runtime.docker.ssh_box import DockerSSHBox
-from opendevin.runtime.plugins import (
- JupyterRequirement,
- PluginRequirement,
- SWEAgentCommandsRequirement,
-)
-
-config = load_app_config()
-
-BIOCODER_BENCH_CONTAINER_IMAGE = 'public.ecr.aws/i5g0m1f6/eval_biocoder:v1.0'
-
-
-@dataclass
-class BiocoderData:
- filePath: str
- numLines: int
- lineStart: int
- lineEnd: int
- signature: str
- comment: str
- content: str
- repository: str
- promptSummaryOnly: str
- contextCode: str
- goldenCode: str
- test_case_id: str
- language: str
-
- def to_dict(self):
- return {
- 'filePath': self.filePath,
- 'numLines': self.numLines,
- 'lineStart': self.lineStart,
- 'lineEnd': self.lineEnd,
- 'signature': self.signature,
- 'comment': self.comment,
- 'content': self.content,
- 'repository': self.repository,
- 'promptSummaryOnly': self.promptSummaryOnly,
- 'contextCode': self.contextCode,
- 'goldenCode': self.goldenCode,
- 'test_case_id': self.test_case_id,
- 'language': self.language,
- }
-
-
-def get_likely_indent_size(array_of_tabs) -> int:
- sizes = defaultdict(int)
-
- for i in range(len(array_of_tabs) - 1):
- diff = array_of_tabs[i + 1] - array_of_tabs[i]
- if diff > 0:
- sizes[diff] += 1
- if len(sizes) == 0:
- return 4
- return int(max(sizes, key=sizes.get))
-
-
-class BiocoderSSHBox(DockerSSHBox):
- def __init__(
- self,
- container_image: str,
- timeout: int = 120,
- sid: str | None = None,
- biocoder_instance_id: str | None = None,
- biocoder_instance: BiocoderData | None = None,
- skip_workspace_mount: bool = True,
- sandbox_plugins: list[PluginRequirement] = [], # noqa: B006
- biocoder_cache_folder: str = 'biocoder_cache',
- workspace_dir_name: str | None = None,
- ):
- if biocoder_instance_id is None:
- raise ValueError('biocoder_instance_id must be provided')
- self.biocoder_instance_id = biocoder_instance_id
- self.biocoder_instance = biocoder_instance
- self.skip_workspace_mount = skip_workspace_mount
- self.biocoder_cache_folder = biocoder_cache_folder
- self.first_line_after_removed = None
- self.workspace_dir_name = workspace_dir_name
- self.workspace_base = config.workspace_base
- self.workspace_mount_path = config.workspace_mount_path
- # self.workspace_dir_name_host = os.path.join(config.workspace_base, workspace_dir_name)
-
- self.context_path = None
- self.generated_path = None
- self.golden_path = None
-
- assert (
- container_image is not None
- ), 'container_image is required for BiocoderBenchSSHBox!'
- super().__init__(container_image, timeout, sid)
- self.init_plugins(sandbox_plugins)
-
- @property
- def volumes(self):
- if self.skip_workspace_mount:
- return {
- k: v
- for k, v in super().volumes.items()
- if not v['bind'] == self.sandbox_workspace_dir
- }
- return super().volumes
-
- def get_target_filepath(self):
- target_filepath = os.path.join(
- self.workspace_mount_path,
- self.biocoder_instance.repository.split('/')[1],
- self.biocoder_instance.filePath,
- )
- return target_filepath
-
- def get_changed_code(self, include_signature=False):
- # copies changed code into /testing_files/
- # Note that this does NOT copy the function signature
- target_filepath = self.get_target_filepath()
- selected_lines = []
- offset = 1 if include_signature else 0
- if self.first_line_after_removed is None:
- logger.warning('First line after removed is None')
- with open(target_filepath, 'r') as f:
- lines = f.read().split('\n')
- for i in range(self.biocoder_instance.lineStart - offset, len(lines)):
- if lines[i].strip() == self.first_line_after_removed.strip():
- break
- selected_lines.append(lines[i])
- text = '\n'.join(selected_lines)
- return text
-
- def copy_changed_code(self):
- changed_code = self.get_changed_code(include_signature=True)
- with open(self.generated_path, 'w') as f:
- f.write(changed_code)
- exit_code, output = self.execute_and_check(
- f'cp -r /workspace/{self.biocoder_cache_folder}/* /testing_files',
- 'Failed to copy the files',
- )
-
- def remove_code(self):
- comment_prefix = {'python': '#', 'java': '//'}
-
- target_filepath = self.get_target_filepath()
- line_start = self.biocoder_instance.lineStart
- line_end = self.biocoder_instance.lineEnd
- with open(target_filepath, 'r') as f:
- lines = f.read().split('\n')
- # print("="*10+"ORIGINAL"+"="*10)
- # print("\n".join(lines))
- signature_line = lines[line_start - 1]
-
- # get the number of tabs
- def get_indent_size(s: str):
- return len(re.match(r'\s*', s).group())
-
- indent_sizes = list(map(get_indent_size, lines))
- indent_size = get_likely_indent_size(indent_sizes)
- comment_indent_size = get_indent_size(signature_line) + indent_size
- lines = (
- lines[:line_start]
- + [
- f"{' '*comment_indent_size+comment_prefix[self.biocoder_instance.language.lower()]}TODO: replace with your code here"
- ]
- + ([''] * 2)
- + lines[line_end:]
- )
- first_line_after_removed_index = line_start
- while len(
- lines[first_line_after_removed_index].strip()
- ) == 0 and first_line_after_removed_index < len(lines):
- first_line_after_removed_index += 1
- self.first_line_after_removed = lines[first_line_after_removed_index]
- # print("FIRST LINE AFTER REMOVED: ", self.first_line_after_removed)
-
- with open(target_filepath, 'w') as f:
- f.write('\n'.join(lines))
-
- # with open(target_filepath, 'r') as f:
- # print("="*10+"MODIFIED"+"="*10)
- # print(f.read())
-
- def execute_and_check(self, cmd: str, error_msg: str) -> tuple[int, str]:
- exit_code, output = self.execute(cmd)
- if exit_code != 0:
- logger.error(error_msg)
- sys.exit(1)
- return exit_code, output
-
- @classmethod
- def get_box_for_instance(
- cls,
- instance,
- workspace_dir_name=None,
- skip_workspace_mount: bool = False,
- workspace_mount_path: str | None = None,
- sandbox_plugins: list[PluginRequirement] = [], # noqa: B006
- ) -> 'BiocoderSSHBox':
- """This method initializes a container image, then runs some initialization commands"""
- if workspace_dir_name is None:
- workspace_dir_name = f'{instance.repository}__{instance.test_case_id[:10]}__{os.getpid()}'.replace(
- '/', '__'
- )
-
- workspace_base = str(os.path.join(config.workspace_base, workspace_dir_name))
- old_workspace_base = config.workspace_base
- old_workspace_mount_path = config.workspace_mount_path
-
- try:
- config.workspace_base = workspace_base
- config.workspace_mount_path = workspace_base
-
- # linting python after editing helps LLM fix indentations
- config.sandbox.enable_auto_lint = True
-
- # create folder for transferring files back/forth
- biocoder_cache_folder = 'biocoder_cache'
- if not os.path.exists(os.path.join(workspace_base, biocoder_cache_folder)):
- os.makedirs(
- os.path.join(workspace_base, biocoder_cache_folder), exist_ok=True
- )
-
- file_ext = {
- 'python': 'py',
- 'java': 'java',
- 'c': 'c',
- 'cpp': 'cpp',
- 'javascript': 'js',
- 'typescript': 'ts',
- }[instance.language.lower()]
-
- context_path = os.path.join(
- workspace_base, biocoder_cache_folder, 'context.' + file_ext
- )
- generated_path = os.path.join(
- workspace_base, biocoder_cache_folder, 'generated.' + file_ext
- )
- golden_path = os.path.join(
- workspace_base, biocoder_cache_folder, 'golden.' + file_ext
- )
-
- # print(instance.contextCode)
- with open(context_path, 'w') as f:
- f.write(instance.contextCode)
- with open(generated_path, 'w') as f:
- f.write(instance.goldenCode)
- with open(golden_path, 'w') as f:
- f.write(instance.goldenCode)
-
- testcase_json = {
- 'test_case_id': instance.test_case_id,
- 'num_cases': 1000,
- 'language': instance.language.lower(),
- }
-
- with open(
- os.path.join(
- workspace_base, biocoder_cache_folder, 'testcase_biocoder.json'
- ),
- 'w',
- ) as f:
- f.write(json.dumps(testcase_json, indent=4))
-
- # linting python after editing helps LLM fix indentations
- config.sandbox.enable_auto_lint = True
-
- sandbox = cls(
- container_image=BIOCODER_BENCH_CONTAINER_IMAGE,
- biocoder_instance_id=instance.test_case_id,
- biocoder_instance=instance,
- skip_workspace_mount=skip_workspace_mount,
- sandbox_plugins=sandbox_plugins,
- biocoder_cache_folder=biocoder_cache_folder,
- workspace_dir_name=workspace_dir_name,
- )
- except Exception:
- raise
- finally:
- config.workspace_base = old_workspace_base
- config.workspace_mount_path = old_workspace_mount_path
-
- sandbox.context_path = context_path
- sandbox.generated_path = generated_path
- sandbox.golden_path = golden_path
-
- logger.info(f'SSH box started for instance {instance.test_case_id}.')
- # cd to the workspace
- exit_code, output = sandbox.execute_and_check(
- 'cd /workspace', 'Failed to cd to workspace'
- )
- logger.info(f'cd to workspace: {output}')
-
- # download repository archive
- repository_url = f"https://biocoder.lilbillbiscuit.com/repos/{instance.repository.split('/')[1]}.zip"
- exit_code, output = sandbox.execute_and_check(
- 'wget -O repo.zip ' + repository_url, 'Failed to download the repository'
- )
- logger.info(f'Downloaded the repository: {output}')
- exit_code, output = sandbox.execute_and_check(
- 'unzip -o -q repo.zip', 'Failed to unzip the repository'
- )
- logger.info(f'Unzipped the repository: {output}')
-
- # copy the context, generated and golden files to the /testing_files folder
- exit_code, output = sandbox.execute_and_check(
- f'cp -r /workspace/{biocoder_cache_folder}/* /testing_files',
- 'Failed to copy the files',
- )
-
- # chmod 777
- exit_code, output = sandbox.execute_and_check(
- 'chmod -R 777 /workspace',
- 'Failed to chmod the files',
- )
-
- return sandbox
-
-
-if __name__ == '__main__':
- biocoder_dataset = load_dataset('Lilbillbiscuit/biocoder_public')
- EXAMPLE_INSTANCE = biocoder_dataset['test'][0]
- EXAMPLE_INSTANCE = BiocoderData(**EXAMPLE_INSTANCE)
-
- sandbox = BiocoderSSHBox.get_box_for_instance(
- instance=EXAMPLE_INSTANCE,
- workspace_mount_path='/home/ubuntu/OpenDevinBioCoder/workspace',
- skip_workspace_mount=False,
- sandbox_plugins=[JupyterRequirement(), SWEAgentCommandsRequirement()],
- )
-
- # PRE TEST
- exit_code, output = sandbox.execute_and_check(
- 'cd /testing',
- 'Failed to cd /testing',
- )
- logger.info(f'cd $REPO_PATH: {output}')
-
- exit_code, output = sandbox.execute_and_check(
- 'whoami',
- 'Failed to run whoami',
- )
- logger.info(f'whoami: {output}')
-
- # TEST
- exit_code, output = sandbox.execute(
- '/home/devin/mambaforge/bin/mamba run -n test python3 /testing/start_test_opendevin.py'
- )
- assert exit_code == 0, 'Expected exit code 0 (this should have passed)'
- logger.info(f'$TEST_CMD:\n{output}')
-
- exit_code, output = sandbox.execute_and_check(
- 'cat /testing_files/results_biocoder.json', 'Failed to read the result file'
- )
-
- print(output)
- json_obj = json.loads(output)
- if json_obj['result'] == 'pass':
- print('PASS')
- else:
- print('FAIL')
-
- sys.stdout.flush()
- try:
- while True:
- try:
- user_input = input('>>> ')
- except EOFError:
- logger.info('Exiting...')
- break
- if user_input.lower() == 'exit':
- logger.info('Exiting...')
- break
- exit_code, output = sandbox.execute(user_input)
- logger.info('exit code: %d', exit_code)
- logger.info(output)
- sys.stdout.flush()
- except KeyboardInterrupt:
- logger.info('Exiting...')
- sandbox.close()
diff --git a/evaluation/biocoder/run_infer.py b/evaluation/biocoder/run_infer.py
index 3fb14e8bd7..3fb2137440 100644
--- a/evaluation/biocoder/run_infer.py
+++ b/evaluation/biocoder/run_infer.py
@@ -1,33 +1,38 @@
import asyncio
+import functools
import json
-import logging
import os
-import pathlib
-from functools import partial
+import tempfile
+from typing import Any
import pandas as pd
from datasets import load_dataset
-from evaluation.biocoder.biocoder_env_box import BiocoderData, BiocoderSSHBox
+from evaluation.biocoder.utils import BiocoderData
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import CmdRunAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
- 'CodeActAgent': partial(
+ 'CodeActAgent': functools.partial(
codeact_user_response, encapsulate_solution=True, try_parse=None
),
}
@@ -36,111 +41,218 @@ AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command:
exit .\n'
}
+FILE_EXT_MAP = {
+ 'python': 'py',
+ 'java': 'java',
+ 'c': 'c',
+ 'cpp': 'cpp',
+ 'javascript': 'js',
+ 'typescript': 'ts',
+}
+
+
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ BIOCODER_BENCH_CONTAINER_IMAGE = 'public.ecr.aws/i5g0m1f6/eval_biocoder:v1.0'
+
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image=BIOCODER_BENCH_CONTAINER_IMAGE,
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: BiocoderData, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ file_ext = FILE_EXT_MAP[instance.language.lower()]
+
+ action = CmdRunAction(command='mkdir -p /workspace && mkdir -p /testing_files')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ context_path = os.path.join(tmpdir, 'context.' + file_ext)
+ with open(context_path, 'w') as f:
+ f.write(instance.contextCode)
+ await runtime.copy_to(context_path, '/testing_files')
+
+ golden_path = os.path.join(tmpdir, 'golden.' + file_ext)
+ with open(golden_path, 'w') as f:
+ f.write(instance.goldenCode)
+ await runtime.copy_to(golden_path, '/testing_files')
+
+ testcase_json = {
+ 'test_case_id': instance.test_case_id,
+ 'num_cases': 1000,
+ 'language': instance.language.lower(),
+ }
+ testcase_path = os.path.join(tmpdir, 'testcase_biocoder.json')
+ with open(testcase_path, 'w') as f:
+ f.write(json.dumps(testcase_json, indent=4))
+
+ await runtime.copy_to(testcase_path, '/testing_files')
+
+ # setup paths
+ remove_code_script = os.path.join(
+ os.path.dirname(__file__), 'scripts', 'setup', 'remove_code.py'
+ )
+ await runtime.copy_to(remove_code_script, '/testing_files')
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ # download repository archive
+ repository_url = f"https://biocoder.lilbillbiscuit.com/repos/{instance.repository.split('/')[1]}.zip"
+ action = CmdRunAction(command='wget -O repo.zip ' + repository_url)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0, f'Failed to download the repository: {obs.content}'
+
+ # unzip the repository
+ action = CmdRunAction(command='unzip -o -q repo.zip && rm repo.zip')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0, f'Failed to unzip the repository: {obs.content}'
+
+ # chmod 777
+ action = CmdRunAction(command='chmod -R 777 /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0, f'Failed to chmod the files: {obs.content}'
+
+ # remove code for evaluation instance
+ target_filepath = os.path.join(
+ '/workspace', instance.repository.split('/')[1], instance.filePath
+ )
+ line_start = instance.lineStart
+ line_end = instance.lineEnd
+ language = instance.language.lower()
+ action = CmdRunAction(
+ command=f'python3 /testing_files/remove_code.py --target_filepath {target_filepath} --line_start {line_start} --line_end {line_end} --language {language}'
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0, f'Failed to remove the code: {obs.content}'
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def complete_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
-def get_test_result(instance, sandbox, workspace_dir_name):
test_result = {'result': {}, 'metadata': {}}
- try:
- code = sandbox.get_changed_code(include_signature=True)
- sandbox.copy_changed_code()
+
+ copy_changed_code_script = os.path.join(
+ os.path.dirname(__file__), 'scripts', 'setup', 'copy_changed_code.py'
+ )
+ await runtime.copy_to(copy_changed_code_script, '/testing_files')
+
+ file_ext = FILE_EXT_MAP[instance.language.lower()]
+ target_filepath = os.path.join(
+ '/workspace', instance.repository.split('/')[1], instance.filePath
+ )
+ generated_path = os.path.join('/testing_files', 'generated.' + file_ext)
+
+ action = CmdRunAction(
+ command=f'python3 /testing_files/copy_changed_code.py --target_filepath {target_filepath} --generated_code_filepath {generated_path} --line_start {instance.lineStart} --include_signature'
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ if obs.exit_code == 0:
test_result['metadata']['1_copy_change_success'] = True
+
+ action = CmdRunAction(command=f'cat {generated_path}', keep_prompt=False)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ code = obs.content
test_result['metadata']['1_copy_change_code'] = code
- except Exception:
- logger.error('Error fetching changed code for this instance')
+ else:
test_result['metadata']['1_copy_change_success'] = False
test_result['metadata']['1_copy_change_code'] = None
- exit_code, output = sandbox.execute_and_check(
- 'cd /testing',
- 'Failed to cd /testing',
- )
- logger.info(f'cd $REPO_PATH: {output}')
+ action = CmdRunAction(command='cd /testing_files')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
- exit_code, output = sandbox.execute_and_check(
- 'whoami',
- 'Failed to run whoami',
+ action = CmdRunAction(
+ command='/home/devin/mambaforge/bin/mamba run -n test python3 /testing/start_test_opendevin.py'
)
- logger.info(f'whoami: {output}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
- exit_code, output = sandbox.execute(
- '/home/devin/mambaforge/bin/mamba run -n test python3 /testing/start_test_opendevin.py'
+ action = CmdRunAction(
+ command='cat /testing_files/results_biocoder.json', keep_prompt=False
)
- logger.info(f'$TEST_CMD:\n{output}')
-
- exit_code, output = sandbox.execute_and_check(
- 'cat /testing_files/results_biocoder.json', 'Failed to read the result file'
- )
- if exit_code == 0:
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ if obs.exit_code == 0:
test_result['metadata']['2_run_test_success'] = True
- test_result['metadata']['2_run_test_result'] = str(output)
+ test_result['metadata']['2_run_test_result'] = str(obs.content)
+ json_obj = json.loads(obs.content)
+ test_result['result'] = json_obj['result']
else:
test_result['metadata']['2_run_test_success'] = False
- test_result['metadata']['2_run_test_result'] = str(output)
- json_obj = json.loads(output)
- test_result['result'] = json_obj['result']
+ test_result['metadata']['2_run_test_result'] = str(obs.content)
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
return test_result
-def process_instance(
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
+) -> EvalOutput:
+ config = get_config(metadata)
instance = BiocoderData(**instance)
print(instance)
- workspace_dir_name = (
- f'{instance.repository}__{instance.test_case_id[:10]}__{os.getpid()}'.replace(
- '/', '__'
- )
- )
- workspace_mount_path = os.path.join(config.workspace_base, workspace_dir_name)
- # create process-specific workspace dir
- # if `not skip_workspace_mount` - we will create a workspace directory for EACH process
- # so that different agent don't interfere with each other.
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+ instance_id = f'{instance.repository}__{instance.instance_id[:10]}'
- # Setup the logger properly, so you can run multi-processing to parallize the evaluation
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{instance.test_case_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.test_case_id}.\nHint: run "tail -f {log_file}" to see live logs in a seperate shell'
- )
- # 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}')
-
- # NOTE: this is something special we do for SWE-Bench due to the reason described in the previous section
- # You can omit this if you don't need to setup specialized sandbox
- workspace_dir_name = f'{instance.repository}__{instance.test_case_id[:10]}'.replace(
- '/', '__'
- )
- sandbox = BiocoderSSHBox.get_box_for_instance(
- instance,
- workspace_dir_name,
- skip_workspace_mount=False,
- workspace_mount_path=workspace_mount_path,
- sandbox_plugins=agent.sandbox_plugins,
- )
-
- sandbox.remove_code()
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance_id}.')
# Prepare instruction
instruction = (
@@ -160,80 +272,76 @@ def process_instance(
'Make sure to include proper formatting in Java and Python, including correct braces and/or indentation.\n'
)
# NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
# use a session id for concurrent evaluation
- sid = instance.test_case_id.replace('/', '__')
+ sid = instance.instance_id.replace('/', '__')
+
+ runtime = await create_runtime(config, sid=sid)
+
+ await initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
- agent.__class__.__name__
- ],
- sandbox=sandbox,
- sid=sid,
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
)
- test_result = get_test_result(instance, sandbox, workspace_dir_name)
-
if state is None:
raise ValueError('State should not be None.')
- metrics = state.metrics.get() if state.metrics else None
+ test_result = await complete_runtime(runtime, instance)
+ metrics = state.metrics.get() if state.metrics else None
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = state.history.compatibility_for_eval_history_pairs()
- # Save the output
- output = {
- 'test_case_id': instance.test_case_id,
- 'biocoder_instance': instance.to_dict(),
- 'instruction': instruction,
- 'generated': test_result['metadata']['1_copy_change_code'],
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': test_result,
- }
+ test_result['generated'] = test_result['metadata']['1_copy_change_code']
- # Close the sandbox
- sandbox.close()
+ # Save the output
+ output = EvalOutput(
+ instance_id=instance.instance_id,
+ instance=instance.to_dict(),
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result=test_result,
+ )
return output
if __name__ == '__main__':
- id_column = 'test_case_id'
args = parse_arguments()
- dataset = load_dataset('lilbillbiscuit/biocoder_public')
- biocoder_tests = dataset['test'].to_pandas()
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ dataset = load_dataset('lilbillbiscuit/biocoder_public')
+ biocoder_tests = dataset['train'].to_pandas()
+ biocoder_tests['instance_id'] = biocoder_tests['test_case_id']
+
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ 'biocoder',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
+ instances = prepare_dataset(biocoder_tests, output_file, args.eval_n_limit)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/biocoder/scripts/run_infer.sh b/evaluation/biocoder/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/biocoder/scripts/setup/copy_changed_code.py b/evaluation/biocoder/scripts/setup/copy_changed_code.py
new file mode 100644
index 0000000000..2cee1e97b6
--- /dev/null
+++ b/evaluation/biocoder/scripts/setup/copy_changed_code.py
@@ -0,0 +1,45 @@
+import argparse
+
+
+def get_changed_code(target_filepath, line_start, include_signature=False):
+ # copies changed code into /testing_files/
+ # Note that this does NOT copy the function signature
+ selected_lines = []
+ offset = 1 if include_signature else 0
+
+ with open('/testing_files/first_line_after_removed.txt', 'r') as f:
+ first_line_after_removed = f.read()
+ if first_line_after_removed is None:
+ print('First line after removed is None')
+
+ with open(target_filepath, 'r') as f:
+ lines = f.read().split('\n')
+ for i in range(line_start - offset, len(lines)):
+ if lines[i].strip() == first_line_after_removed.strip():
+ break
+ selected_lines.append(lines[i])
+ text = '\n'.join(selected_lines)
+ return text
+
+
+def copy_changed_code(
+ target_filepath, generated_code_filepath, line_start, include_signature=False
+):
+ changed_code = get_changed_code(target_filepath, line_start, include_signature)
+ with open(generated_code_filepath, 'w') as f:
+ f.write(changed_code)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--target_filepath', type=str, required=True)
+ parser.add_argument('--generated_code_filepath', type=str, required=True)
+ parser.add_argument('--line_start', type=int, required=True)
+ parser.add_argument('--include_signature', action='store_true')
+ args = parser.parse_args()
+ copy_changed_code(
+ args.target_filepath,
+ args.generated_code_filepath,
+ args.line_start,
+ args.include_signature,
+ )
diff --git a/evaluation/biocoder/scripts/setup/remove_code.py b/evaluation/biocoder/scripts/setup/remove_code.py
new file mode 100644
index 0000000000..3c76a41738
--- /dev/null
+++ b/evaluation/biocoder/scripts/setup/remove_code.py
@@ -0,0 +1,74 @@
+import argparse
+import os
+import re
+from collections import defaultdict
+
+
+def get_likely_indent_size(array_of_tabs) -> int:
+ sizes = defaultdict(int)
+
+ for i in range(len(array_of_tabs) - 1):
+ diff = array_of_tabs[i + 1] - array_of_tabs[i]
+ if diff > 0:
+ sizes[diff] += 1
+ if len(sizes) == 0:
+ return 4
+ return int(max(sizes, key=sizes.get))
+
+
+def get_target_filepath(self):
+ target_filepath = os.path.join(
+ self.workspace_mount_path,
+ self.biocoder_instance.repository.split('/')[1],
+ self.biocoder_instance.filePath,
+ )
+ return target_filepath
+
+
+def remove_code(target_filepath: str, line_start: int, line_end: int, language: str):
+ comment_prefix = {'python': '#', 'java': '//'}
+
+ with open(target_filepath, 'r') as f:
+ lines = f.read().split('\n')
+ # print("="*10+"ORIGINAL"+"="*10)
+ # print("\n".join(lines))
+ signature_line = lines[line_start - 1]
+
+ # get the number of tabs
+ def get_indent_size(s: str):
+ return len(re.match(r'\s*', s).group())
+
+ indent_sizes = list(map(get_indent_size, lines))
+ indent_size = get_likely_indent_size(indent_sizes)
+ comment_indent_size = get_indent_size(signature_line) + indent_size
+ lines = (
+ lines[:line_start]
+ + [
+ f"{' '*comment_indent_size+comment_prefix[language.lower()]}TODO: replace with your code here"
+ ]
+ + ([''] * 2)
+ + lines[line_end:]
+ )
+ first_line_after_removed_index = line_start
+ while len(
+ lines[first_line_after_removed_index].strip()
+ ) == 0 and first_line_after_removed_index < len(lines):
+ first_line_after_removed_index += 1
+
+ first_line_after_removed = lines[first_line_after_removed_index]
+ print('FIRST LINE AFTER REMOVED: ', first_line_after_removed)
+ with open('/testing_files/first_line_after_removed.txt', 'w') as f:
+ f.write(first_line_after_removed)
+
+ with open(target_filepath, 'w') as f:
+ f.write('\n'.join(lines))
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--target_filepath', type=str, required=True)
+ parser.add_argument('--line_start', type=int, required=True)
+ parser.add_argument('--line_end', type=int, required=True)
+ parser.add_argument('--language', type=str, required=True)
+ args = parser.parse_args()
+ remove_code(args.target_filepath, args.line_start, args.line_end, args.language)
diff --git a/evaluation/biocoder/utils.py b/evaluation/biocoder/utils.py
new file mode 100644
index 0000000000..2376d61041
--- /dev/null
+++ b/evaluation/biocoder/utils.py
@@ -0,0 +1,36 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class BiocoderData:
+ instance_id: str
+ filePath: str
+ numLines: int
+ lineStart: int
+ lineEnd: int
+ signature: str
+ comment: str
+ content: str
+ repository: str
+ promptSummaryOnly: str
+ contextCode: str
+ goldenCode: str
+ test_case_id: str
+ language: str
+
+ def to_dict(self):
+ return {
+ 'filePath': self.filePath,
+ 'numLines': self.numLines,
+ 'lineStart': self.lineStart,
+ 'lineEnd': self.lineEnd,
+ 'signature': self.signature,
+ 'comment': self.comment,
+ 'content': self.content,
+ 'repository': self.repository,
+ 'promptSummaryOnly': self.promptSummaryOnly,
+ 'contextCode': self.contextCode,
+ 'goldenCode': self.goldenCode,
+ 'test_case_id': self.test_case_id,
+ 'language': self.language,
+ }
diff --git a/evaluation/bird/README.md b/evaluation/bird/README.md
index 05e0fd8021..072da010f8 100644
--- a/evaluation/bird/README.md
+++ b/evaluation/bird/README.md
@@ -2,43 +2,14 @@
Implements evaluation of agents on BIRD introduced in [Can LLM Already Serve as A Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs](https://arxiv.org/abs/2305.03111). Please see [here](https://bird-bench.github.io/) for the reference implementation used in the paper.
-## Setup Environment
+## Setup Environment and LLM Configuration
-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"
-
-[sandbox]
-enable_auto_lint = true
-
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on Bird
```bash
-./evaluation/bird/scripts/run_infer.sh eval_gpt4_1106_preview [model_config] [git-version]
+./evaluation/bird/scripts/run_infer.sh [model_config] [git-version]
```
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
diff --git a/evaluation/bird/run_infer.py b/evaluation/bird/run_infer.py
index d370f6780e..70402c6a29 100644
--- a/evaluation/bird/run_infer.py
+++ b/evaluation/bird/run_infer.py
@@ -1,12 +1,12 @@
import asyncio
import json
-import logging
import os
import pathlib
import re
-import shutil
import sqlite3
import subprocess
+import zipfile
+from typing import Any
import pandas as pd
from datasets import load_dataset
@@ -15,20 +15,24 @@ from tqdm import tqdm
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.events.action import MessageAction
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import CmdRunAction, MessageAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
def codeact_user_response(state: State) -> str:
@@ -62,6 +66,27 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
def execute_sql(db_path, gen_sql, gold_sql):
"""Execute the generated SQL and the ground truth SQL and compare the results."""
with sqlite3.connect(db_path) as conn:
@@ -76,12 +101,213 @@ def execute_sql(db_path, gen_sql, gold_sql):
return res
-def get_test_result(instance, path, timeout=30):
+LOCAL_DATASET_PATH = os.path.join(os.path.dirname(__file__), 'data')
+
+
+def load_bird():
+ """Main function to handle the flow of downloading, processing, and loading the bird dataset."""
+
+ def _download_bird():
+ """Downloads and extracts the bird dataset from a specified URL into a local directory."""
+ devset_path = os.path.join(LOCAL_DATASET_PATH, 'dev')
+ if not os.path.exists(devset_path):
+ logger.info(
+ f'{LOCAL_DATASET_PATH} folder does not exist, starting download and extraction...'
+ )
+ os.makedirs(LOCAL_DATASET_PATH, exist_ok=True)
+
+ download_url = 'https://bird-bench.oss-cn-beijing.aliyuncs.com/dev.zip'
+ download_path = os.path.join(LOCAL_DATASET_PATH, 'dev.zip')
+ if not os.path.exists(download_path):
+ logger.info('Start Downloading...')
+ subprocess.run(['wget', download_url, '-O', download_path])
+ logger.info('Download completed.')
+
+ devset_path = os.path.join(LOCAL_DATASET_PATH, 'dev')
+ if not os.path.exists(devset_path):
+ logger.info('Start Extracting...')
+ os.makedirs(devset_path, exist_ok=True)
+ with zipfile.ZipFile(download_path, 'r') as zip_ref:
+ zip_ref.extractall(devset_path)
+ # move everything in 'dev_20240627' to the root folder
+ for file in os.listdir(os.path.join(devset_path, 'dev_20240627')):
+ os.rename(
+ os.path.join(devset_path, 'dev_20240627', file),
+ os.path.join(devset_path, file),
+ )
+ os.rmdir(os.path.join(devset_path, 'dev_20240627'))
+ logger.info('Extraction completed.')
+
+ # extract databases
+ database_path = os.path.join(devset_path, 'dev_databases.zip')
+ assert os.path.exists(database_path)
+ logger.info('Start Extracting...')
+ with zipfile.ZipFile(database_path, 'r') as zip_ref:
+ zip_ref.extractall(devset_path)
+ logger.info('Extraction completed.')
+ else:
+ logger.info(f'{LOCAL_DATASET_PATH} folder already exists.')
+ return devset_path
+
+ def _extract_create_table_prompt(db_path, limit_value=0):
+ """Generates a SQL prompt with CREATE TABLE statements and sample data from the database."""
+ table_query = "SELECT * FROM sqlite_master WHERE type='table';"
+ tables = sqlite3.connect(db_path).cursor().execute(table_query).fetchall()
+ prompt = ''
+ for table in tables:
+ table_name = table[1]
+ create_table_statement = table[-1]
+
+ table_info_query = f'PRAGMA table_info(`{table_name}`);'
+ top_k_row_query = f'SELECT * FROM {table_name} LIMIT {limit_value};'
+ try:
+ headers = [
+ x[1]
+ for x in sqlite3.connect(db_path)
+ .cursor()
+ .execute(table_info_query)
+ .fetchall()
+ ]
+ except Exception:
+ logger.error(f'Error Connection: {table_info_query}, {top_k_row_query}')
+ exit(0)
+
+ prompt += create_table_statement + ';\n'
+ if limit_value > 0:
+ top_k_rows = (
+ sqlite3.connect(db_path)
+ .cursor()
+ .execute(top_k_row_query)
+ .fetchall()
+ )
+ prompt += (
+ f"/*\n3 example rows:\n{top_k_row_query}\n{' '.join(headers)}\n"
+ )
+ for row in top_k_rows:
+ row = [str(x) for x in row]
+ row = [x if x is not None else '' for x in row]
+ prompt += ' '.join(row) + '\n'
+ prompt += '*/\n'
+ prompt += '\n'
+ return prompt
+
+ def _create_prompt(e, database_path):
+ """Create a prompt for the given example"""
+ db_id = e['db_id']
+ db_path = pathlib.Path(database_path) / db_id / f'{db_id}.sqlite'
+
+ # Extract the CREATE TABLE statements and sample data from the database
+ prompt = _extract_create_table_prompt(db_path)
+ prompt += f"-- External Knowledge: {e['evidence']}\n\n"
+ prompt += '-- Using valid SQLite and understanding External Knowledge, answer the following questions for the tables provided above.\n\n'
+ prompt += '-- Using valid SQLite, answer the following questions for the tables provided above.\n'
+ prompt += f"Question: {e['question']}\n"
+
+ return prompt
+
+ def _process_bird(dataset_path):
+ """Processes the raw bird dataset into a structured format and saves it as JSON."""
+ processed_path = os.path.join(LOCAL_DATASET_PATH, 'dev', 'processed_dev.json')
+ if not os.path.exists(processed_path):
+ logger.info(
+ f'{processed_path} folder does not exist, starting processing...'
+ )
+ raw_data_path = os.path.join(LOCAL_DATASET_PATH, 'dev', 'dev.json')
+ database_path = os.path.join(LOCAL_DATASET_PATH, 'dev', 'dev_databases')
+ processed_data = []
+ with pathlib.Path(raw_data_path).open('r') as f:
+ data = json.load(f)
+ for e in tqdm(data):
+ item = {
+ 'instance_id': f'{len(processed_data)}',
+ 'db_path': os.path.join(
+ database_path, e['db_id'], f"{e['db_id']}.sqlite"
+ ),
+ 'db_id': e['db_id'],
+ 'instruction': _create_prompt(e, database_path),
+ 'SQL': e['SQL'],
+ }
+ processed_data.append(item)
+
+ with pathlib.Path(processed_path).open('w') as f:
+ json.dump(processed_data, f, indent=2)
+ logger.info(f'Processed data saved to {processed_path}')
+ else:
+ logger.info(f'{processed_path} folder already exists.')
+ bird_dataset = load_dataset('json', data_files={'test': processed_path})
+ return bird_dataset
+
+ raw_dataset_path = _download_bird()
+ bird_dataset = _process_bird(raw_dataset_path)
+ return bird_dataset
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Copy the database to the workspace
+ db_file = os.path.join(
+ LOCAL_DATASET_PATH,
+ 'dev',
+ 'dev_databases',
+ instance.db_id,
+ f'{instance.db_id}.sqlite',
+ )
+ await runtime.copy_to(db_file, '/workspace')
+
+ # Check the database is copied
+ action = CmdRunAction(
+ command='cd /workspace && ls -l',
+ keep_prompt=False,
+ )
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+ assert f'{instance.db_id}.sqlite' in obs.content
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def complete_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
+ timeout = 30
+
test_result = {'result': {}, 'metadata': {}}
# Read the generated python file
- with open(path, 'r') as f:
- gen_file = f.read()
+ instance_id = instance.instance_id.replace('/', '__')
+ path = os.path.join('/workspace', f'{instance_id}.py')
+
+ action = CmdRunAction(
+ command=f'cat {path}',
+ keep_prompt=False,
+ )
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+
+ if obs.exit_code != 0:
+ test_result['result'] = {'passed': 0, 'status': 'error'}
+ return test_result
+
+ gen_file = obs.content.strip().replace('\r\n', '\n')
# Extract the SQL from the python file
gen_sql = ''
@@ -96,7 +322,13 @@ def get_test_result(instance, path, timeout=30):
# Execute the SQL
try:
res = func_timeout(
- timeout, execute_sql, args=(instance.db_path, gen_sql, gold_sql)
+ timeout,
+ execute_sql,
+ args=(
+ instance.db_path,
+ gen_sql,
+ gold_sql,
+ ),
)
status = 'success'
except FunctionTimedOut:
@@ -114,68 +346,28 @@ def get_test_result(instance, path, timeout=30):
'gen_sql': gen_sql,
'gold_sql': gold_sql,
}
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
return test_result
-def process_instance(
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- workspace_mount_path = os.path.join(
- config.workspace_mount_path, 'bird_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)
-
- # reset workspace to config
- config.workspace_mount_path = workspace_mount_path
-
- # Copy the database to the workspace
- db_root = os.path.join(
- config.workspace_base, 'evaluation_bird/dev/dev_databases', instance.db_id
- )
- target_path = os.path.join(workspace_mount_path, f'{instance.db_id}')
- if not os.path.exists(target_path):
- logger.info(f'Copying database from {db_root} to {target_path}...')
- shutil.copytree(db_root, target_path)
-
- # Set up the database path
- database_path = os.path.join(instance.db_id, f'{instance.db_id}.sqlite')
-
+) -> EvalOutput:
+ config = get_config(metadata)
# use session id for concurrent evaluation
- sid = instance.task_id.replace('/', '__')
+ instance_id = instance.instance_id.replace('/', '__')
# Set up the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir,
- 'logs',
- f'instance_{sid}.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}')
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance_id}.')
# Create file with BIRD instance
+ database_path = os.path.join('/workspace', f'{instance.db_id}.sqlite')
statements = f"""
import sqlite3
def execute_sql(db_path, sql):
@@ -192,12 +384,12 @@ def process_instance(
result = execute_sql(db_path, sql)
print(result)
"""
- path = os.path.join(config.workspace_mount_path, f'{sid}.py')
+
instruction = (
f'You are a SQL expert and need to complete the following text-to-SQL tasks.'
f'\n\n{instance.instruction}\n\n'
'Please write the SQL in one line without line breaks.'
- f'And write a new python file named {sid}.py to call the SQL you wrote.'
+ f'And write a new python file named {instance_id}.py to call the SQL you wrote.'
'You need to follow the code template below:'
f'\n\n{statements}\n\n'
'Environment has been set up for you to start working.'
@@ -208,23 +400,21 @@ def process_instance(
'You SHOULD INCLUDE PROPER INDENTATION in your edit commands.\n'
)
# NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
+
+ runtime = await create_runtime(config, sid=instance_id)
+ await initialize_runtime(runtime, instance)
+
# Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
- agent.__class__.__name__
- ],
- sid=sid,
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
+ runtime=runtime,
)
# ======= Attempt to evaluate the agent's edits =======
- test_result = get_test_result(instance, path)
+ test_result = await complete_runtime(runtime, instance)
# If you are working on some simpler 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.
@@ -238,162 +428,43 @@ def process_instance(
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
- output = {
- 'task_id': instance.task_id,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': test_result,
- }
+ output = EvalOutput(
+ instance_id=instance.instance_id,
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result=test_result,
+ )
return output
-def load_bird():
- """Main function to handle the flow of downloading, processing, and loading the bird dataset."""
- raw_dataset_path = download_bird()
- bird_dataset = process_bird(raw_dataset_path)
- return bird_dataset
-
-
-def download_bird():
- """Downloads and extracts the bird dataset from a specified URL into a local directory."""
- dataset_path = os.path.join(config.workspace_base, 'evaluation_bird')
- devset_path = os.path.join(dataset_path, 'dev')
- if not os.path.exists(dataset_path):
- logger.info(
- f'{dataset_path} folder does not exist, starting download and extraction...'
- )
- os.makedirs(dataset_path, exist_ok=True)
- download_url = 'https://bird-bench.oss-cn-beijing.aliyuncs.com/dev.zip'
- download_path = os.path.join(dataset_path, 'dev.zip')
- logger.info('Start Downloading...')
- subprocess.run(['wget', download_url, '-O', download_path])
- logger.info('Download completed.')
- logger.info('Start Extracting...')
- subprocess.run(['unzip', download_path, '-d', dataset_path])
- # extract databases
- devset_path = os.path.join(dataset_path, 'dev')
- database_path = os.path.join(devset_path, 'dev_databases.zip')
- subprocess.run(['unzip', database_path, '-d', devset_path])
- logger.info('Extraction completed.')
- else:
- logger.info(f'{dataset_path} folder already exists.')
- return devset_path
-
-
-def process_bird(dataset_path):
- """Processes the raw bird dataset into a structured format and saves it as JSON."""
- processed_path = os.path.join(dataset_path, 'processed_dev.json')
- if not os.path.exists(processed_path):
- logger.info(f'{processed_path} folder does not exist, starting processing...')
- raw_data_path = os.path.join(dataset_path, 'dev.json')
- database_path = os.path.join(dataset_path, 'dev_databases')
- processed_data = []
- with pathlib.Path(raw_data_path).open('r') as f:
- data = json.load(f)
- for e in tqdm(data):
- item = {
- 'task_id': f'{len(processed_data)}',
- 'db_path': os.path.join(
- database_path, e['db_id'], f"{e['db_id']}.sqlite"
- ),
- 'db_id': e['db_id'],
- 'instruction': create_prompt(e, database_path),
- 'SQL': e['SQL'],
- }
- processed_data.append(item)
-
- with pathlib.Path(processed_path).open('w') as f:
- json.dump(processed_data, f, indent=2)
- logger.info(f'Processed data saved to {processed_path}')
- else:
- logger.info(f'{processed_path} folder already exists.')
- bird_dataset = load_dataset('json', data_files={'test': processed_path})
- return bird_dataset
-
-
-def extract_create_table_prompt(db_path, limit_value=0):
- """Generates a SQL prompt with CREATE TABLE statements and sample data from the database."""
- table_query = "SELECT * FROM sqlite_master WHERE type='table';"
- tables = sqlite3.connect(db_path).cursor().execute(table_query).fetchall()
- prompt = ''
- for table in tables:
- table_name = table[1]
- create_table_statement = table[-1]
-
- table_info_query = f'PRAGMA table_info(`{table_name}`);'
- top_k_row_query = f'SELECT * FROM {table_name} LIMIT {limit_value};'
- try:
- headers = [
- x[1]
- for x in sqlite3.connect(db_path)
- .cursor()
- .execute(table_info_query)
- .fetchall()
- ]
- except Exception:
- logger.error(f'Error Connection: {table_info_query}, {top_k_row_query}')
- exit(0)
-
- prompt += create_table_statement + ';\n'
- if limit_value > 0:
- top_k_rows = (
- sqlite3.connect(db_path).cursor().execute(top_k_row_query).fetchall()
- )
- prompt += (
- f"/*\n3 example rows:\n{top_k_row_query}\n{' '.join(headers)}\n"
- )
- for row in top_k_rows:
- row = [str(x) for x in row]
- row = [x if x is not None else '' for x in row]
- prompt += ' '.join(row) + '\n'
- prompt += '*/\n'
- prompt += '\n'
- return prompt
-
-
-def create_prompt(e, database_path):
- """Create a prompt for the given example"""
- db_id = e['db_id']
- db_path = pathlib.Path(database_path) / db_id / f'{db_id}.sqlite'
-
- # Extract the CREATE TABLE statements and sample data from the database
- prompt = extract_create_table_prompt(db_path)
- prompt += f"-- External Knowledge: {e['evidence']}\n\n"
- prompt += '-- Using valid SQLite and understanding External Knowledge, answer the following questions for the tables provided above.\n\n'
- prompt += '-- Using valid SQLite, answer the following questions for the tables provided above.\n'
- prompt += f"Question: {e['question']}\n"
-
- return prompt
-
-
if __name__ == '__main__':
- id_column = 'task_id'
args = parse_arguments()
bird_dataset = load_bird()
dataset = bird_dataset['test'].to_pandas()
+ dataset.rename(columns={'task_id': 'instance_id'}, inplace=True)
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ 'BIRD',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
+ instances = prepare_dataset(dataset, output_file, args.eval_n_limit)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/bird/scripts/run_infer.sh b/evaluation/bird/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/browsing_delegation/README.md b/evaluation/browsing_delegation/README.md
index 495ee0bd6b..fe71dacb88 100644
--- a/evaluation/browsing_delegation/README.md
+++ b/evaluation/browsing_delegation/README.md
@@ -5,30 +5,9 @@ Some of OpenDevin's agent supports agent delegation action, for example, CodeAct
This evaluation tests whether CodeActAgent can correctly delegate the instruction from WebArena and MiniWob benchmark to the BrowsingAgent.
If so, the browsing performance upper-bound of CodeActAgent will be the performance of BrowsingAgent.
+## Setup Environment and LLM Configuration
-## Setup Environment
-
-Please follow [this document](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to set up a local development 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
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview_llm]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model_llm]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference
diff --git a/evaluation/browsing_delegation/run_infer.py b/evaluation/browsing_delegation/run_infer.py
index 02e529fe0d..983d753c8e 100644
--- a/evaluation/browsing_delegation/run_infer.py
+++ b/evaluation/browsing_delegation/run_infer.py
@@ -1,5 +1,4 @@
import asyncio
-import logging
import os
import re
@@ -9,56 +8,61 @@ from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
# Only CodeActAgent can delegate to BrowsingAgent
SUPPORTED_AGENT_CLS = {'CodeActAgent'}
-def process_instance(
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ assert (
+ metadata.max_iterations == 1
+ ), 'max_iterations must be 1 for browsing delegation evaluation.'
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=False,
+ use_host_network=False,
+ ),
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- env_id = instance.instance_id
+) -> EvalOutput:
+ config = get_config(metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{env_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 {env_id}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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)
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
- logger.info(f'Starting evaluation for instance {env_id}.')
+ logger.info(f'Starting evaluation for instance {instance.instance_id}.')
instruction = (
f'You can delegate browsing tasks to a browser agent. '
@@ -67,21 +71,14 @@ def process_instance(
f'NOTE: You should copy the "query" as is into the
tag. DO NOT change ANYTHING in the query.'
)
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- sid=env_id,
- )
+ runtime = await create_runtime(config, sid=instance.instance_id)
+
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
)
- # ======= Attempt to evaluate the agent's environment impact =======
-
- # If you are working on some simpler 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.')
@@ -115,20 +112,19 @@ def process_instance(
result['is_exact_match'] = is_exact_match
# Save the output
- output = {
- 'instance_id': env_id,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': {
+ output = EvalOutput(
+ instance_id=instance.instance_id,
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
'query': instance.instruction,
'action': last_delegate_action,
'result': result,
},
- }
-
+ )
return output
@@ -138,9 +134,13 @@ if __name__ == '__main__':
dataset = load_dataset('OpenDevin/eval-browsing-instructions')
dataset = dataset['train'].to_pandas()
assert dataset.columns.tolist() == ['instance_id', 'instruction']
- id_column = 'instance_id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
@@ -150,18 +150,20 @@ if __name__ == '__main__':
args.eval_note,
args.eval_output_dir,
)
+
if metadata.agent_class not in SUPPORTED_AGENT_CLS:
raise ValueError(
f'Agent class {metadata.agent_class} not supported with AgentDelegation.'
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances = prepare_dataset(dataset, output_file, args.eval_n_limit)
+ asyncio.run(
+ run_evaluation(
+ instances,
+ metadata,
+ output_file,
+ args.eval_num_workers,
+ process_instance,
+ )
)
diff --git a/evaluation/gaia/README.md b/evaluation/gaia/README.md
index 6cf911c954..cd7e7c9677 100644
--- a/evaluation/gaia/README.md
+++ b/evaluation/gaia/README.md
@@ -2,9 +2,9 @@
This folder contains evaluation harness for evaluating agents on the [GAIA benchmark](https://arxiv.org/abs/2311.12983).
-## Configure OpenDevin and your LLM
+## Setup Environment and LLM Configuration
-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.
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run the evaluation
We are using the GAIA dataset hosted on [Hugging Face](https://huggingface.co/datasets/gaia-benchmark/GAIA).
diff --git a/evaluation/gaia/run_infer.py b/evaluation/gaia/run_infer.py
index cfbfb32eb9..7a791c4aa7 100644
--- a/evaluation/gaia/run_infer.py
+++ b/evaluation/gaia/run_infer.py
@@ -1,10 +1,7 @@
import asyncio
-import logging
+import functools
import os
-import pathlib
import re
-import shutil
-from functools import partial
import huggingface_hub
import pandas as pd
@@ -13,28 +10,31 @@ from datasets import load_dataset
from evaluation.gaia.scorer import question_scorer
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.events.action import CmdRunAction, MessageAction
-from opendevin.llm.llm import LLM
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import AgentFinishAction, CmdRunAction, MessageAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
-config = load_app_config()
-
-DATASET_CACHE_DIR = '~/.cache/open-devin/evals/gaia'
-DATASET_CACHE_DIR = os.path.expanduser(DATASET_CACHE_DIR)
+DATASET_CACHE_DIR = os.path.join(os.path.dirname(__file__), 'data')
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
- 'CodeActAgent': partial(codeact_user_response, encapsulate_solution=True),
+ 'CodeActAgent': functools.partial(codeact_user_response, encapsulate_solution=True),
}
AGENT_CLS_TO_INST_SUFFIX = {
@@ -42,151 +42,174 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
-def process_instance(
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ if instance['file_name'] != '':
+ # if this question comes with a file, we need to save it to the workspace
+ assert metadata.data_split is not None
+ src_file = os.path.join(
+ DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
+ )
+ assert os.path.exists(src_file)
+ dest_file = os.path.join('/workspace', instance['file_name'])
+ await runtime.copy_to(src_file, dest_file)
+
+ # rename to file.extension_name
+ extension_name = instance['file_name'].split('.')[-1]
+ action = CmdRunAction(
+ command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- # 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
+) -> EvalOutput:
+ config = get_config(metadata)
- try:
- 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 parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance['instance_id'], log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance["instance_id"]}.')
- # Setup the logger properly, so you can run multi-processing to parallelize 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)
+ if instance['file_name'] != '':
+ extension_name = instance['file_name'].split('.')[-1]
+ dest_file = os.path.join('/workspace', f'file.{extension_name}')
+ else:
+ dest_file = None
- 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
- assert metadata.data_split is not None
- 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]}"
- # 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 and .\n'
+ instruction += (
+ 'For example: The answer to the question is 42 .\n'
+ )
+ # NOTE: You can actually set slightly different instruction for different agents
+ instruction += AGENT_CLS_TO_INST_SUFFIX.get(metadata.agent_class, '')
+ logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
- 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 and .\n'
- instruction += (
- 'For example: The answer to the question is 42 .\n'
- )
- # NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX.get(agent.__class__.__name__, '')
- logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
+ runtime = await create_runtime(config, sid=instance['instance_id'])
+ await initialize_runtime(runtime, instance)
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
- agent.__class__.__name__
- ],
- sid=instance['task_id'],
- )
- )
- # ======= Attempt to evaluate the agent's edits =======
- # If you are working on simpler 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.
+ # Here's how you can run the agent (similar to the `main` function) and get the final task state
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
+ )
+ # ======= Attempt to evaluate the agent's edits =======
+ # If you are working on simpler 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.')
+ if state is None:
+ raise ValueError('State should not be None.')
- model_answer_raw = ''
-
- # get the last message or thought from the agent
- for event in state.history.get_events(reverse=True):
- if isinstance(event, CmdRunAction) and event.source == 'agent':
+ model_answer_raw = ''
+ # get the last message or thought from the agent
+ for event in state.history.get_events(reverse=True):
+ if event.source == 'agent':
+ if isinstance(event, AgentFinishAction):
model_answer_raw = event.thought
- elif isinstance(event, MessageAction) and event.source == 'agent':
+ break
+ elif isinstance(event, CmdRunAction):
+ model_answer_raw = event.thought
+ break
+ elif isinstance(event, MessageAction):
model_answer_raw = event.content
+ break
- # attempt to parse model_answer
- model_answer = re.findall(r'(.*?)', 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]
+ # attempt to parse model_answer
+ model_answer = re.findall(r'(.*?)', 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'],
- }
- metrics = state.metrics.get() if state.metrics else None
+ 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'],
+ }
+ metrics = state.metrics.get() if state.metrics else None
- # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
- # for compatibility with the existing output format, we can remake the pairs here
- # remove when it becomes unnecessary
- histories = state.history.compatibility_for_eval_history_pairs()
+ # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+ # for compatibility with the existing output format, we can remake the pairs here
+ # remove when it becomes unnecessary
+ histories = state.history.compatibility_for_eval_history_pairs()
- # Save the output
- output = {
- 'instance_id': instance['task_id'],
- 'instance': instance,
- 'instruction': instance['Question'],
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': test_result,
- }
- except Exception:
- logger.error('Process instance failed')
- raise
- finally:
- config.workspace_mount_path = old_workspace_mount_path
+ # Save the output
+ output = EvalOutput(
+ instance_id=instance['instance_id'],
+ instance=instance.to_dict(),
+ instruction=instance['Question'],
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result=test_result,
+ )
return output
@@ -197,13 +220,19 @@ if __name__ == '__main__':
type=str,
help='gaia level to evaluate, eg. 2023_level1',
)
+ parser.add_argument(
+ '--data-split',
+ type=str,
+ help='data split to evaluate, eg. test',
+ default='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}')
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config=llm_config,
@@ -222,20 +251,18 @@ if __name__ == '__main__':
repo_type='dataset',
local_dir=DATASET_CACHE_DIR,
)
- gaia_tests = dataset[metadata.data_split]
+ gaia_tests = dataset[metadata.data_split].to_pandas()
+ gaia_tests.rename(columns={'task_id': 'instance_id'}, inplace=True)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- prepared_dataset = prepare_dataset(
- gaia_tests.to_pandas(), output_file, args.eval_n_limit, 'task_id'
- )
+ prepared_dataset = prepare_dataset(gaia_tests, output_file, args.eval_n_limit)
- agent = Agent.get_cls(args.agent_cls)(llm=LLM(config.llm))
-
- run_evaluation(
- dataset=prepared_dataset,
- metadata=metadata,
- output_file=output_file,
- num_workers=args.eval_num_workers,
- process_instance_func=process_instance,
- id_column='task_id',
+ asyncio.run(
+ run_evaluation(
+ dataset=prepared_dataset,
+ metadata=metadata,
+ output_file=output_file,
+ num_workers=args.eval_num_workers,
+ process_instance_func=process_instance,
+ )
)
diff --git a/evaluation/gaia/scripts/run_infer.sh b/evaluation/gaia/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/gorilla/README.md b/evaluation/gorilla/README.md
index c5da3ad453..106a83a251 100644
--- a/evaluation/gorilla/README.md
+++ b/evaluation/gorilla/README.md
@@ -2,20 +2,16 @@
This folder contains evaluation harness we built on top of the original [Gorilla APIBench](https://github.com/ShishirPatil/gorilla) ([paper](https://arxiv.org/pdf/2305.15334)).
-## Setup Environment
+## Setup Environment and LLM Configuration
-Please follow [this document](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to setup local development environment for OpenDevin.
-
-## Configure OpenDevin and your LLM
-
-Run `make setup-config` to set up the `config.toml` file if it does not exist at the root of the workspace.
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on APIBench Instances
Make sure your Docker daemon is running, then run this bash script:
```bash
-bash evaluation/gorilla/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [hubs]
+./evaluation/gorilla/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [hubs]
```
where `model_config` is mandatory, while all other arguments are optional.
@@ -39,5 +35,5 @@ Note: in order to use `eval_limit`, you must also set `agent`; in order to use `
For example,
```bash
-bash evaluation/gorilla/scripts/run_infer.sh llm 0.6.2 CodeActAgent 10 th
+./evaluation/gorilla/scripts/run_infer.sh llm 0.6.2 CodeActAgent 10 th
```
diff --git a/evaluation/gorilla/run_infer.py b/evaluation/gorilla/run_infer.py
index ea68d445a1..8aad9d9da9 100644
--- a/evaluation/gorilla/run_infer.py
+++ b/evaluation/gorilla/run_infer.py
@@ -1,59 +1,28 @@
import asyncio
import json
-import logging
-import multiprocessing as mp
import os
-import pathlib
-import subprocess
-import time
-from concurrent.futures import ProcessPoolExecutor
-from tqdm import tqdm
+import pandas as pd
-from opendevin.controller.agent import Agent
+from evaluation.gorilla.utils import encode_question, get_data_for_hub
+from evaluation.utils.shared import (
+ EvalMetadata,
+ EvalOutput,
+ codeact_user_response,
+ make_metadata,
+ prepare_dataset,
+ reset_logger_for_multiprocessing,
+ run_evaluation,
+)
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.events.action import MessageAction
-from opendevin.llm.llm import LLM
-
-from .utils import encode_question, get_data
-
-config = load_app_config()
-
-
-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:
- msg = (
- #'Please continue working on the task on whatever approach you think is suitable.\n'
- 'Please run the following command: exit .\n'
- #'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP OR USE THE INTERNET TO SOLVE THIS TASK.\n'
- )
-
- # check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
- if state.history:
- user_msgs = [
- event
- for event in state.history.get_events()
- if isinstance(event, MessageAction) and event.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: exit .\n'
- )
- return msg
-
+from opendevin.core.main import create_runtime, run_controller
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -64,105 +33,95 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
-def process_instance(agent, question_id, question, 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
- try:
- 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
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
- # 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_{question_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 {question_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}')
- # Prepare instruction
- instruction = encode_question(question, metadata['hub'])
- 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[agent.__class__.__name__]
- # logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
+async def process_instance(
+ instance: pd.Series,
+ metadata: EvalMetadata,
+ reset_logger: bool = True,
+) -> EvalOutput:
+ config = get_config(metadata)
+ instance_id = instance['question_id']
+ question = instance['question']
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
- agent.__class__.__name__
- ),
- sid=question_id,
- )
- )
- # ======= Attempt to evaluate the agent's edits =======
- # If you are working on simpler 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.
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance_id}.')
- if state is None:
- raise ValueError('State should not be None.')
+ # Prepare instruction
+ instruction = encode_question(question, instance['hub'])
+ 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[metadata.agent_class]
+ # logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
- # retrieve the last message from the agent
- model_answer_raw = state.history.get_last_agent_message()
+ # Here's how you can run the agent (similar to the `main` function) and get the final task state
+ runtime = await create_runtime(config, sid=instance_id)
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
+ metadata.agent_class
+ ),
+ )
+ # ======= Attempt to evaluate the agent's edits =======
+ # If you are working on simpler 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.
- # attempt to parse model_answer
- _, _, ast_eval = get_data(metadata['hub'])
- correct, hallucination = ast_eval(question_id, model_answer_raw)
- metrics = state.metrics.get() if state.metrics else None
- logger.info(
- f'Final message: {model_answer_raw} | Correctness: {correct} | Hallucination: {hallucination}'
- )
+ if state is None:
+ raise ValueError('State should not be None.')
- # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
- # for compatibility with the existing output format, we can remake the pairs here
- # remove when it becomes unnecessary
- histories = state.history.compatibility_for_eval_history_pairs()
+ # retrieve the last message from the agent
+ model_answer_raw = state.history.get_last_agent_message()
- # Save the output
- output = {
- 'question_id': question_id,
+ # attempt to parse model_answer
+ ast_eval_fn = instance['ast_eval']
+ correct, hallucination = ast_eval_fn(instance_id, model_answer_raw)
+ metrics = state.metrics.get() if state.metrics else None
+ logger.info(
+ f'Final message: {model_answer_raw} | Correctness: {correct} | Hallucination: {hallucination}'
+ )
+
+ # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+ # for compatibility with the existing output format, we can remake the pairs here
+ # remove when it becomes unnecessary
+ histories = state.history.compatibility_for_eval_history_pairs()
+
+ output = EvalOutput(
+ instance_id=instance_id,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
'text': model_answer_raw,
'correct': correct,
'hallucination': hallucination,
- 'answer_id': 'None',
- 'model_id': metadata['model_name'],
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- }
- except Exception:
- logger.error('Process instance failed')
- raise
- finally:
- config.workspace_mount_path = old_workspace_mount_path
+ },
+ )
return output
@@ -175,188 +134,62 @@ if __name__ == '__main__':
default='hf,torch,tf',
)
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}')
- # 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`
+ llm_config = None
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}')
- 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,
- 'gorilla',
- 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}')
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
- hubs = []
- if 'hf' in args.hubs:
- hubs.append('hf')
- if 'torch' in args.hubs or 'th' in args.hubs:
- hubs.append('torch')
- if 'tf' in args.hubs:
- hubs.append('tf')
- if hubs == []:
+ hubs = args.hubs.split(',')
+ if len(hubs) == 0:
raise ValueError('Please choose at least one from hf, torch, and tf for hubs.')
+ dfs = []
for hub in hubs:
logger.info(f'Evaluating APIBench {hub} test')
- questions, question_ids, ast_eval = get_data(hub)
+ df = get_data_for_hub(hub)
+ dfs.append(df)
+ dataset_df = pd.concat(dfs)
+ dataset_df.rename(columns={'question_id': 'instance_id'}, inplace=True)
- # TEST METADATA
- metadata = {
- 'hub': hub,
- '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, f'metadata_{hub}.json'), 'w') as f:
- json.dump(metadata, f)
+ metadata = make_metadata(
+ llm_config=llm_config,
+ dataset_name=f'gorilla-{hub}',
+ agent_class=args.agent_cls,
+ max_iterations=args.max_iterations,
+ eval_note=args.eval_note,
+ eval_output_dir=args.eval_output_dir,
+ data_split=args.data_split,
+ )
+ output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- # LIMIT EVALUATION
- eval_n_limit = args.eval_n_limit
- if eval_n_limit:
- questions = questions[: (eval_n_limit // len(hubs))]
- question_ids = question_ids[: (eval_n_limit // len(hubs))]
- logger.info(
- f'Limiting evaluation to a total of first {eval_n_limit} instances -> first {eval_n_limit//len(hubs)} instances per hub.'
- )
- output_file = os.path.join(eval_output_dir, f'output_{model_name}_{hub}.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)
- for i in range(len(question_ids)):
- if question_ids[i] == int(data['question_id']):
- finished_task_ids.add(data['question_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}.'
+ dataset = prepare_dataset(
+ dataset_df, output_file=output_file, eval_n_limit=args.eval_n_limit
+ )
+
+ asyncio.run(
+ run_evaluation(
+ dataset=dataset,
+ metadata=metadata,
+ output_file=output_file,
+ num_workers=args.eval_num_workers,
+ process_instance_func=process_instance,
)
- # =============================================
- # filter out finished instances
- new_questions = []
- new_question_ids = []
- for i in range(len(question_ids)):
- if question_ids[i] in finished_task_ids:
- logger.info(
- f'Skipping instance {question_ids[i]} as it is already finished.'
- )
- continue
- new_questions.append(questions[i])
- new_question_ids.append(question_ids[i])
+ )
- finished_task_number = len(finished_task_ids)
- questions = new_questions
- question_ids = new_question_ids
- logger.info(
- f'Finished instances: {finished_task_number}, Remaining instances: {len(question_ids)}'
- )
- # =============================================
- pbar = tqdm(total=len(question_ids))
-
- # This function tracks the progress AND write the output to a JSONL file
- def update_progress(future, pbar, output_fp, finished_task_ids):
- pbar.update(1)
- output = future.result()
- pbar.set_description(f'Instance {output["question_id"]}')
- pbar.set_postfix_str(f'Test Result: {output["correct"]}')
- logger.info(
- f'Finished evaluation for instance {output["question_id"]}: {output["correct"]}'
- )
- output_fp.write(json.dumps(output) + '\n')
- output_fp.flush()
- finished_task_ids.add(output['question_id'])
-
- # Create the agent
- agent = Agent.get_cls(agent_class)(llm=LLM(config.llm))
-
- # 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 i in range(len(question_ids)):
- try:
- question_id = question_ids[i]
- question = questions[i]
- future = executor.submit(
- process_instance,
- agent,
- question_id,
- question,
- metadata,
- reset_logger=bool(num_workers > 1),
- )
- future.add_done_callback(
- update_progress, pbar, output_fp, finished_task_ids
- )
- futures.append(future)
- except Exception:
- continue
-
- # Wait for all futures to complete
- for future in futures:
- try:
- future.result()
- except Exception:
- continue
- except KeyboardInterrupt:
- logger.info('KeyboardInterrupt received. Cleaning up...')
- cleanup()
-
- output_fp.close()
- total_correct = 0
- total_hallucination = 0
- output = []
- with open(output_file, 'r') as f:
- for line in f:
- data = json.loads(line)
- output.append(data)
- if int(data['question_id']) in finished_task_ids:
- if str(data['correct']).lower() == 'true':
- total_correct += 1
- if str(data['hallucination']).lower() == 'true':
- total_hallucination += 1
- # sort all output by question_id
- output = sorted(output, key=lambda x: x['question_id'])
- with open(output_file, 'w') as f:
- for dat in output:
- f.write(json.dumps(dat) + '\n')
- f.flush()
-
- logger.info(
- f'Evaluation finished for {hub}. Total: {len(question_ids)+finished_task_number}; Correct: {total_correct}; Hallucination: {total_hallucination}. Accuracy: {total_correct / (len(question_ids)+finished_task_number)}'
- )
+ # Read the output file and calculate the accuracy
+ total_correct = 0
+ total_hallucination = 0
+ output = []
+ with open(output_file, 'r') as f:
+ for line in f:
+ data = json.loads(line)
+ if data['test_result']['correct']:
+ total_correct += 1
+ if data['test_result']['hallucination']:
+ total_hallucination += 1
+ output.append(data)
+ logger.info(
+ f'Evaluation finished for {hub}. Total: {len(output)}; Correct: {total_correct}; Hallucination: {total_hallucination}. Accuracy: {total_correct / len(output)}'
+ )
diff --git a/evaluation/gorilla/scripts/run_infer.sh b/evaluation/gorilla/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/gorilla/utils.py b/evaluation/gorilla/utils.py
index 66f878976b..8c45cce58a 100644
--- a/evaluation/gorilla/utils.py
+++ b/evaluation/gorilla/utils.py
@@ -1,6 +1,8 @@
import json
+import os
from functools import partial
+import pandas as pd
import requests
from ast_eval_hf import ast_eval_hf, ast_parse
from ast_eval_tf import ast_eval_tf
@@ -48,48 +50,59 @@ def encode_question(question, api_name):
return prompts
-def get_data(hub):
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+os.makedirs(DATA_DIR, exist_ok=True)
+
+
+def fetch_data(url, filename):
+ cache_path = os.path.join(DATA_DIR, filename)
+ if os.path.exists(cache_path):
+ with open(cache_path, 'r') as f:
+ return f.read()
+ else:
+ response = requests.get(url)
+ if response.status_code == 200:
+ with open(cache_path, 'w') as f:
+ f.write(response.text)
+ return response.text
+ else:
+ raise Exception(f'Failed to fetch data from {url}')
+
+
+def get_data_for_hub(hub: str):
if hub == 'hf':
question_data = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/eval/eval-data/questions/huggingface/questions_huggingface_0_shot.jsonl'
api_dataset = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/data/api/huggingface_api.jsonl'
apibench = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/data/apibench/huggingface_eval.json'
ast_eval = ast_eval_hf
- if hub == 'torch':
+ elif hub == 'torch':
question_data = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/eval/eval-data/questions/torchhub/questions_torchhub_0_shot.jsonl'
api_dataset = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/data/api/torchhub_api.jsonl'
apibench = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/data/apibench/torchhub_eval.json'
ast_eval = ast_eval_th
- if hub == 'tf':
+ elif hub == 'tf':
question_data = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/eval/eval-data/questions/tensorflowhub/questions_tensorflowhub_0_shot.jsonl'
api_dataset = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/data/api/tensorflowhub_api.jsonl'
apibench = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/data/apibench/tensorflow_eval.json'
ast_eval = ast_eval_tf
- # get questions and question_ids
+ question_data = fetch_data(question_data, 'question_data.jsonl')
+ api_dataset = fetch_data(api_dataset, 'api_dataset.jsonl')
+ apibench = fetch_data(apibench, 'apibench.json')
+
+ # Parse question data
questions = []
question_ids = []
- question_data = requests.get(question_data)
- if question_data.status_code == 200:
- lines = question_data.text.splitlines()
- for line in lines:
- questions.append(json.loads(line)['text'])
- question_ids.append(json.loads(line)['question_id'])
+ for line in question_data.splitlines():
+ data = json.loads(line)
+ questions.append(data['text'])
+ question_ids.append(data['question_id'])
- # get the api datasest
- api_database = []
- api_dataset = requests.get(api_dataset)
- if api_dataset.status_code == 200:
- lines = api_dataset.text.splitlines()
- for line in lines:
- api_database.append(json.loads(line))
+ # Parse API dataset
+ api_database = [json.loads(line) for line in api_dataset.splitlines()]
- # get the question answer pair datasest
- qa_pairs = []
- apibench = requests.get(apibench)
- if apibench.status_code == 200:
- lines = apibench.text.splitlines()
- for line in lines:
- qa_pairs.append(json.loads(line)['api_data'])
+ # Parse question-answer pairs
+ qa_pairs = [json.loads(line)['api_data'] for line in apibench.splitlines()]
# Parse all apis to ast trees
ast_database = []
@@ -97,4 +110,15 @@ def get_data(hub):
ast_tree = ast_parse(data['api_call'])
ast_database.append(ast_tree)
ast_eval = partial(ast_eval, api_database, qa_pairs, ast_database)
- return questions, question_ids, ast_eval
+
+ return pd.DataFrame(
+ {
+ 'question_id': question_ids,
+ 'question': questions,
+ 'api_database': [api_database] * len(questions),
+ 'qa_pairs': [qa_pairs] * len(questions),
+ 'ast_database': [ast_database] * len(questions),
+ 'ast_eval': [ast_eval] * len(questions),
+ 'hub': [hub] * len(questions),
+ }
+ )
diff --git a/evaluation/gpqa/README.md b/evaluation/gpqa/README.md
index b8249be56d..7e1981c1f0 100644
--- a/evaluation/gpqa/README.md
+++ b/evaluation/gpqa/README.md
@@ -15,31 +15,9 @@ Further references:
- https://paperswithcode.com/dataset/gpqa
- https://github.com/idavidrein/gpqa
+## Setup Environment and LLM Configuration
-## 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 (you can copy from `config.template.toml`) if it does not exist at the root of the workspace.
-
-Add the following configurations:
-
-```toml
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_azure_openai_compatible_model]
-model = "AZURE_OPENAI_EXACT_DEPLOYMENT_MODEL_NAME"
-base_url = "AZURE_OPENAI_ENDPOINT"
-api_key = "AZURE_ENDPOINT_API_KEY"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on GPQA Benchmark
'gpqa_main', 'gqpa_diamond', 'gpqa_experts', 'gpqa_extended' -- data split options
@@ -55,8 +33,3 @@ like to evaluate. It could also be a release tag like `0.6.2`.
- `num_samples_eval`: Number of samples to evaluate (useful for testing and debugging).
- `data_split`: The data split to evaluate on. Must be one of `gpqa_main`, `gqpa_diamond`, `gpqa_experts`, `gpqa_extended`. Defaults to `gpqa_diamond` as done in the paper.
- `AgentClass`: The agent class to use for evaluation. Currently only supports `CodeActAgent` for CodeActAgent.
-
-
-## Benchmark Evaluation Results
-
-- [] TODO: Finish the evaluation run across the entire benchmark and compile results
diff --git a/evaluation/gpqa/run_infer.py b/evaluation/gpqa/run_infer.py
index 9cc8751f3e..32cee5fb7e 100644
--- a/evaluation/gpqa/run_infer.py
+++ b/evaluation/gpqa/run_infer.py
@@ -17,9 +17,7 @@ TODOs:
"""
import asyncio
-import logging
import os
-import pathlib
import random
import re
from typing import Callable
@@ -29,22 +27,27 @@ from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
- codeact_user_response,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.events.action import Action, AgentFinishAction, MessageAction
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import (
+ Action,
+ AgentFinishAction,
+ MessageAction,
+)
from opendevin.events.observation import Observation
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
ACTION_FORMAT = """
< AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
def gpqa_codeact_user_response(
state: State,
encapsulate_solution: bool = False,
@@ -68,11 +92,10 @@ def gpqa_codeact_user_response(
' exit \n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP TO SOLVE THIS TASK.\n'
)
-
return msg
-AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {'CodeActAgent': codeact_user_response}
+AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {'CodeActAgent': gpqa_codeact_user_response}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': '\n\n SUPER IMPORTANT: When you think you have solved the question, first report it back to the user in the requested format. Only once that is done, in the next turn, please run the following command: exit .\n'
@@ -146,57 +169,23 @@ def convert_instance_dict(instance):
return out_instance_dict
-def process_instance(
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- old_workspace_mount_path = config.workspace_mount_path
- old_workspace_base = config.workspace_base
- try:
- workspace_mount_path = os.path.join(
- config.workspace_mount_path, '_eval_workspace'
- )
- # create process-specific workspace dir
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+ config = get_config(metadata)
- # reset workspace to config
- config.workspace_base = workspace_mount_path
- config.workspace_mount_path = workspace_mount_path
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance['instance_id'], log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance["instance_id"]}.')
- # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
- if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{instance.instance_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.instance_id}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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)
- else:
- logger.info(f'Starting evaluation for instance {instance.instance_id}.')
-
- logger.info(f'Process-specific workspace mounted at {workspace_mount_path}')
-
- # ======= Run the agent on the instance =======
- # Prepare instruction for the agent using suggested format in gpqa codebase
- instruction = f"""
+ # ======= Run the agent on the instance =======
+ # Prepare instruction for the agent using suggested format in gpqa codebase
+ instruction = f"""
What is the correct answer to this question:\n
{instance['question']}\n
@@ -225,109 +214,98 @@ Again do not quit without reporting the answer first.
Ok now its time to start solving the question. Good luck!
"""
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
- agent.__class__.__name__
- ),
- sid=f'gptq_{str(instance.instance_id)}',
- )
- )
- assert state is not None, 'State should not be None.'
+ runtime = await create_runtime(config, sid=f'gptq_{str(instance.instance_id)}')
- # ======= Attempt to evaluate the agent's edits =======
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
+ metadata.agent_class
+ ),
+ )
+ assert state is not None, 'State should not be None.'
- question_choices = {
- 'A': instance['choices'][0],
- 'B': instance['choices'][1],
- 'C': instance['choices'][2],
- 'D': instance['choices'][3],
- }
- # get the final message from the state history (default to empty if not found)
- found_answers = {
- 'A': False,
- 'B': False,
- 'C': False,
- 'D': False,
- }
- for event in state.history.get_events(reverse=True):
- if (
- isinstance(event, AgentFinishAction)
- and event.source != 'user'
- and '< 0:
+ _selected = random.choice(found_options)
+ # if the final message is None, then the agent did not report the answer in the correct format
+ # so we randomly select one of the found options and compare it with the correct solution
+ test_result = _selected == instance.correct_solution
logger.info('#############################################')
- logger.info(f'Final message generated by the agent: {final_message}')
+ logger.info('Agent did not report the answer in the correct format.')
+ logger.info(f'Found options: {found_options}')
+ logger.info(f'Selected option: {_selected}')
logger.info('#############################################')
- # check if the model output matches the ground truth
- test_result = compare_answers(final_message, instance.correct_solution)
- if final_message is None and len(found_options) > 0:
- _selected = random.choice(found_options)
- # if the final message is None, then the agent did not report the answer in the correct format
- # so we randomly select one of the found options and compare it with the correct solution
- test_result = _selected == instance.correct_solution
- logger.info('#############################################')
- logger.info('Agent did not report the answer in the correct format.')
- logger.info(f'Found options: {found_options}')
- logger.info(f'Selected option: {_selected}')
- logger.info('#############################################')
+ logger.info('#############################################')
+ logger.info(f'Test result: {test_result}')
+ logger.info('#############################################')
- logger.info('#############################################')
- logger.info(f'Test result: {test_result}')
- logger.info('#############################################')
+ # If you are working on some simpler 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.')
- # If you are working on some simpler 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.')
+ metrics = state.metrics.get() if state.metrics else None
- metrics = state.metrics.get() if state.metrics else None
-
- # Save the output
- output = {
- 'task_id': instance.task_id,
- 'instance_id': instance.instance_id,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': state.history.compatibility_for_eval_history_pairs(),
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': {
- 'result': test_result,
- 'found_answers': found_answers,
- 'last_message': final_message,
- },
- }
-
- except Exception:
- logger.error('Process instance failed')
- raise
- finally:
- config.workspace_mount_path = old_workspace_mount_path
- config.workspace_base = old_workspace_base
+ # Save the output
+ output = EvalOutput(
+ instance_id=str(instance.instance_id),
+ instruction=instruction,
+ metadata=metadata,
+ history=state.history.compatibility_for_eval_history_pairs(),
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
+ 'result': test_result,
+ 'found_answers': found_answers,
+ 'last_message': final_message,
+ },
+ )
return output
@@ -343,8 +321,11 @@ if __name__ == '__main__':
)
args, _ = parser.parse_known_args()
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
# 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
@@ -355,8 +336,6 @@ if __name__ == '__main__':
gpqa_dataset = gpqa_dataset.to_pandas()
# Add a new column 'instance_id' with the index
gpqa_dataset['instance_id'] = gpqa_dataset.index
- gpqa_dataset['task_id'] = gpqa_dataset.index
- # gpqa_dataset = dataset['train'].to_pandas().sort_values(by='id').reset_index(drop=True)
if args.agent_cls != 'CodeActAgent':
raise ValueError(
@@ -374,15 +353,14 @@ if __name__ == '__main__':
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- prepared_dataset = prepare_dataset(
- gpqa_dataset, output_file, args.eval_n_limit, 'task_id'
- )
+ prepared_dataset = prepare_dataset(gpqa_dataset, output_file, args.eval_n_limit)
- run_evaluation(
- dataset=prepared_dataset,
- metadata=metadata,
- output_file=output_file,
- num_workers=args.eval_num_workers,
- process_instance_func=process_instance,
- id_column='task_id',
+ asyncio.run(
+ run_evaluation(
+ dataset=prepared_dataset,
+ metadata=metadata,
+ output_file=output_file,
+ num_workers=args.eval_num_workers,
+ process_instance_func=process_instance,
+ )
)
diff --git a/evaluation/humanevalfix/README.md b/evaluation/humanevalfix/README.md
index d231be1a61..5a056a54da 100644
--- a/evaluation/humanevalfix/README.md
+++ b/evaluation/humanevalfix/README.md
@@ -1,39 +1,10 @@
# 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.
+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. Currently only `python` evaluation is supported.
-## Setup Environment
+## Setup Environment and LLM Configuration
-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"
-
-[sandbox]
-enable_auto_lint = true
-
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on HumanEvalFix
diff --git a/evaluation/humanevalfix/run_infer.py b/evaluation/humanevalfix/run_infer.py
index fa68396633..94d23f9313 100644
--- a/evaluation/humanevalfix/run_infer.py
+++ b/evaluation/humanevalfix/run_infer.py
@@ -9,9 +9,9 @@ TODOs:
"""
import asyncio
-import logging
import os
-import pathlib
+import tempfile
+from typing import Any
import pandas as pd
from datasets import load_dataset
@@ -19,20 +19,25 @@ from evaluate import load
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import CmdRunAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
IMPORT_HELPER = {
'python': [
@@ -72,19 +77,105 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
-def get_test_result(instance, path, language='python', timeout=10):
- # Evaluation reference: https://github.com/bigcode-project/bigcode-evaluation-harness/blob/84b96da31b7f840b55c5733325346176140cdb6b/bigcode_eval/tasks/humanevalpack.py#L347
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+def _get_instance_id(instance: pd.Series) -> str:
+ return instance.task_id.replace('/', '__')
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ problem_statement = (
+ instance.declaration + instance.buggy_solution + '\n' + instance.test
+ )
+ filename = f'{_get_instance_id(instance)}.py'
+ with tempfile.TemporaryDirectory() as tmpdir:
+ host_script_path = os.path.join(tmpdir, filename)
+ with open(host_script_path, 'w') as f:
+ f.write(problem_statement)
+ await runtime.copy_to(
+ host_script_path,
+ '/workspace',
+ )
+
+ # check file exists
+ action = CmdRunAction(command=f'ls /workspace/{_get_instance_id(instance)}.py')
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def complete_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # default value
+ language = 'python'
+ timeout = 10
+
test_result = {'result': {}, 'metadata': {}}
code_metric = load('Muennighoff/code_eval_octopack')
timeout = LANGUAGE_TO_TIMEOUT[language]
num_workers = LANGUAGE_TO_NUM_WORKERS[language]
python_imports = '\n'.join(IMPORT_HELPER[language])
- # Load function from path
- with open(path, 'r') as f:
- function = f.read()
+ action = CmdRunAction(
+ command=f'cat /workspace/{_get_instance_id(instance)}.py', keep_prompt=False
+ )
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
- function = [[python_imports + '\n' + function.strip()]]
+ function = obs.content.replace('\r\n', '\n')
+ logger.info(f'Function: {function}')
+ function = [[python_imports + '\n' + function]]
results, logs = code_metric.compute(
references=[instance.test],
@@ -99,129 +190,79 @@ def get_test_result(instance, path, language='python', timeout=10):
'timeout': timeout,
'num_workers': num_workers,
}
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
return test_result
-def process_instance(
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- old_workspace_mount_path = config.workspace_mount_path
- old_workspace_base = config.workspace_base
+) -> EvalOutput:
+ config = get_config(metadata)
+ # use a session id for concurrent evaluation
+ sid = _get_instance_id(instance)
- try:
- workspace_mount_path = os.path.join(
- config.workspace_mount_path, '_eval_workspace'
- )
- # create process-specific workspace dir
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance.task_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance.task_id}.')
- # reset workspace to config
- config.workspace_base = workspace_mount_path
- config.workspace_mount_path = workspace_mount_path
+ # Create file with HumanEvalFix problem
+ # Prompt reference: https://github.com/bigcode-project/bigcode-evaluation-harness/blob/84b96da31b7f840b55c5733325346176140cdb6b/bigcode_eval/tasks/humanevalpack.py#L509
+ problem_statement = (
+ instance.declaration + instance.buggy_solution + '\n' + instance.test
+ )
- # use a session id for concurrent evaluation
- sid = instance.task_id.replace('/', '__')
+ # Prepare instruction
+ instruction = (
+ f'Please fix the function in {sid}.py such that all test cases pass.\n'
+ 'Environment has been set up for you to start working. You may assume all necessary tools are installed.\n\n'
+ '# Problem Statement\n'
+ f'{problem_statement}\n\n'
+ )
+ instruction += (
+ 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
+ 'You should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\n'
+ 'You SHOULD INCLUDE PROPER INDENTATION in your edit commands.\n'
+ )
+ # NOTE: You can actually set slightly different instruction for different agents
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
- # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
- if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir,
- 'logs',
- f'instance_{sid}.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)
+ # Here's how you can run the agent (similar to the `main` function) and get the final task state
+ runtime = await create_runtime(config, sid=sid)
+ await initialize_runtime(runtime, instance)
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
+ metadata.agent_class
+ ),
+ )
- logger.info(f'Process-specific workspace mounted at {workspace_mount_path}')
+ if state is None:
+ raise ValueError('State should not be None.')
+ metrics = state.metrics.get() if state.metrics else None
+ test_result = await complete_runtime(runtime, instance)
- # Create file with HumanEvalFix problem
- # Prompt reference: https://github.com/bigcode-project/bigcode-evaluation-harness/blob/84b96da31b7f840b55c5733325346176140cdb6b/bigcode_eval/tasks/humanevalpack.py#L509
- problem_statement = (
- instance.declaration + instance.buggy_solution + '\n' + instance.test
- )
- path = os.path.join(workspace_mount_path, f'{sid}.py')
- with open(path, 'w') as f:
- f.write(problem_statement)
+ # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+ # for compatibility with the existing output format, we can remake the pairs here
+ # remove when it becomes unnecessary
+ histories = state.history.compatibility_for_eval_history_pairs()
- # Prepare instruction
- instruction = (
- f'Please fix the function in {instance.task_id.replace("/", "__")}.py such that all test cases pass.\n'
- 'Environment has been set up for you to start working. You may assume all necessary tools are installed.\n\n'
- '# Problem Statement\n'
- f'{problem_statement}\n\n'
- )
- instruction += (
- 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
- 'You should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\n'
- 'You SHOULD INCLUDE PROPER INDENTATION in your edit commands.\n'
- )
- # NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
-
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
- agent.__class__.__name__
- ),
- sid=sid,
- )
- )
-
- # ======= Attempt to evaluate the agent's edits =======
- test_result = get_test_result(instance, path)
-
- # If you are working on some simpler 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.')
- metrics = state.metrics.get() if state.metrics else None
-
- # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
- # for compatibility with the existing output format, we can remake the pairs here
- # remove when it becomes unnecessary
- histories = state.history.compatibility_for_eval_history_pairs()
-
- # Save the output
- output = {
- 'task_id': instance.task_id,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': test_result,
- }
- except Exception:
- logger.error('Process instance failed')
- raise
- finally:
- config.workspace_mount_path = old_workspace_mount_path
- config.workspace_base = old_workspace_base
+ # Save the output
+ output = EvalOutput(
+ instance_id=instance.task_id,
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result=test_result,
+ )
return output
@@ -234,28 +275,31 @@ if __name__ == '__main__':
'bigcode/humanevalpack', 'python'
) # TODO: Support other languages
hefix_tests = dataset['test'].to_pandas()
+ hefix_tests.rename(columns={'task_id': 'instance_id'}, inplace=True)
- id_column = 'task_id'
-
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ 'humanevalfix-python',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
+ instances = prepare_dataset(hefix_tests, output_file, args.eval_n_limit)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ asyncio.run(
+ run_evaluation(
+ instances,
+ metadata,
+ output_file,
+ args.eval_num_workers,
+ process_instance,
+ )
)
diff --git a/evaluation/humanevalfix/scripts/run_infer.sh b/evaluation/humanevalfix/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/logic_reasoning/Dockerfile b/evaluation/logic_reasoning/Dockerfile
new file mode 100644
index 0000000000..0730c2e36d
--- /dev/null
+++ b/evaluation/logic_reasoning/Dockerfile
@@ -0,0 +1,7 @@
+FROM ubuntu:22.04
+
+RUN apt-get update && apt-get install -y python3 python3-pip
+
+RUN pip install scitools-pyke
+
+# docker build -t xingyaoww/od_logic_reasoning .
diff --git a/evaluation/logic_reasoning/README.md b/evaluation/logic_reasoning/README.md
index c0e313cf8b..79faae4fe0 100644
--- a/evaluation/logic_reasoning/README.md
+++ b/evaluation/logic_reasoning/README.md
@@ -2,38 +2,13 @@
This folder contains evaluation harness for evaluating agents on the logic reasoning benchmark [ProntoQA](https://github.com/asaparov/prontoqa) and [ProofWriter](https://allenai.org/data/proofwriter).
-## Configure OpenDevin and your LLM
+## Setup Environment and LLM Configuration
-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"
-
-[sandbox]
-enable_auto_lint = true
-
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview_llm]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model_llm]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on logic_reasoning
-The following code will run inference on the first example of the ProntoQA dataset,
-using OpenDevin 0.6.2 version.
+The following code will run inference on the first example of the ProofWriter dataset,
```bash
-./evaluation/logic_reasoning/scripts/run_infer.sh ProntoQA eval_gpt4_1106_preview_llm 0.6.2 1
+./evaluation/logic_reasoning/scripts/run_infer.sh eval_gpt4_1106_preview_llm ProofWriter
```
diff --git a/evaluation/logic_reasoning/instruction.txt b/evaluation/logic_reasoning/instruction.txt
index bb49e883c7..2a9b16582e 100644
--- a/evaluation/logic_reasoning/instruction.txt
+++ b/evaluation/logic_reasoning/instruction.txt
@@ -3,12 +3,12 @@ you can interact with an interactive Python (Jupyter Notebook) environment and r
In this task, you need to use the code in [[logic_inference_path.py]] to help you. Specifically, you first need to instantiate a **LogicInferenceEngine** class and use the **safe_execute_program** method to prove the **logic programs**. You should receive *answer*, *flag*, *error_message* from the output.
An example would be look like this:
-
- import sys
- sys.path.append(workspace_mount_path)
- engine = LogicInferenceEngine(dataset_name, workspace_mount_path)
- answer, flag, error_message = engine.safe_execute_program(logic_programs)
-
+
+import sys
+sys.path.append('/workspace')
+engine = LogicInferenceEngine()
+answer, flag, error_message = engine.safe_execute_program(logic_programs)
+
Please send the *answer* variable through message.
diff --git a/evaluation/logic_reasoning/logic_inference.py b/evaluation/logic_reasoning/logic_inference.py
index fd8404da10..fd6d52b4c6 100644
--- a/evaluation/logic_reasoning/logic_inference.py
+++ b/evaluation/logic_reasoning/logic_inference.py
@@ -191,9 +191,9 @@ class PykeProgram:
class LogicInferenceEngine:
- def __init__(self, dataset_name, workspace_mount_path):
- self.dataset_name = dataset_name
- self.workspace_mount_path = workspace_mount_path
+ def __init__(self):
+ self.dataset_name = os.environ.get('DATASET_NAME', 'ProofWriter')
+ self.workspace_mount_path = '/workspace'
def random_backup(self):
if self.dataset_name == 'ProntoQA':
diff --git a/evaluation/logic_reasoning/run_infer.py b/evaluation/logic_reasoning/run_infer.py
index c6efbba833..d0739d4f39 100644
--- a/evaluation/logic_reasoning/run_infer.py
+++ b/evaluation/logic_reasoning/run_infer.py
@@ -1,29 +1,35 @@
import asyncio
-import logging
import os
-import pathlib
-import shutil
import pandas as pd
from datasets import load_dataset
-from evaluation.swe_bench.swe_env_box import DockerSSHBox
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import (
+ AgentFinishAction,
+ CmdRunAction,
+ IPythonRunCellAction,
+ MessageAction,
+)
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -34,6 +40,28 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='xingyaoww/od-eval-logic-reasoning:v1.0',
+ enable_auto_lint=True,
+ use_host_network=False,
+ od_runtime_extra_deps='$OD_INTERPRETER_PATH -m pip install scitools-pyke',
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
def get_choice(answer_str):
choices = [
'A',
@@ -83,7 +111,7 @@ def get_test_result(
'the correct answer is',
'The correct answer is',
'The correct option is',
- 'Thus, the answer is',
+ 'the answer is',
]
if prediction is None:
for indicator in indicators:
@@ -97,162 +125,143 @@ def get_test_result(
return test_result
-def process_instance(
+CUR_EVAL_DIR = os.path.dirname(__file__)
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ # copy logic_inference.py to /workspace
+ await runtime.copy_to(
+ os.path.join(CUR_EVAL_DIR, 'logic_inference.py'), '/workspace'
+ )
+ # check if the file exists
+ obs = await runtime.run_action(CmdRunAction(command='ls /workspace'))
+ assert obs.exit_code == 0
+ assert 'logic_inference.py' in obs.content
+
+ await runtime.add_env_vars({'DATASET_NAME': metadata.dataset})
+
+ action = CmdRunAction(command='mkdir -p /workspace/.cache_program')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = IPythonRunCellAction(code='%pip install scitools-pyke')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ ipynb_obs = await runtime.run_action(action)
+ logger.info(ipynb_obs, extra={'msg_type': 'OBSERVATION'})
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+# Prepare instruction
+with open(os.path.join(CUR_EVAL_DIR, 'instruction.txt'), 'r') as f:
+ INSTRUCTION_TEMPLATE = f.read()
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- old_workspace_mount_path = config.workspace_mount_path
- old_workspace_base = config.workspace_base
+ config = get_config(metadata)
- try:
- workspace_mount_path = os.path.join(
- config.workspace_mount_path, '_eval_workspace'
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance['instance_id'], log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance["instance_id"]}.')
+
+ instance_logic_programs = instance['raw_logic_programs'][0].strip()
+ instruction = (
+ INSTRUCTION_TEMPLATE.replace('[[dataset_name]]', dataset_name)
+ .replace('[[logic_programs]]', instance_logic_programs)
+ .replace('[[logic_inference_path.py]]', '/workspace/logic_inference.py')
+ )
+
+ # NOTE: You can actually set slightly different instruction for different agents
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
+
+ # use a session id for concurrent evaluation
+ sid = instance['instance_id']
+
+ runtime = await create_runtime(config, sid=sid)
+ await initialize_runtime(runtime, instance)
+
+ # Here's how you can run the agent (similar to the `main` function) and get the final task state
+ state: State | None = asyncio.run(
+ run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
+ metadata.agent_class
+ ),
)
- # create process-specific workspace dir
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+ )
+ # ======= Attempt to evaluate the agent's edits =======
+ # If you are working on simpler 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.
- # reset workspace to config
- config.workspace_base = workspace_mount_path
- config.workspace_mount_path = workspace_mount_path
+ if state is None:
+ raise ValueError('State should not be None.')
- # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
- if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{instance["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["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)
+ final_message = ''
+ for event in state.history.get_events(reverse=True):
+ if isinstance(event, AgentFinishAction):
+ final_message = event.thought
+ break
+ elif isinstance(event, MessageAction):
+ final_message = event.content
+ break
- logger.info(f'Process-specific workspace mounted at {workspace_mount_path}')
+ final_message = final_message.strip("'")
+ logger.info(
+ f'Predicted answer: {final_message}, Ground truth: {instance["answer"]}'
+ )
- # sandbox = DockerSSHBox()
- logic_inference_path = os.path.join(workspace_mount_path, 'logic_inference.py')
- if not os.path.exists(logic_inference_path):
- shutil.copyfile(
- './evaluation/logic_reasoning/logic_inference.py', logic_inference_path
- )
- logger.info(f'logic_inference.py copied to {workspace_mount_path}')
+ test_result = get_test_result(
+ model_answer=final_message, ground_truth=instance['answer']
+ )
+ test_result['final_message'] = final_message
- cache_dir = os.path.join(workspace_mount_path, '.cache_program')
- if not os.path.exists(cache_dir):
- os.makedirs(cache_dir)
-
- # Prepare instruction
-
- with open('./evaluation/logic_reasoning/instruction.txt', 'r') as f:
- instruction = f.read()
-
- instance_logic_programs = instance['raw_logic_programs'][0].strip()
- instruction = instruction.replace('[[dataset_name]]', dataset_name)
- instruction = instruction.replace('[[logic_programs]]', instance_logic_programs)
- instruction = instruction.replace(
- '[[logic_inference_path.py]]', logic_inference_path
- )
-
- # NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
-
- # use a session id for concurrent evaluation
- sid = instance['id'] + '_' + str(os.getpid())
- sandbox = DockerSSHBox(
- config=config.sandbox,
- persist_sandbox=False,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- sid=sid,
- )
- exit_code, command_output = sandbox.execute('pip install scitools-pyke')
-
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
- agent.__class__.__name__
- ),
- sandbox=sandbox,
- sid=sid,
- )
- )
- # ======= Attempt to evaluate the agent's edits =======
- # If you are working on simpler 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 = ''
- messages = []
- for event in state.history.get_events(reverse=True):
- # will this be a MessageAction?
- # TODO we can filter for types of events if we know what to expect
- messages.append(event.content)
- if str(event.content) in ["'A'", "'B'", "'C'"]:
- final_message = event.content
- break
-
- final_message = final_message.strip("'")
- logger.info(
- f'Predicted answer: {final_message}, Ground truth: {instance["answer"]}'
- )
-
- test_result = get_test_result(
- model_answer=final_message, ground_truth=instance['answer']
- )
- metrics = state.metrics.get() if state.metrics else None
-
- # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
- # for compatibility with the existing output format, we can remake the pairs here
- # remove when it becomes unnecessary
- histories = state.history.compatibility_for_eval_history_pairs()
-
- # Save the output
- output = {
- 'id': instance['id'],
- 'instance': instance,
- 'instruction': instruction,
- # 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'final_message': final_message,
- 'messages': messages,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': test_result,
- }
- except Exception:
- logger.error('Process instance failed')
- raise
- finally:
- config.workspace_mount_path = old_workspace_mount_path
- config.workspace_base = old_workspace_base
-
- # Close the sandbox
- sandbox.close()
+ metrics = state.metrics.get() if state.metrics else None
+ # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+ # for compatibility with the existing output format, we can remake the pairs here
+ # remove when it becomes unnecessary
+ histories = state.history.compatibility_for_eval_history_pairs()
+ # Save the output
+ output = EvalOutput(
+ instance_id=instance['instance_id'],
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result=test_result,
+ )
return output
@@ -262,7 +271,7 @@ if __name__ == '__main__':
'--dataset',
type=str,
help='the logic reasoning dataset to evaluate on {ProntoQA, ProofWriter}',
- default='ProntoQA',
+ default='ProofWriter',
)
parser.add_argument(
'--data_split',
@@ -270,36 +279,32 @@ if __name__ == '__main__':
help='data split to evaluate on {validation}', # right now we only support validation split
default='validation',
)
-
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}')
dataset_name = args.dataset
data_split = args.data_split
dataset = load_dataset(f'renma/{dataset_name}')
- logic_reasoning_tests = dataset[data_split]
+ dataset_df = dataset[data_split].to_pandas()
+ dataset_df.rename(columns={'id': 'instance_id'}, inplace=True)
- id_column = 'id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ dataset_name,
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances = prepare_dataset(dataset_df, output_file, args.eval_n_limit)
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/logic_reasoning/scripts/run_infer.sh b/evaluation/logic_reasoning/scripts/run_infer.sh
old mode 100644
new mode 100755
index b7cec5cd54..21fb7a1ae7
--- a/evaluation/logic_reasoning/scripts/run_infer.sh
+++ b/evaluation/logic_reasoning/scripts/run_infer.sh
@@ -3,8 +3,8 @@ set -eo pipefail
source "evaluation/utils/version_control.sh"
-DATASET=$1
-MODEL_CONFIG=$2
+MODEL_CONFIG=$1
+DATASET=$2
COMMIT_HASH=$3
EVAL_LIMIT=$4
AGENT=$5
@@ -23,6 +23,11 @@ if [ -z "$AGENT" ]; then
AGENT="CodeActAgent"
fi
+if [ -z "$DATASET" ]; then
+ echo "Dataset not specified, use default ProofWriter"
+ DATASET="ProofWriter"
+fi
+
get_agent_version
echo "AGENT: $AGENT"
diff --git a/evaluation/miniwob/Dockerfile b/evaluation/miniwob/Dockerfile
new file mode 100644
index 0000000000..b7d191ac67
--- /dev/null
+++ b/evaluation/miniwob/Dockerfile
@@ -0,0 +1,10 @@
+FROM ubuntu:22.04
+
+RUN apt-get update && apt-get install -y python3 python3-pip git
+
+RUN git clone https://github.com/Farama-Foundation/miniwob-plusplus.git /miniwob-plusplus && \
+ git -C "/miniwob-plusplus" reset --hard 7fd85d71a4b60325c6585396ec4f48377d049838
+
+ENV MINIWOB_URL="file:///miniwob-plusplus/miniwob/html/miniwob/"
+
+# docker build -t xingyaoww/od-eval-miniwob .
diff --git a/evaluation/miniwob/README.md b/evaluation/miniwob/README.md
index 7c26872a8d..4426ebff70 100644
--- a/evaluation/miniwob/README.md
+++ b/evaluation/miniwob/README.md
@@ -2,52 +2,9 @@
This folder contains evaluation for [MiniWoB++](https://miniwob.farama.org/) benchmark, powered by [BrowserGym](https://github.com/ServiceNow/BrowserGym) for easy evaluation of how well an agent capable of browsing can perform on synthetic web browsing tasks.
-## Setup OpenDevin Environment
+## Setup Environment and LLM Configuration
-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"
-
-[sandbox]
-box_type = "ssh"
-timeout = 120
-
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
-
-## Setup MiniWoB++ Environment and Environment Variables of MiniWoB++
-MiniWoB++ requires you to set up websites containing a static website that is accessible via URL to the machine running the OpenDevin agents.
-
-- Clone miniwob (use a specific frozen commit for reproducibility)
-```sh
-git clone git@github.com:Farama-Foundation/miniwob-plusplus.git
-git -C "./miniwob-plusplus" reset --hard 7fd85d71a4b60325c6585396ec4f48377d049838
-```
-
-- Setup Miniwob URL (change `PATH_TO_MINIWOB_CLONED_REPO` here to the absolute path to your `miniwob-plusplus` folder) in `evaluation/miniwob/scripts/run_infer.sh`
-```sh
-export MINIWOB_URL="file:///miniwob/html/miniwob/"
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Test if your environment works
@@ -56,7 +13,7 @@ Access with browser the above MiniWoB URLs and see if they load correctly.
## Run Evaluation
```sh
-bash evaluation/miniwob/scripts/run_infer.sh
+./evaluation/miniwob/scripts/run_infer.sh llm.claude-35-sonnet-eval
```
Results will be in `evaluation/evaluation_outputs/outputs/miniwob/`
diff --git a/evaluation/miniwob/run_infer.py b/evaluation/miniwob/run_infer.py
index ad59ad6492..92c96bf425 100644
--- a/evaluation/miniwob/run_infer.py
+++ b/evaluation/miniwob/run_infer.py
@@ -1,7 +1,7 @@
import asyncio
import json
-import logging
import os
+from typing import Any
import browsergym.miniwob # noqa F401 register miniwob tasks as gym environments
import gymnasium as gym
@@ -9,91 +9,131 @@ import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-from opendevin.runtime.docker.ssh_box import DockerSSHBox
-from opendevin.runtime.tools import RuntimeTool
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import (
+ BrowseInteractiveAction,
+ CmdRunAction,
+ MessageAction,
+)
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.browser.browser_env import (
+ BROWSER_EVAL_GET_GOAL_ACTION,
+ BROWSER_EVAL_GET_REWARDS_ACTION,
+)
+from opendevin.runtime.runtime import Runtime
SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
-docker_ssh_box: DockerSSHBox | None = None
+
+def get_config(
+ metadata: EvalMetadata,
+ env_id: str,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='xingyaoww/od-eval-miniwob:v1.0',
+ enable_auto_lint=True,
+ use_host_network=False,
+ browsergym_eval_env=env_id,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
-def get_sandbox():
- global docker_ssh_box
- if docker_ssh_box is None:
- docker_ssh_box = DockerSSHBox(
- config=config.sandbox,
- persist_sandbox=False,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- )
- return docker_ssh_box
+async def initialize_runtime(
+ runtime: Runtime,
+) -> str:
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = BrowseInteractiveAction(browser_actions=BROWSER_EVAL_GET_GOAL_ACTION)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ goal = obs.content
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+ return goal
-def process_instance(
+async def complete_runtime(
+ runtime: Runtime,
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ action = BrowseInteractiveAction(browser_actions=BROWSER_EVAL_GET_REWARDS_ACTION)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
+ return {
+ 'rewards': json.loads(obs.content),
+ }
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
+) -> EvalOutput:
env_id = instance.id
+ config = get_config(metadata, env_id)
+
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{env_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 {env_id}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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)
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, env_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {env_id}.')
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- runtime_tools_config = {
- RuntimeTool.BROWSER: {
- 'browsergym_eval': env_id,
- 'browsergym_eval_save_dir': metadata.eval_output_dir,
- }
- }
+ runtime = await create_runtime(config, sid=env_id)
+ task_str = await initialize_runtime(runtime)
state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- 'PLACEHOLDER_GOAL',
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- runtime_tools_config=runtime_tools_config,
- sandbox=get_sandbox(),
- sid=env_id,
+ run_controller(
+ config=config,
+ task_str=task_str, # take output from initialize_runtime
+ runtime=runtime,
)
)
@@ -106,18 +146,17 @@ def process_instance(
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
- browsergym_eval_dir = os.path.join(metadata.eval_output_dir, env_id.split('/')[1])
- # read goal
- with open(
- os.path.join(browsergym_eval_dir, 'goal.txt'), 'r', encoding='utf-8'
- ) as f:
- instruction = f.read()
- # read reward
- with open(
- os.path.join(browsergym_eval_dir, 'rewards.json'), 'r', encoding='utf-8'
- ) as f:
- rewards = json.load(f)
- reward = max(rewards)
+
+ # Instruction is the first message from the USER
+ instruction = ''
+ for event in state.history.get_events():
+ if isinstance(event, MessageAction):
+ instruction = event.content
+ break
+
+ return_val = await complete_runtime(runtime)
+ logger.info(f'Return value from complete_runtime: {return_val}')
+ reward = max(return_val['rewards'])
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -125,16 +164,17 @@ def process_instance(
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
- output = {
- 'instance_id': env_id,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': reward,
- }
-
+ output = EvalOutput(
+ instance_id=env_id,
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
+ 'reward': reward,
+ },
+ )
return output
@@ -143,7 +183,7 @@ if __name__ == '__main__':
dataset = pd.DataFrame(
{
- 'id': [
+ 'instance_id': [
id
for id in gym.envs.registry.keys()
if id.startswith('browsergym/miniwob')
@@ -151,26 +191,25 @@ if __name__ == '__main__':
}
)
- id_column = 'id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ 'miniwob',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
- _ = get_sandbox() # Initialize the sandbox
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances = prepare_dataset(dataset, output_file, args.eval_n_limit)
+
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/miniwob/scripts/run_infer.sh b/evaluation/miniwob/scripts/run_infer.sh
old mode 100644
new mode 100755
index e6131b8c98..0fe525ba79
--- a/evaluation/miniwob/scripts/run_infer.sh
+++ b/evaluation/miniwob/scripts/run_infer.sh
@@ -3,14 +3,10 @@ set -eo pipefail
source "evaluation/utils/version_control.sh"
-# configure miniwob website, change URL to yours
-export MINIWOB_URL="file:///home/fangzhex/miniwob-plusplus/miniwob/html/miniwob/"
-
# configure browsing agent
export USE_NAV="false"
export USE_CONCISE_ANSWER="true"
-
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
@@ -42,7 +38,7 @@ COMMAND="poetry run python evaluation/miniwob/run_infer.py \
--llm-config $MODEL_CONFIG \
--max-iterations 10 \
--max-chars 10000000 \
- --eval-num-workers $NUM_WORKERS \
+ --eval-num-workers $NUM_WORKERS"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
diff --git a/evaluation/mint/Dockerfile b/evaluation/mint/Dockerfile
new file mode 100644
index 0000000000..af7366763d
--- /dev/null
+++ b/evaluation/mint/Dockerfile
@@ -0,0 +1,10 @@
+FROM ubuntu:22.04
+
+RUN apt-get update && apt-get install -y python3 python3-pip git gcc
+
+WORKDIR /root
+
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+# docker build -t xingyaoww/od-eval-mint:v1.0 .
diff --git a/evaluation/mint/README.md b/evaluation/mint/README.md
index 1e07bd6431..3925cf5e40 100644
--- a/evaluation/mint/README.md
+++ b/evaluation/mint/README.md
@@ -2,9 +2,11 @@
This folder contains the evaluation harness for the [MINT benchmark](https://arxiv.org/abs/2309.10691) on LLMs' ability to solve tasks with multi-turn interactions.
-## Configure OpenDevin and LM
+We support evaluation of the [Eurus subset focus on math and code reasoning](https://arxiv.org/abs/2404.02078), including MATH, MMLU, TheoremQA, HumanEval, MBPP.
-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.
+## Setup Environment and LLM Configuration
+
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Start the evaluation
diff --git a/evaluation/mint/run_infer.py b/evaluation/mint/run_infer.py
index fd7ce88f3c..42ea459014 100644
--- a/evaluation/mint/run_infer.py
+++ b/evaluation/mint/run_infer.py
@@ -1,33 +1,36 @@
-import asyncio
import functools
-import logging
import os
-import pathlib
from typing import Any, Dict
+import pandas as pd
from datasets import load_dataset
-from evaluation.swe_bench.swe_env_box import DockerSSHBox
+from evaluation.mint.datatypes import TaskState
+from evaluation.mint.env import SimplifiedEnv
+from evaluation.mint.prompts import ToolPromptTemplate
+from evaluation.mint.tasks import Task
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-from .datatypes import TaskState
-from .env import SimplifiedEnv
-from .prompts import ToolPromptTemplate
-from .tasks import Task
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import (
+ CmdRunAction,
+)
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
def codeact_user_response_mint(state: State, task: Task, task_config: Dict[str, int]):
@@ -42,7 +45,7 @@ def codeact_user_response_mint(state: State, task: Task, task_config: Dict[str,
last_action = state.history.get_last_action()
result_state: TaskState = env.step(last_action.message or '')
- state.task_state = result_state
+ state.extra_data['task_state'] = result_state
if not result_state.latest_output:
# Task is finished
@@ -62,85 +65,107 @@ AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': '\nIMPORTANT: When your answer is confirmed by the user to be correct, you can exit using the following command: exit .\n'
}
+with open(os.path.join(os.path.dirname(__file__), 'requirements.txt'), 'r') as f:
+ MINT_DEPENDENCIES = f.read().splitlines()
-def process_instance(
+
+def load_incontext_example(task_name: str, with_tool: bool = True):
+ assert with_tool, 'NOT with_tool is not supported yet'
+ subset = {
+ 'gsm8k': 'reasoning',
+ 'math': 'reasoning',
+ 'mmlu': 'reasoning',
+ 'theoremqa': 'reasoning',
+ 'mbpp': 'mbpp',
+ 'humaneval': 'humaneval',
+ }[task_name]
+ with open(
+ os.path.join(
+ os.path.dirname(__file__),
+ 'tasks',
+ 'in_context_examples',
+ subset,
+ 'with_tool.txt',
+ ),
+ 'r',
+ ) as f:
+ return f.read()
+
+
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='xingyaoww/od-eval-mint:v1.0',
+ enable_auto_lint=True,
+ use_host_network=False,
+ od_runtime_extra_deps=f'$OD_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}',
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(runtime: Runtime):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def process_instance(
instance: Any,
metadata: EvalMetadata,
reset_logger: bool = True,
):
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(metadata.llm_config))
- workspace_mount_path = os.path.join(config.workspace_mount_path, '_eval_workspace')
- # create process-specific workspace dir
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+ config = get_config(metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.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}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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}')
-
- # use a session id for concurrent processing
- sid = instance.task_id + '_' + str(os.getpid())
- sandbox = DockerSSHBox(
- config=config.sandbox,
- persist_sandbox=False,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- sid=sid,
- )
-
- requirements_host_src = 'evaluation/mint/requirements.txt'
- requirements_sandbox_dest = '/opendevin/plugins/mint/requirements.txt'
- sandbox.copy_to(
- host_src=requirements_host_src,
- sandbox_dest=requirements_sandbox_dest,
- recursive=False,
- )
- logger.info(
- f'Copied files from [{requirements_host_src}] to [{requirements_sandbox_dest}] inside sandbox.'
- )
- exit_code, output = sandbox.execute(f'pip install -r {requirements_sandbox_dest}')
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance.instance_id}.')
# Prepare instruction
assert metadata.details is not None
instruction = ToolPromptTemplate(use_tool=True)(
max_total_steps=metadata.max_iterations,
max_propose_solution=metadata.details['max_propose_solution'],
- in_context_example=instance.in_context_example(
- use_tool=True, with_feedback=False
- ),
+ in_context_example=instance.in_context_example,
task_prompt='Task:\n' + instance.prompt,
)
instruction += 'IMPORTANT: You should ONLY interact with the environment provided to you or provide the concise RESULT inside tag AND NEVER ASK FOR HUMAN HELP.\n'
# NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
# Here's how you can run the agent (similar to the `main` function) and get the final task state
fake_user_response_fn = functools.partial(
- AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[agent.__class__.__name__],
+ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
task=instance,
task_config={
'max_iterations': metadata.max_iterations,
@@ -148,24 +173,22 @@ def process_instance(
},
)
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=fake_user_response_fn,
- sandbox=sandbox,
- sid=sid,
- )
+ runtime = await create_runtime(config, sid=instance.instance_id)
+ await initialize_runtime(runtime)
+
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=fake_user_response_fn,
)
if state is None:
raise ValueError('State should not be None.')
task_state = None
- if hasattr(state, 'task_state'):
- task_state = state.task_state
+ if 'task_state' in state.extra_data:
+ task_state = state.extra_data['task_state']
logger.info('Task state: ' + str(task_state.to_dict()))
metrics = state.metrics.get() if state.metrics else None
@@ -176,30 +199,37 @@ def process_instance(
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
- output = {
- 'id': instance.task_id,
- 'instance': instance.to_dict(),
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': task_state.success if task_state else False,
- }
-
- # Close the sandbox
- sandbox.close()
-
+ output = EvalOutput(
+ instance_id=instance.instance_id,
+ instance=instance.to_dict(),
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
+ 'success': task_state.success if task_state else False,
+ },
+ )
return output
if __name__ == '__main__':
parser = get_parser()
+ SUBSETS = [
+ # Eurus subset: https://arxiv.org/abs/2404.02078
+ 'math',
+ # 'gsm8k',
+ 'mmlu',
+ 'theoremqa',
+ 'mbpp',
+ 'humaneval',
+ ]
parser.add_argument(
'--subset',
- default='math',
- choices=['math', 'gsm8k', 'mmlu', 'theoremqa', 'mbpp', 'humaneval'],
+ default='all',
+ choices=SUBSETS + ['all'],
type=str,
help='subset of the dataset to be used',
)
@@ -214,19 +244,36 @@ if __name__ == '__main__':
# 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
- mint_dataset = load_dataset(
- 'ryanhoangt/xingyaoww-mint-bench', name=args.subset, split='test'
- )
- logger.info(f'Evaluating MINT - {args.subset} subset')
- mint_tests = mint_dataset.to_pandas()
+ if args.subset == 'all':
+ subsets = SUBSETS
+ else:
+ subsets = [args.subset]
- id_column = 'id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ dataset_dfs = []
+ for subset in subsets:
+ in_context_example = load_incontext_example(subset)
+ _cur_dataset = load_dataset(
+ 'ryanhoangt/xingyaoww-mint-bench', name=subset, split='test'
+ )
+ logger.info(f'Loaded MINT - {subset} subset')
+ _df = _cur_dataset.to_pandas().rename(columns={'id': 'instance_id'})
+ _df['instance_id'] = _df['instance_id'].apply(lambda x: f'{subset}/{x}') # noqa
+ _df['in_context_example'] = in_context_example
+ dataset_dfs.append(_df)
+ logger.info(f'Loaded {len(_df)} instances for subset: {subset}')
+
+ dataset_df = pd.concat(dataset_dfs)
+ logger.info(f'Loaded {len(dataset_df)} instances for subset: {subsets}')
+
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ f'MINT-{args.subset}',
args.agent_cls,
args.max_iterations,
args.eval_note,
@@ -234,12 +281,7 @@ if __name__ == '__main__':
details={'max_propose_solution': args.max_propose_solution},
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(mint_dataset, output_file, args.eval_n_limit, id_column)
+ instances = prepare_dataset(dataset_df, output_file, args.eval_n_limit)
run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances, metadata, output_file, args.eval_num_workers, process_instance
)
diff --git a/evaluation/mint/scripts/run_infer.sh b/evaluation/mint/scripts/run_infer.sh
old mode 100644
new mode 100755
index 8c8e017aa8..b9ec6d7a7a
--- a/evaluation/mint/scripts/run_infer.sh
+++ b/evaluation/mint/scripts/run_infer.sh
@@ -29,15 +29,16 @@ COMMAND="poetry run python ./evaluation/mint/run_infer.py \
--llm-config $MODEL_CONFIG \
--max-iterations 5 \
--max-propose-solution 2 \
- --eval-num-workers $NUM_WORKERS \
+ --eval-num-workers $NUM_WORKERS
+"
if [ -n "$SUBSET" ]; then
echo "SUBSET: $SUBSET"
COMMAND="$COMMAND --subset $SUBSET"
# otherwise default to use the math subset
else
- echo "SUBSET: math"
- COMMAND="$COMMAND --subset math"
+ echo "SUBSET: all"
+ COMMAND="$COMMAND --subset all"
fi
if [ -n "$EVAL_LIMIT" ]; then
diff --git a/evaluation/ml_bench/README.md b/evaluation/ml_bench/README.md
index 51e59cad79..e6199327a7 100644
--- a/evaluation/ml_bench/README.md
+++ b/evaluation/ml_bench/README.md
@@ -10,40 +10,9 @@ The task introduces new challenges for LLMs, such as comprehending long and lang
For more details on the ML-Bench task and dataset, please refer to the paper: [ML-Bench: Evaluating Large Language Models for Code Generation in Repository-Level Machine Learning Tasks](https://arxiv.org/abs/2311.09835).
-## Setup Environment
+## Setup Environment and LLM Configuration
-Please follow the [OpenDevin setup guide](https://github.com/OpenDevin/OpenDevin/blob/main/docs/setup.md) to set up the local development 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"
-run_as_devin = false
-sandbox_container_image = "public.ecr.aws/i5g0m1f6/ml-bench" # Use the latest image from the ML-Bench repository
-
-[sandbox]
-enable_auto_lint = true
-
-
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on ML-Bench
diff --git a/evaluation/ml_bench/run_infer.py b/evaluation/ml_bench/run_infer.py
index 07589f4d03..c7baa77e03 100644
--- a/evaluation/ml_bench/run_infer.py
+++ b/evaluation/ml_bench/run_infer.py
@@ -13,29 +13,34 @@ TODOs:
- Clean up the code and docker image used for evaluation.
"""
-import asyncio
-import logging
import os
-import pathlib
from typing import Any
+import pandas as pd
from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+ load_app_config,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-from opendevin.runtime.docker.ssh_box import DockerSSHBox
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import CmdRunAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
config = load_app_config()
@@ -66,169 +71,203 @@ ID2CONDA = {
}
-def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool = True):
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- old_workspace_mount_path = config.workspace_mount_path
- old_workspace_base = config.workspace_base
- try:
- workspace_mount_path = os.path.join(
- config.workspace_mount_path, '_eval_workspace'
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='public.ecr.aws/i5g0m1f6/ml-bench',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ # Set up the task environment
+ action = CmdRunAction(command=f'conda activate {ID2CONDA[instance["github_id"]]}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ repo_url = instance['github']
+ repo_name = repo_url.split('/')[-1]
+ action = CmdRunAction(command=f'git clone {repo_url} /workspace/{repo_name}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command=f'chmod -R 777 /workspace/{repo_name}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ # Navigate to the task's code path
+ task_path = os.path.join('/workspace', repo_name, instance['path'][2:])
+ action = CmdRunAction(command=f'cd {task_path}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def complete_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ repo_url = instance['github']
+ repo_name = repo_url.split('/')[-1]
+ task_path = os.path.join('/workspace', repo_name, instance['path'][2:])
+
+ # Evaluate the agent's script
+ eval_script = os.path.join(task_path, 'run.sh')
+ logger.info(f'Running evaluation script: {eval_script}')
+
+ action = CmdRunAction(command=f'cat {eval_script}', keep_prompt=False)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ if obs.exit_code == 0:
+ eval_script_content = obs.content
+ else:
+ logger.error(f'Error reading evaluation script: {obs.content}')
+ eval_script_content = ''
+
+ action = CmdRunAction(
+ command=f'timeout 120s conda run -n {ID2CONDA[instance["github_id"]]} bash {eval_script}',
+ timeout=600,
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ if obs.exit_code == 0:
+ eval_output = obs.content
+ else:
+ logger.error(f'Error running evaluation script: {obs.content}')
+ eval_output = ''
+
+ outputs = {
+ 'eval_script_content': eval_script_content,
+ 'eval_output': eval_output,
+ }
+ if obs.exit_code != 0 and obs.exit_code != 124:
+ logger.warning(f'Evaluation script failed with exit code {obs.exit_code}')
+ logger.warning(f'Output: {eval_output}')
+ outputs['success'] = int(
+ 'KeyboardInterrupt' in eval_output
+ ) # super-dainiu: assume ``KeyboardInterrupt`` is a success as is done in ML-Bench
+
+ else:
+ logger.info(f'Evaluation script succeeded with exit code {obs.exit_code}')
+ logger.info(f'Output: {eval_output}')
+ outputs['success'] = 1
+ outputs['eval_exit_code'] = obs.exit_code
+
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
+ return outputs
+
+
+async def process_instance(
+ instance: Any, metadata: EvalMetadata, reset_logger: bool = True
+):
+ config = get_config(metadata)
+
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance['instance_id'], log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance["instance_id"]}.')
+
+ # Create a sandbox, using the instance ID and PID as the session ID to avoid conflicts
+ sid = str(instance['instance_id'])
+
+ repo_url = instance['github']
+ repo_name = repo_url.split('/')[-1]
+ task_path = os.path.join('/workspace', repo_name, instance['path'][2:])
+ # Prepare the task instruction
+ instruction = (
+ f'Please complete the Machine Learning task in the following repository: {repo_name}\n\n'
+ f'{instance["instruction"]}\n\n'
+ 'You should create a script named `run.sh` under the specified path in the repo to run the task.\n\n'
+ f'You can find the task repo at: {task_path}\n\n'
+ + (
+ 'Here is the prefix code for the task:\n'
+ '```bash\n'
+ f'{instance["prefix_code"]}\n'
+ '```\n\n'
+ if instance['prefix_code']
+ else ''
)
- # create process-specific workspace dir
- # so that different agent don't interfere with each other.
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+ + 'You should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).'
+ )
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
- # reset workspace to config
- config.workspace_base = workspace_mount_path
- config.workspace_mount_path = workspace_mount_path
+ runtime = await create_runtime(config, sid=sid)
+ await initialize_runtime(runtime, instance)
- # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
- if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir,
- 'logs',
- f"instance_{instance['id']}_pid_{os.getpid()}.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['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)
+ # Run the agent
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
+ metadata.agent_class
+ ),
+ )
+ assert state is not None
+ metrics = state.metrics.get() if state.metrics else {}
- logger.info(f'Process-specific workspace mounted at {workspace_mount_path}')
+ test_result = await complete_runtime(runtime)
- # Create a sandbox, using the instance ID and PID as the session ID to avoid conflicts
- sid = str(instance['id']) + '_' + str(os.getpid())
- sandbox = DockerSSHBox(
- config=config.sandbox,
- persist_sandbox=False,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- sid=sid,
- )
+ # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+ # for compatibility with the existing output format, we can remake the pairs here
+ # remove when it becomes unnecessary
+ histories = state.history.compatibility_for_eval_history_pairs()
- # Set up the task environment
- sandbox.execute(f'conda activate {ID2CONDA[instance["github_id"]]}')
-
- # Clone the task repo into the sandbox
- repo_url = instance['github']
- repo_name = repo_url.split('/')[-1]
- sandbox.execute(f'git clone {repo_url} /workspace/{repo_name}')
- sandbox.execute(f'chmod -R 777 /workspace/{repo_name}')
-
- # Navigate to the task's code path
- task_path = os.path.join('/workspace', repo_name, instance['path'][2:])
- sandbox.execute(f'cd {task_path}')
-
- # Prepare the task instruction
- instruction = (
- f'Please complete the Machine Learning task in the following repository: {repo_name}\n\n'
- f'The task is: {instance["task"]}\n\n'
- f'{instance["instruction"]}\n\n'
- 'You should create a script named `run.sh` under the specified path in the repo to run the task.\n\n'
- f'You can find the task repo at: {task_path}\n\n'
- + (
- 'Here is the prefix code for the task:\n'
- '```bash\n'
- f'{instance["prefix_code"]}\n'
- '```\n\n'
- if instance['prefix_code']
- else ''
- )
- + 'You should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).'
- )
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
-
- # Run the agent
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
- agent.__class__.__name__
- ),
- sandbox=sandbox,
- sid=sid,
- )
- )
- assert state is not None
- metrics = state.metrics.get() if state.metrics else {}
-
- # Evaluate the agent's script
- eval_script = os.path.join(task_path, 'run.sh')
- logger.info(f'Running evaluation script: {eval_script}')
-
- try:
- _, eval_script_content = sandbox.execute(f'cat {eval_script}')
- except Exception as e:
- logger.error(f'Error reading evaluation script: {e}')
- eval_script_content = ''
-
- try:
- exit_code, eval_output = sandbox.execute(
- f'timeout 120s conda run -n {ID2CONDA[instance["github_id"]]} bash {eval_script}',
- timeout=600,
- )
- except Exception as e:
- logger.error(f'Error running evaluation script: {e}')
- exit_code = -1
- eval_output = ''
-
- if exit_code != 0 and exit_code != 124:
- logger.warning(f'Evaluation script failed with exit code {exit_code}')
- logger.warning(f'Output: {eval_output}')
- metrics['success'] = int(
- 'KeyboardInterrupt' in eval_output
- ) # super-dainiu: assume ``KeyboardInterrupt`` is a success as is done in ML-Bench
- else:
- logger.info(f'Evaluation script succeeded with exit code {exit_code}')
- logger.info(f'Output: {eval_output}')
- metrics['success'] = 1
-
- # history is now available as a stream of events, rather than list of pairs of (Action, Observation)
- # for compatibility with the existing output format, we can remake the pairs here
- # remove when it becomes unnecessary
- histories = state.history.compatibility_for_eval_history_pairs()
-
- # Save the output
- output = {
- 'instance_id': instance['id'],
- 'repo': repo_url,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'eval_script': eval_script_content,
- 'eval_exit_code': exit_code,
- 'eval_output': eval_output,
- 'metrics': metrics,
- }
-
- except Exception as e:
- logger.error(f'Error processing instance {instance["id"]}: {e}')
- raise
- finally:
- config.workspace_mount_path = old_workspace_mount_path
- config.workspace_base = old_workspace_base
-
- # Shutdown the sandbox
- sandbox.close()
+ # Save the output
+ output = EvalOutput(
+ instance_id=instance['instance_id'],
+ instance=instance.to_dict(),
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ test_result=test_result,
+ metrics=metrics,
+ )
return output
@@ -246,30 +285,26 @@ if __name__ == '__main__':
data_split = args.eval_split
- # 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
ml_bench = load_dataset('super-dainiu/ml-bench', split=data_split).to_pandas()
+ ml_bench.rename(columns={'id': 'instance_id'}, inplace=True)
- id_column = 'instance_id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
- args.dataset_name,
+ f'ml-bench-{data_split}',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(ml_bench, output_file, args.eval_n_limit, id_column)
+ instances = prepare_dataset(ml_bench, output_file, args.eval_n_limit)
run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances, metadata, output_file, args.eval_num_workers, process_instance
)
diff --git a/evaluation/ml_bench/scripts/run_infer.sh b/evaluation/ml_bench/scripts/run_infer.sh
old mode 100644
new mode 100755
diff --git a/evaluation/swe_bench/README.md b/evaluation/swe_bench/README.md
index 39713ecfc9..146069ed4e 100644
--- a/evaluation/swe_bench/README.md
+++ b/evaluation/swe_bench/README.md
@@ -1,132 +1,79 @@
# SWE-Bench Evaluation with OpenDevin SWE-Bench Docker Image
-This folder contains the evaluation harness that we built on top of the original [SWE-Bench benchmark](https://www.swebench.com/) ([paper](https://arxiv.org/abs/2310.06770)). We created [a fork of SWE-Bench](https://github.com/OpenDevin/OD-SWE-bench.git) mostly built on top of [the original repo](https://github.com/princeton-nlp/SWE-bench) and [containerized](#opendevin-swe-bench-docker-image) it for easy evaluation.
+This folder contains the evaluation harness that we built on top of the original [SWE-Bench benchmark](https://www.swebench.com/) ([paper](https://arxiv.org/abs/2310.06770)).
**UPDATE (7/1/2024): We now support the official SWE-Bench dockerized evaluation as announced [here](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md).**
-## Setup Environment
+The evaluation consists of three steps:
-Please follow [this document](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to set up a local development environment for OpenDevin.
+1. Environment setup: [install python environment](../README.md#development-environment), [configure LLM config](../README.md#configure-opendevin-and-your-llm), and [pull docker](#opendevin-swe-bench-instance-level-docker-support).
+2. [Run inference](#run-inference-on-swe-bench-instances): Generate a edit patch for each Github issue
+3. [Evaluate patches using SWE-Bench docker](#evaluate-generated-patches)
-## OpenDevin SWE-Bench Docker Image
+## Setup Environment and LLM Configuration
-In [OpenDevin-SWE-Bench fork](https://github.com/OpenDevin/OD-SWE-bench.git) (mostly from [original repo](https://github.com/princeton-nlp/SWE-bench) with some fixes), we try to pre-build the **testbed** (i.e., code of the repository we want the agent to edit) AND the **conda environment**, so that in evaluation (inference) time, we can directly leverage existing environments for efficient evaluation.
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
-**We pack everything you need for SWE-Bench inference into one, gigantic, docker image.** To use it:
+## OpenDevin SWE-Bench Instance-level Docker Support
+
+OpenDevin now support using the [official evaluation docker](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md) for both **[inference](#run-inference-on-swe-bench-instances) and [evaluation](#evaluate-generated-patches)**.
+This is now the default behavior.
+
+### Download Docker Images
+
+**(Recommended for reproducibility)** If you have extra local space (e.g., 100GB), you can try pull the [instance-level docker images](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level) we've prepared by running:
```bash
-docker pull ghcr.io/opendevin/eval-swe-bench:full-v1.2.1
+evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh instance
```
-The Docker image contains several important directories:
-
-- `/swe_util/OD-SWE-bench`: root directory for the OD-SWE-bench repository
-- `/swe_util/eval_data`: directory to eval data
- - `/swe_util/eval_data/eval_logs/`: evaluation logs
- - `/swe_util/eval_data/eval_temp/`: temporary folder for the evaluation process
- - `/swe_util/eval_data/instances/`: swe-bench raw instances
- - `/swe_util/eval_data/outputs/`: model or agent outputs
- - `/swe_util/eval_data/testbed_logs/`: logs for testbed building
- - `/swe_util/eval_data/testbeds/`: directory for all testbeds
-- `/swe_util/miniforge3/`: directory for miniforge3
-
-To reproduce how we pack the image, check [this doc](./BUILD_TESTBED_AND_ENV.md).
-
-NOTE: We only support SWE-Bench lite for now. But modifying our existing scripts for full SWE-Bench should be quite straightforward.
-
-## 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"
-
-[sandbox]
-box_type = "ssh"
-timeout = 120
-
-run_as_devin = false
-max_budget_per_task = 4 # 4 USD
-
-[sandbox]
-# SWEBench eval specific
-use_host_network = false
-enable_auto_lint = true
-
-# TODO: Change these to the model you want to evaluate
-[llm.eval_gpt4_1106_preview_llm]
-model = "gpt-4-1106-preview"
-api_key = "XXX"
-temperature = 0.0
-
-[llm.eval_some_openai_compatible_model_llm]
-model = "openai/MODEL_NAME"
-base_url = "https://OPENAI_COMPATIBLE_URL/v1"
-api_key = "XXX"
-temperature = 0.0
-```
-
-## Test if your environment works
-
-Make sure your Docker daemon is running, and you have pulled the `eval-swe-bench:full-v1.2`
-docker image. Then run this python script:
+If you want to save disk space a bit (e.g., with ~50GB free disk space), while speeding up the image pre-build process, you can pull the environment-level docker images:
```bash
-# export USE_INSTANCE_IMAGE=true # if you want to test support for instance-level docker images
-poetry run python evaluation/swe_bench/swe_env_box.py
+evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh env
```
-If you get to the interactive shell successfully, it means your environment works!
-If you see an error, please make sure your `config.toml` contains all
-`SWEBench eval specific` settings as shown in the previous section.
-
## Run Inference on SWE-Bench Instances
+Make sure your Docker daemon is running, and you have pulled the [instance-level docker image](#opendevin-swe-bench-instance-level-docker-support).
+
```bash
./evaluation/swe_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers]
-# e.g., ./evaluation/swe_bench/scripts/run_infer.sh eval_gpt4_1106_preview_llm HEAD CodeActAgent 300
+# e.g., ./evaluation/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 300
```
-where `model_config` is mandatory, while `agent` and `eval_limit` are optional.
+where `model_config` is mandatory, and the rest are optional.
-`model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
+- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
LLM settings, as defined in your `config.toml`.
-
-`git-version`, e.g. `HEAD`, is the git commit hash of the OpenDevin version you would
+- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenDevin version you would
like to evaluate. It could also be a release tag like `0.6.2`.
-
-`agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
+- `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. By
+- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances. By
default, the script evaluates the entire SWE-bench_Lite test set (300 issues). Note:
in order to use `eval_limit`, you must also set `agent`.
-
-`max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
+- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
default, it is set to 30.
-
-`num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
+- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
default, it is set to 1.
There are also two optional environment variables you can set.
```
-export USE_HINT_TEXT=true # if you want to use hint text in the evaluation. Ignore this if you are not sure.
-export USE_INSTANCE_IMAGE=true # if you want to use instance-level docker images
+export USE_HINT_TEXT=true # if you want to use hint text in the evaluation. Default to false. Ignore this if you are not sure.
+export USE_INSTANCE_IMAGE=true # if you want to use instance-level docker images. Default to true
```
-Let's say you'd like to run 10 instances using `eval_gpt4_1106_preview_llm` and CodeActAgent,
+Let's say you'd like to run 10 instances using `llm.eval_gpt4_1106_preview` and CodeActAgent,
then your command would be:
```bash
-./evaluation/swe_bench/scripts/run_infer.sh eval_gpt4_1106_preview_llm HEAD CodeActAgent 10
+./evaluation/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 10
```
+### Specify a subset of tasks to run infer
+
If you would like to specify a list of tasks you'd like to benchmark on, you could
create a `config.toml` under `./evaluation/swe_bench/` folder, and put a list
attribute named `selected_ids`, e.g.
@@ -146,22 +93,12 @@ With `output.jsonl` file, you can run `eval_infer.sh` to evaluate generated patc
**This evaluation is performed using the official dockerized evaluation announced [here](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md).**
-If you want to evaluate existing results, you should first run this to clone existing outputs
+> If you want to evaluate existing results, you should first run this to clone existing outputs
+>```bash
+>git clone https://huggingface.co/spaces/OpenDevin/evaluation evaluation/evaluation_outputs
+>```
-```bash
-git clone https://huggingface.co/spaces/OpenDevin/evaluation evaluation/evaluation_outputs
-```
-
-If you have extra local space (e.g., 500GB), you can try pull the [instance-level docker images](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level) we've prepared to speed up the evaluation by running:
-
-```bash
-evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh instance
-```
-
-If you want to save disk space a bit (e.g., with ~50GB free disk space), while speeding up the image pre-build process, you can pull the environment-level docker images:
-```bash
-evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh env
-```
+NOTE, you should have already pulled the instance-level OR env-level docker images following [this section](#opendevin-swe-bench-instance-level-docker-support).
Then you can run the following:
@@ -171,13 +108,13 @@ Then you can run the following:
./evaluation/swe_bench/scripts/eval_infer.sh evaluation/evaluation_outputs/outputs/swe_bench/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/output.jsonl
```
-PS: You can also pass in a JSONL with [SWE-Bench format](https://github.com/princeton-nlp/SWE-bench/blob/main/tutorials/evaluation.md#-creating-predictions) to `./evaluation/swe_bench/scripts/eval_infer.sh`, where each line is a JSON of `{"model_patch": "XXX", "model_name_or_path": "YYY", "instance_id": "ZZZ"}`.
+> You can also pass in a JSONL with [SWE-Bench format](https://github.com/princeton-nlp/SWE-bench/blob/main/tutorials/evaluation.md#-creating-predictions) to `./evaluation/swe_bench/scripts/eval_infer.sh`, where each line is a JSON of `{"model_patch": "XXX", "model_name_or_path": "YYY", "instance_id": "ZZZ"}`.
The final results will be saved to `evaluation/evaluation_outputs/outputs/swe_bench/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/` with the following files/directory:
- `README.md`: a report showing what are the instances that passed, failed, etc.
- `report.json`: a JSON file that contains keys like `"resolved_ids"` pointing to instance IDs that are resolved by the agent.
-- `eval_outputs/`: a directory of test logs
+- `logs/`: a directory of test logs
## Visualize Results
@@ -189,9 +126,10 @@ git clone https://huggingface.co/spaces/OpenDevin/evaluation
**(optional) setup streamlit environment with conda**:
```bash
+cd evaluation
conda create -n streamlit python=3.10
conda activate streamlit
-pip install streamlit altair st_pages
+pip install -r requirements.txt
```
**run the visualizer**:
diff --git a/evaluation/swe_bench/prompt.py b/evaluation/swe_bench/prompt.py
new file mode 100644
index 0000000000..6b9da9afb1
--- /dev/null
+++ b/evaluation/swe_bench/prompt.py
@@ -0,0 +1,28 @@
+CODEACT_SWE_PROMPT = """Now, you're going to solve this issue on your own. Your terminal session has started and you're in the repository's root directory. You can use any bash commands or the special interface to help you. Edit all the files you need to and run any checks or tests that you want.
+Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
+When you're satisfied with all of the changes you've made, you can run the following command: exit .
+Note however that you cannot use any interactive session commands (e.g. vim) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python .py`.
+
+NOTE ABOUT THE EDIT COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
+
+IMPORTANT TIPS:
+1. Always start by trying to replicate the bug that the issues discusses.
+ If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment, and run it to make sure you can reproduce the bug.
+ Then start trying to fix it.
+ When you think you've fixed the bug, re-run the bug reproduction script to make sure that the bug has indeed been fixed.
+
+ If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file,
+ so that you can be sure that the script indeed ran fine all the way through.
+
+2. If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it!
+
+3. If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, don't just use the scroll_down command multiple times. Instead, use the goto 583 command. It's much quicker.
+
+4. If the bug reproduction script requires inputting/reading a specific file, such as buggy-input.png, and you'd like to understand how to input that file, conduct a search in the existing repo code, to see whether someone else has already done that. Do this by running the command: find_file("buggy-input.png") If that doesn't work, use the linux 'find' command.
+
+5. Always make sure to look at the currently open file and the current working directory (which appears right after the currently open file). The currently open file might be in a different directory than the working directory! Note that some commands, such as 'create', open files, so they might change the current open file.
+
+6. When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
+
+[Current directory: /workspace/{workspace_dir_name}]
+"""
diff --git a/evaluation/swe_bench/run_infer.py b/evaluation/swe_bench/run_infer.py
index e20f080fe3..089b6f0402 100644
--- a/evaluation/swe_bench/run_infer.py
+++ b/evaluation/swe_bench/run_infer.py
@@ -1,34 +1,39 @@
import asyncio
-import logging
+import json
import os
-import pathlib
+import tempfile
+from typing import Any
import pandas as pd
import toml
-import whatthepatch
from datasets import load_dataset
import agenthub
-from evaluation.swe_bench.swe_env_box import SWEBenchSSHBox
+from evaluation.swe_bench.prompt import CODEACT_SWE_PROMPT
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import CmdRunAction
+from opendevin.events.observation import CmdOutputObservation, ErrorObservation
+from opendevin.runtime.runtime import Runtime
-config = load_app_config()
-
-USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false') == 'true'
-USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false') == 'true'
+USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
+USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -41,184 +46,12 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
-def get_test_result(instance, sandbox, workspace_dir_name):
- test_result = {'result': {}, 'metadata': {}}
- # NOTE: if you need to do something in the sandbox to get the correctness metric, modify this function
- try:
- test_patch_parsed = whatthepatch.parse_patch(instance.test_patch)
- # get a list of filepaths that are involved in the patch
- involved_filepaths = set()
- for patch in test_patch_parsed:
- involved_filepaths.add(patch.header.old_path.removeprefix('a/'))
- involved_filepaths.add(patch.header.new_path.removeprefix('b/'))
- involved_filepaths = list(involved_filepaths)
- test_result['metadata']['1_test_patch_parse_success'] = True
- test_result['metadata']['1_test_involved_filepaths'] = involved_filepaths
- except Exception as e:
- logger.error(
- f'Error parsing test patch for instance {instance.instance_id}: {e}'
- )
- test_result['metadata']['1_test_patch_parse_success'] = False
- test_result['metadata']['1_test_patch_parse_error'] = str(e)
- test_result['metadata']['1_test_involved_filepaths'] = None
- involved_filepaths = []
-
- # Try to revert the changes for involved filepaths
- err_code, output = sandbox.execute(f'cd /workspace/{workspace_dir_name}')
- test_result['metadata']['2_revert_test_involved_filepaths_success'] = []
- for filepath in involved_filepaths:
- err_code, output = sandbox.execute(
- f'git checkout {instance["base_commit"]} -- {filepath}'
- )
- if err_code != 0:
- logger.error(f'Error reverting changes for {filepath}: {output}')
- test_result['metadata']['2_revert_test_involved_filepaths_success'].append(
- False
- )
- else:
- test_result['metadata']['2_revert_test_involved_filepaths_success'].append(
- True
- )
-
- # Apply the testcase
- err_code, output = sandbox.execute('git apply $SWE_TASK_DIR/test.patch')
- if err_code != 0:
- logger.error(f'Error applying test patch: {output}')
- test_result['metadata']['3_apply_test_patch_success'] = False
- test_result['metadata']['3_apply_test_patch_error'] = output
- else:
- test_result['metadata']['3_apply_test_patch_success'] = True
-
- # Run the test command
- err_code, output = sandbox.execute(
- '$TEST_CMD > /workspace/$SWE_INSTANCE_ID.log 2>&1'
- )
- if err_code != 0:
- logger.error(f'Error running test command: {output}')
- test_result['metadata']['4_run_test_command_success'] = False
- test_result['metadata']['4_run_test_command_error'] = output
- else:
- test_result['metadata']['4_run_test_command_success'] = True
-
- # Get the test output
- err_code, output = sandbox.execute('cat /workspace/$SWE_INSTANCE_ID.log')
- if err_code != 0:
- logger.error(f'Error getting test output: {output}')
- test_result['metadata']['4_get_test_output_success'] = False
- test_result['metadata']['4_get_test_output_error'] = output
- else:
- test_result['metadata']['4_get_test_output_success'] = True
- test_result['test_output'] = output
-
- # Reformat instance.json
- # $SWE_TASK_DIR/instance.json is a dict {"XXX": "YYY"}, add a [ before and a ] after
- err_code, output = sandbox.execute(
- (
- 'cat $SWE_TASK_DIR/instance.json | sed "s/^{/[{/" | sed "s/}$/}]/" > /workspace/instance.json'
- )
- )
- if err_code != 0:
- logger.error(f'Error creating instance.json: {output}')
- test_result['metadata']['5_reformat_instance_json_success'] = False
- test_result['metadata']['5_reformat_instance_json_error'] = output
- else:
- test_result['metadata']['5_reformat_instance_json_success'] = True
-
- if USE_INSTANCE_IMAGE:
- # instance report is not supported in instance image mode
- test_result['metadata']['6_get_instance_report_success'] = False
- test_result['metadata']['6_get_instance_report_error'] = (
- 'Instance report is not supported in instance image mode.'
- )
-
- else:
- # Get the instance report
- err_code, output = sandbox.execute(
- (
- 'cd /swe_util/OD-SWE-bench '
- '&& export PYTHONPATH=$(pwd):$PYTHONPATH '
- '&& conda run -n swe-bench-eval python swebench/metrics/get_instance_report.py --swe_bench_task /workspace/instance.json --log_path /workspace/$SWE_INSTANCE_ID.log'
- )
- )
- if err_code != 0:
- logger.error(f'Error getting instance report: {output}')
- test_result['metadata']['6_get_instance_report_success'] = False
- test_result['metadata']['6_get_instance_report_error'] = output
- else:
- test_result['metadata']['6_get_instance_report_success'] = True
- test_result['result_raw'] = output
-
- # try to parse output
- for line in output.strip().split('\n'):
- line = line.strip('-')
- try:
- key, value = line.split(':')
- except ValueError:
- # skip this line
- print(f'Error parsing result line: {line}')
- continue
- value = value.strip()
- try:
- value = int(value)
- except ValueError:
- pass
- test_result['result'][key.strip()] = value
- return test_result
+def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
+ return f'{instance.repo}__{instance.version}'.replace('/', '__')
-def process_instance(
- instance: pd.Series,
- metadata: EvalMetadata,
- reset_logger: bool = True,
-):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
-
- workspace_mount_path = os.path.join(config.workspace_mount_path, '_eval_workspace')
- # create process-specific workspace dir
- workspace_mount_path = os.path.join(workspace_mount_path, str(os.getpid()))
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
-
- # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
- if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir,
- 'infer_logs',
- f'instance_{instance.instance_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.instance_id}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # Remove all existing handlers from logger
- for handler in logger.handlers[:]:
- logger.removeHandler(handler)
- os.makedirs(os.path.dirname(log_file), exist_ok=True)
- file_handler = logging.FileHandler(log_file)
- file_handler.setFormatter(
- logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
- )
- logger.addHandler(file_handler)
- else:
- logger.info(f'Starting evaluation for instance {instance.instance_id}.')
-
- # NOTE: this is something special we do for SWE-Bench due to the reason described in the previous section
- # You can omit this if you don't need to setup specialized sandbox
- workspace_dir_name = f'{instance.repo}__{instance.version}'.replace('/', '__')
- sandbox = SWEBenchSSHBox.get_box_for_instance(
- config,
- instance,
- workspace_dir_name,
- workspace_mount_path=workspace_mount_path,
- sandbox_plugins=agenthub.Agent.get_cls(metadata.agent_class).sandbox_plugins,
- use_instance_image=USE_INSTANCE_IMAGE,
- )
-
+def get_instruction(instance: pd.Series, metadata: EvalMetadata):
+ workspace_dir_name = _get_swebench_workspace_dir_name(instance)
# Prepare instruction
if metadata.agent_class == 'CodeActSWEAgent':
instruction = (
@@ -227,39 +60,11 @@ def process_instance(
f'{instance.problem_statement}\n'
'--- END ISSUE ---\n\n'
)
-
if USE_HINT_TEXT and instance.hints_text:
instruction += (
f'--- BEGIN HINTS ---\n{instance.hints_text}\n--- END HINTS ---\n'
)
- instruction += f"""Now, you're going to solve this issue on your own. Your terminal session has started and you're in the repository's root directory. You can use any bash commands or the special interface to help you. Edit all the files you need to and run any checks or tests that you want.
-Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
-When you're satisfied with all of the changes you've made, you can run the following command: exit .
-Note however that you cannot use any interactive session commands (e.g. vim) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python .py`.
-
-NOTE ABOUT THE EDIT COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
-
-IMPORTANT TIPS:
-1. Always start by trying to replicate the bug that the issues discusses.
- If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment, and run it to make sure you can reproduce the bug.
- Then start trying to fix it.
- When you think you've fixed the bug, re-run the bug reproduction script to make sure that the bug has indeed been fixed.
-
- If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file,
- so that you can be sure that the script indeed ran fine all the way through.
-
-2. If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it!
-
-3. If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, don't just use the scroll_down command multiple times. Instead, use the goto 583 command. It's much quicker.
-
-4. If the bug reproduction script requires inputting/reading a specific file, such as buggy-input.png, and you'd like to understand how to input that file, conduct a search in the existing repo code, to see whether someone else has already done that. Do this by running the command: find_file("buggy-input.png") If that doesn't work, use the linux 'find' command.
-
-5. Always make sure to look at the currently open file and the current working directory (which appears right after the currently open file). The currently open file might be in a different directory than the working directory! Note that some commands, such as 'create', open files, so they might change the current open file.
-
-6. When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
-
-[Current directory: /workspace/{workspace_dir_name}]
-"""
+ instruction += CODEACT_SWE_PROMPT.format(workspace_dir_name=workspace_dir_name)
else:
# Testing general agents
instruction = (
@@ -277,61 +82,278 @@ IMPORTANT TIPS:
)
# NOTE: You can actually set slightly different instruction for different agents
- instruction += AGENT_CLS_TO_INST_SUFFIX[agent.__class__.__name__]
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
+ return instruction
+
+
+def get_config(
+ instance: pd.Series,
+ metadata: EvalMetadata,
+) -> AppConfig:
+ SWE_BENCH_CONTAINER_IMAGE = 'ghcr.io/opendevin/eval-swe-bench:full-v1.2.1'
+ if USE_INSTANCE_IMAGE:
+ # We use a different instance image for the each instance of swe-bench eval
+ container_image = 'sweb.eval.x86_64.' + instance['instance_id']
+ else:
+ container_image = SWE_BENCH_CONTAINER_IMAGE
+
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_budget_per_task=4,
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image=container_image,
+ enable_auto_lint=True,
+ use_host_network=False,
+ # large enough timeout, since some testcases take very long to run
+ timeout=300,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required
+):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info('-' * 30)
+ logger.info('BEGIN Runtime Initialization Fn')
+ logger.info('-' * 30)
+ workspace_dir_name = _get_swebench_workspace_dir_name(instance)
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(
+ command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc"""
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ if USE_INSTANCE_IMAGE:
+ # inject the init script
+ script_dir = os.path.dirname(__file__)
+
+ # inject the instance info
+ action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert (
+ obs.exit_code == 0
+ ), f'Failed to create /swe_util/eval_data/instances: {obs.content}'
+
+ swe_instance_json_name = 'swe-bench-instance.json'
+ with tempfile.TemporaryDirectory() as temp_dir:
+ # Construct the full path for the desired file name within the temporary directory
+ temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
+ # Write to the file with the desired name within the temporary directory
+ with open(temp_file_path, 'w') as f:
+ if not isinstance(instance, dict):
+ json.dump([instance.to_dict()], f)
+ else:
+ json.dump([instance], f)
+
+ # Copy the file to the desired location
+ await runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
+
+ # inject the instance swe entry
+ await runtime.copy_to(
+ str(os.path.join(script_dir, 'scripts/setup/instance_swe_entry.sh')),
+ '/swe_util/',
+ )
+ action = CmdRunAction(command='cat ~/.bashrc')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='source ~/.bashrc')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='source /swe_util/instance_swe_entry.sh')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+ else:
+ action = CmdRunAction(command='source /swe_util/swe_entry.sh')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert (
+ obs.exit_code == 0
+ ), f'Failed to source /swe_util/swe_entry.sh: {obs.content}'
+
+ action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='git reset --hard')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(
+ command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
+ )
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ logger.info('-' * 30)
+ logger.info('END Runtime Initialization Fn')
+ logger.info('-' * 30)
+
+
+async def complete_runtime(
+ runtime: Runtime,
+ instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info('-' * 30)
+ logger.info('BEGIN Runtime Completion Fn')
+ logger.info('-' * 30)
+ obs: CmdOutputObservation
+ workspace_dir_name = _get_swebench_workspace_dir_name(instance)
+
+ action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='git config --global core.pager ""')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='git add -A')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ assert obs.exit_code == 0
+
+ n_retries = 0
+ git_patch = None
+ while n_retries < 5:
+ action = CmdRunAction(
+ command=f'git diff --no-color --cached {instance["base_commit"]}',
+ keep_prompt=False,
+ )
+ action.timeout = 600 + 100 * n_retries
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ n_retries += 1
+ if isinstance(obs, CmdOutputObservation):
+ if obs.exit_code == 0:
+ git_patch = obs.content.strip()
+ break
+ else:
+ logger.info('Failed to get git diff, retrying...')
+ await asyncio.sleep(10)
+ elif isinstance(obs, ErrorObservation):
+ logger.error(f'Error occurred: {obs.content}. Retrying...')
+ await asyncio.sleep(10)
+ else:
+ raise ValueError(f'Unexpected observation type: {type(obs)}')
+
+ logger.info('-' * 30)
+ logger.info('END Runtime Completion Fn')
+ logger.info('-' * 30)
+ return {'git_patch': git_patch}
+
+
+async def process_instance(
+ instance: pd.Series,
+ metadata: EvalMetadata,
+ reset_logger: bool = True,
+) -> EvalOutput:
+ config = get_config(instance, metadata)
+
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
+ if reset_logger:
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {instance.instance_id}.')
+
+ runtime = await create_runtime(config, sid=instance.instance_id)
+ await initialize_runtime(runtime, instance)
+
+ instruction = get_instruction(instance, metadata)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
- agent.__class__.__name__
- ],
- sandbox=sandbox,
- sid=instance.instance_id,
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
)
# ======= THIS IS SWE-Bench specific =======
# Get git patch
- git_patch = sandbox.get_diff_patch()
- logger.info(f'Got git diff for instance {instance.instance_id}')
+ return_val = await complete_runtime(runtime, instance)
+ git_patch = return_val['git_patch']
+ logger.info(
+ f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
+ )
# ==========================================
# ======= Attempt to evaluate the agent's edits =======
- # TODO: if you need to do something in the sandbox to get the correctness metric, modify this function
- test_result = get_test_result(instance, sandbox, workspace_dir_name)
+ # we use eval_infer.sh to evaluate the agent's edits, not here
+ # because the agent may alter the environment / testcases
+ test_result = {
+ 'git_patch': git_patch,
+ }
# If you are working on some simpler 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.')
- metrics = state.metrics.get() if state.metrics else None
-
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = state.history.compatibility_for_eval_history_pairs()
+ metrics = state.metrics.get() if state.metrics else None
# Save the output
- output = {
- 'instance_id': instance.instance_id,
- 'swe_instance': instance.to_dict(), # SWE Bench specific
- 'instruction': instruction,
- 'git_patch': git_patch, # SWE Bench specific
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': test_result,
- }
-
- # Close the sandbox
- sandbox.close()
+ output = EvalOutput(
+ instance_id=instance.instance_id,
+ instruction=instruction,
+ instance=instance.to_dict(), # SWE Bench specific
+ test_result=test_result,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ )
return output
@@ -359,11 +381,12 @@ if __name__ == '__main__':
dataset = load_dataset('princeton-nlp/SWE-bench_Lite')
swe_bench_tests = filter_dataset(dataset['test'].to_pandas(), 'instance_id')
- id_column = 'instance_id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- if args.llm_config and llm_config is None:
- raise ValueError(f'Could not find LLM config {args.llm_config}')
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
details = {}
_agent_cls = agenthub.Agent.get_cls(args.agent_cls)
@@ -383,14 +406,10 @@ if __name__ == '__main__':
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(
- swe_bench_tests, output_file, args.eval_n_limit, id_column
- )
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
+
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/swe_bench/scripts/eval/convert_od_output_to_swe_json.py b/evaluation/swe_bench/scripts/eval/convert_od_output_to_swe_json.py
index 49f167357c..41d4fe6dae 100644
--- a/evaluation/swe_bench/scripts/eval/convert_od_output_to_swe_json.py
+++ b/evaluation/swe_bench/scripts/eval/convert_od_output_to_swe_json.py
@@ -45,9 +45,16 @@ def process_git_patch(patch):
def convert_row_to_swebench_format(row):
+ if 'git_patch' in row:
+ model_patch = row['git_patch']
+ elif 'test_result' in row and 'git_patch' in row['test_result']:
+ model_patch = row['test_result']['git_patch']
+ else:
+ raise ValueError(f'Row {row} does not have a git_patch')
+
return {
'instance_id': row['instance_id'],
- 'model_patch': process_git_patch(row['git_patch']),
+ 'model_patch': process_git_patch(model_patch),
'model_name_or_path': model_name,
}
diff --git a/evaluation/swe_bench/scripts/run_infer.sh b/evaluation/swe_bench/scripts/run_infer.sh
index 65a76376c9..a31e2c311f 100755
--- a/evaluation/swe_bench/scripts/run_infer.sh
+++ b/evaluation/swe_bench/scripts/run_infer.sh
@@ -27,8 +27,8 @@ if [ -z "$MAX_ITER" ]; then
fi
if [ -z "$USE_INSTANCE_IMAGE" ]; then
- echo "USE_INSTANCE_IMAGE not specified, use default false"
- USE_INSTANCE_IMAGE=false
+ echo "USE_INSTANCE_IMAGE not specified, use default true"
+ USE_INSTANCE_IMAGE=true
fi
export USE_INSTANCE_IMAGE=$USE_INSTANCE_IMAGE
diff --git a/evaluation/swe_bench/scripts/setup/instance_swe_entry.sh b/evaluation/swe_bench/scripts/setup/instance_swe_entry.sh
index c0b6ceb5cd..1d4051fd84 100644
--- a/evaluation/swe_bench/scripts/setup/instance_swe_entry.sh
+++ b/evaluation/swe_bench/scripts/setup/instance_swe_entry.sh
@@ -45,7 +45,11 @@ echo "$item" | jq -r '.patch' > $SWE_TASK_DIR/gold.patch
echo "$item" | jq 'del(.test_patch, .patch)' > $SWE_TASK_DIR/instance.json
# Clear the workspace
-rm -rf /workspace/*
+if [ -d /workspace ]; then
+ rm -rf /workspace/*
+else
+ mkdir /workspace
+fi
# Copy repo to workspace
if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
@@ -61,7 +65,7 @@ mkdir -p $SWE_TASK_DIR/reset_testbed_log_dir
REPO_PATH=/workspace/$WORKSPACE_NAME
echo "Repo Path: $REPO_PATH"
-echo "Test Command: $TEST_CMD"
+# echo "Test Command: $TEST_CMD"
echo "export REPO_PATH=\"$REPO_PATH\"" >> ~/.bashrc
# echo "export TEST_CMD=\"$TEST_CMD\"" >> ~/.bashrc
diff --git a/evaluation/swe_bench/swe_env_box.py b/evaluation/swe_bench/swe_env_box.py
deleted file mode 100644
index 3fed5ea13a..0000000000
--- a/evaluation/swe_bench/swe_env_box.py
+++ /dev/null
@@ -1,313 +0,0 @@
-import json
-import os
-import sys
-import tempfile
-import uuid
-
-from datasets import load_dataset
-from swebench.harness.constants import MAP_REPO_TO_TEST_FRAMEWORK
-from swebench.harness.utils import get_test_directives
-
-from opendevin.core.config import AppConfig, SandboxConfig, load_app_config
-from opendevin.core.logger import opendevin_logger as logger
-from opendevin.runtime.docker.ssh_box import DockerSSHBox
-from opendevin.runtime.plugins import (
- AgentSkillsRequirement,
- JupyterRequirement,
- PluginRequirement,
-)
-
-SWE_BENCH_CONTAINER_IMAGE = 'ghcr.io/opendevin/eval-swe-bench:full-v1.2.1'
-
-
-def get_image_name_from_instance_id(instance_id: str) -> str:
- return 'sweb.eval.x86_64.' + instance_id
-
-
-class SWEBenchSSHBox(DockerSSHBox):
- def __init__(
- self,
- config: AppConfig,
- container_image: str,
- timeout: int = 120,
- sid: str | None = None,
- swe_instance_id: str | None = None,
- swe_instance: dict | None = None,
- skip_workspace_mount: bool = True,
- sandbox_plugins: list[PluginRequirement] = [], # noqa: B006
- workspace_dir_name: str | None = None,
- use_instance_image: bool = False,
- ):
- if swe_instance_id is None:
- raise ValueError('swe_instance_id must be provided!')
- self.swe_instance_id = swe_instance_id
- self.swe_instance = swe_instance
- self.skip_workspace_mount = skip_workspace_mount
- self.workspace_dir_name = workspace_dir_name
-
- assert (
- container_image is not None
- ), 'container_image is required for SWEBenchSSHBox!'
- # Need to run as root to use SWEBench container
- sid = f'swe_bench_{swe_instance_id}_' + str(uuid.uuid4())
- logger.info(f'===Using container image: {container_image}')
- super().__init__(
- config=SandboxConfig(container_image=container_image, timeout=timeout),
- persist_sandbox=config.persist_sandbox,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- ssh_hostname=config.ssh_hostname,
- ssh_password=config.ssh_password,
- ssh_port=config.ssh_port,
- sid=sid,
- )
- self.init_plugins(sandbox_plugins)
-
- exit_code, output = self.execute('mv ~/.bashrc ~/.bashrc.bak')
- assert exit_code == 0, f'Failed to backup ~/.bashrc: {output}'
-
- exit_code, output = self.execute(
- f"echo 'export SWE_INSTANCE_ID={self.swe_instance_id}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo \"alias git='git --no-pager'\" >> ~/.bashrc"
- )
- assert exit_code == 0, f'Failed to set SWE_INSTANCE_ID in ~/.bashrc: {output}'
-
- logger.info('Sourcing swe_entry.sh to set up environment variables')
- logger.info(
- 'Initialization of SWEBench may take approximately 10 minutes due to long-running installations, such as those requiring compilation.'
- )
- logger.info(f'Use instance image: {use_instance_image}')
- if use_instance_image:
- # we directly inject the instance info into the container and the init script
- script_dir = os.path.dirname(__file__)
-
- # inject test command
- test_type = MAP_REPO_TO_TEST_FRAMEWORK[swe_instance['repo']][
- swe_instance['version']
- ]
- swe_instance['test_directives'] = get_test_directives(swe_instance)
- swe_instance['test_cmd'] = (
- f"{test_type} {' '.join(swe_instance['test_directives'])}"
- )
- exit_code, output = self.execute(
- f"""echo "export TEST_CMD='{swe_instance["test_cmd"]}'" >> ~/.bashrc"""
- )
- # assert exit_code == 0, f'Failed to set TEST_CMD in ~/.bashrc: {output}'
-
- # inject the instance info
- self.execute('mkdir -p /swe_util/eval_data/instances')
- swe_instance_json_name = 'swe-bench-instance.json'
- with tempfile.TemporaryDirectory() as temp_dir:
- # Construct the full path for the desired file name within the temporary directory
- temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
- # Write to the file with the desired name within the temporary directory
- with open(temp_file_path, 'w') as f:
- if not isinstance(swe_instance, dict):
- json.dump([swe_instance.to_dict()], f)
- else:
- json.dump([swe_instance], f)
-
- # Copy the file to the desired location
- self.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
-
- # inject the init script
- self.copy_to(
- str(os.path.join(script_dir, 'scripts/setup/instance_swe_entry.sh')),
- '/swe_util/',
- )
- self.execute('cat ~/.bashrc')
- self.execute('source ~/.bashrc')
-
- self.execute('source /swe_util/instance_swe_entry.sh', timeout=600)
- logger.info('exit code: %d', exit_code)
- logger.info(output)
- assert exit_code == 0, f'Failed to source swe_entry.sh: {output}'
- logger.info('Sourced swe_entry.sh successfully')
- else:
- exit_code, output = self.execute(
- 'source /swe_util/swe_entry.sh', timeout=600
- )
- logger.info('exit code: %d', exit_code)
- logger.info(output)
- assert exit_code == 0, f'Failed to source swe_entry.sh: {output}'
- logger.info('Sourced swe_entry.sh successfully')
-
- @property
- def volumes(self):
- if self.skip_workspace_mount:
- return {
- k: v
- for k, v in super().volumes.items()
- if not v['bind'] == self.sandbox_workspace_dir
- }
- return super().volumes
-
- @classmethod
- def get_box_for_instance(
- cls,
- config: AppConfig,
- instance,
- workspace_dir_name=None,
- skip_workspace_mount: bool = True,
- workspace_mount_path: str | None = None,
- sandbox_plugins: list[PluginRequirement] = [], # noqa: B006
- use_instance_image: bool = False,
- ) -> 'SWEBenchSSHBox':
- if workspace_dir_name is None:
- workspace_dir_name = f"{instance['repo']}__{instance['version']}".replace(
- '/', '__'
- )
- old_workspace_base = config.workspace_base
- old_workspace_mount_path = config.workspace_mount_path
-
- try:
- config.workspace_base = workspace_mount_path
- config.workspace_mount_path = workspace_mount_path
-
- # linting python after editing helps LLM fix indentations
- config.sandbox.enable_auto_lint = True
- # Need to run as root to use SWEBench container
- config.run_as_devin = False
- if use_instance_image:
- container_image = get_image_name_from_instance_id(
- instance['instance_id']
- )
- else:
- container_image = SWE_BENCH_CONTAINER_IMAGE
- sandbox = cls(
- container_image=container_image,
- config=config,
- swe_instance_id=instance['instance_id'],
- swe_instance=instance,
- skip_workspace_mount=skip_workspace_mount,
- sandbox_plugins=sandbox_plugins,
- workspace_dir_name=workspace_dir_name,
- use_instance_image=use_instance_image,
- )
- logger.info(f"SSH box started for instance {instance['instance_id']}.")
-
- # cd to the repo
- exit_code, output = sandbox.execute(f'cd /workspace/{workspace_dir_name}')
- if exit_code != 0:
- logger.error(f'Failed to cd to the repo: {output}')
- sys.exit(1)
-
- # remove all future commits & remote following Devin
- # https://www.cognition-labs.com/post/swe-bench-technical-report
- exit_code, output = sandbox.execute('git reset --hard')
- if exit_code != 0:
- logger.error(f'Failed to reset the repo: {output}')
- sys.exit(1)
- exit_code, output = sandbox.execute(
- 'for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
- )
- if exit_code != 0:
- logger.error(f'Failed to remove remote: {output}')
- sys.exit(1)
- except Exception:
- raise
- finally:
- # restore workspace_base and workspace_mount_path
- config.workspace_base = old_workspace_base
- config.workspace_mount_path = old_workspace_mount_path
- return sandbox
-
- def get_diff_patch(self):
- # add everything to the index
- exit_code, output = self.execute(f'cd /workspace/{self.workspace_dir_name}')
- if exit_code != 0:
- logger.error('Failed to cd to the repo')
- return ''
-
- exit_code, _output = self.execute('git config --global core.pager ""')
- if exit_code != 0:
- logger.error('Failed to change git config')
- return ''
-
- # add everything to the index
- exit_code, output = self.execute('git add -A')
- if exit_code != 0:
- logger.error('Failed to add everything to the index')
- return ''
-
- # get the git diff
- exit_code, git_patch = self.execute(
- f'git diff --no-color --cached {self.swe_instance["base_commit"]}'
- )
- if exit_code != 0:
- logger.error('Failed to get git diff')
- return ''
- return git_patch
-
-
-if __name__ == '__main__':
- config = load_app_config()
-
- # 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
- dataset = load_dataset('princeton-nlp/SWE-bench_Lite')
- swe_bench_tests = dataset['test'].to_pandas()
- USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false') == 'true'
- logger.info(f'USE_INSTANCE_IMAGE: {USE_INSTANCE_IMAGE}')
-
- # INSTANCE_ID = 'django__django-11099'
- INSTANCE_ID = 'astropy__astropy-12907'
- swe_bench_tests = swe_bench_tests[swe_bench_tests['instance_id'] == INSTANCE_ID]
- EXAMPLE_INSTANCE = swe_bench_tests.iloc[0].to_dict()
-
- sandbox = SWEBenchSSHBox.get_box_for_instance(
- config=config,
- instance=EXAMPLE_INSTANCE,
- sandbox_plugins=[AgentSkillsRequirement(), JupyterRequirement()],
- use_instance_image=USE_INSTANCE_IMAGE,
- )
-
- # PRE TEST
- exit_code, output = sandbox.execute('cd $REPO_PATH')
- assert exit_code == 0, 'Failed to cd $REPO_PATH'
- logger.info(f'cd $REPO_PATH: {output}')
-
- # apply test patch
- exit_code, output = sandbox.execute('git apply $SWE_TASK_DIR/test.patch')
- assert exit_code == 0, 'Failed to apply test patch'
- logger.info(f'git apply $SWE_TASK_DIR/test.patch: {output}')
-
- # TEST
- exit_code, output = sandbox.execute('$TEST_CMD')
- assert exit_code == 1, 'Expected exit code 1 (since this is a FAIL_TO_PASS)'
- logger.info(f'$TEST_CMD:\n{output}')
-
- # apply gold patch
- exit_code, output = sandbox.execute('git apply $SWE_TASK_DIR/gold.patch')
- logger.info('exit code: %d', exit_code)
- logger.info(f'git apply $SWE_TASK_DIR/gold.patch: {output}')
-
- # TEST
- exit_code, output = sandbox.execute('$TEST_CMD')
- assert exit_code == 0, 'Expected exit code 0 (since we applied the gold patch)'
- logger.info(f'$TEST_CMD:\n{output}')
-
- # Reset the repo
- exit_code, output = sandbox.execute('git reset --hard')
- assert exit_code == 0, 'Failed to reset the repo'
- logger.info(f'git reset --hard: {output}')
-
- sys.stdout.flush()
- try:
- while True:
- try:
- user_input = input('>>> ')
- except EOFError:
- logger.info('Exiting...')
- break
- if user_input.lower() == 'exit':
- logger.info('Exiting...')
- break
- exit_code, output = sandbox.execute(user_input)
- logger.info('exit code: %d', exit_code)
- logger.info(output)
- sys.stdout.flush()
- except KeyboardInterrupt:
- logger.info('Exiting...')
- sandbox.close()
diff --git a/evaluation/toolqa/Dockerfile b/evaluation/toolqa/Dockerfile
new file mode 100644
index 0000000000..a15b774fcf
--- /dev/null
+++ b/evaluation/toolqa/Dockerfile
@@ -0,0 +1,17 @@
+FROM ubuntu:22.04
+
+RUN apt-get update && apt-get install -y python3 python3-pip
+
+RUN mkdir /workspace
+WORKDIR /workspace
+
+
+COPY data/ /workspace/data/
+COPY tools/ /workspace/tools/
+
+# TODO: NEED TO FIGURE DEPENDECIES FOR THESE TOOLS
+
+# pushd evaluation/toolqa
+# mkdir data
+# python3 -c "from utils import download_data, download_tools; download_data('/workspace'); download_tools('/workspace')"
+# docker build --network host -t xingyaoww/od-eval-toolqa .
diff --git a/evaluation/toolqa/README.md b/evaluation/toolqa/README.md
index 058ac96bee..9e9dba3915 100644
--- a/evaluation/toolqa/README.md
+++ b/evaluation/toolqa/README.md
@@ -2,13 +2,9 @@
This folder contains an evaluation harness we built on top of the original [ToolQA](https://github.com/night-chen/ToolQA) ([paper](https://arxiv.org/pdf/2306.13304)).
-## Setup Environment
+## Setup Environment and LLM Configuration
-Please follow [this document](https://github.com/OpenDevin/OpenDevin/blob/main/Development.md) to setup local development environment for OpenDevin.
-
-## Configure OpenDevin and your LLM
-
-Run `make setup-config` to set up the `config.toml` file if it does not exist at the root of the workspace.
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Run Inference on ToolQA Instances
diff --git a/evaluation/toolqa/run_infer.py b/evaluation/toolqa/run_infer.py
index 7653d35568..2eef9d2380 100644
--- a/evaluation/toolqa/run_infer.py
+++ b/evaluation/toolqa/run_infer.py
@@ -1,29 +1,31 @@
import asyncio
-import logging
import os
-import pathlib
from typing import Any
import pandas as pd
+from evaluation.toolqa.utils import encode_question, eval_answer, get_data
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, get_parser, load_app_config
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ get_parser,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-
-from .utils import download_data, download_tools, encode_question, eval_answer, get_data
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import CmdRunAction
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.runtime import Runtime
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -34,59 +36,83 @@ AGENT_CLS_TO_INST_SUFFIX = {
}
-def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool = True):
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- # create process-specific workspace dir
- # we will create a workspace directory for EACH process
- # so that different agent don't interfere with each other.
- workspace_mount_path = config.workspace_mount_path
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
+def get_config(
+ metadata: EvalMetadata,
+) -> AppConfig:
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
+
+
+async def initialize_runtime(runtime: Runtime):
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = CmdRunAction(command='cd /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ await runtime.add_env_vars({'WOLFRAM_ALPHA_APPID': args.wolfram_alpha_appid})
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+
+
+async def process_instance(
+ instance: Any, metadata: EvalMetadata, reset_logger: bool = True
+):
+ config = get_config(metadata)
- # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
- eval_output_dir = metadata.eval_output_dir
qid = instance.qid
question = instance.question
answer = instance.answer
+
+ # Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(eval_output_dir, 'logs', f'instance_{qid}.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 {qid}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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}')
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, qid, log_dir)
+ else:
+ logger.info(f'Starting evaluation for instance {qid}.')
# Prepare instruction
instruction = encode_question(question)
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[agent.__class__.__name__]
- # logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
+ instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
+ logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
+
+ runtime = await create_runtime(config, sid=qid)
+ await initialize_runtime(runtime)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- instruction,
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
- agent.__class__.__name__
- ],
- sid=qid,
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=instruction,
+ runtime=runtime,
+ fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[metadata.agent_class],
)
# ======= Attempt to evaluate the agent's edits =======
# If you are working on simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
@@ -110,17 +136,17 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
- output = {
- 'qid': qid,
- 'text': model_answer_raw,
- 'correct': correct,
- 'answer_id': 'None',
- 'model_id': metadata.model_name,
- 'metadata': metadata,
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- }
+ output = EvalOutput(
+ instance_id=qid,
+ test_result={
+ 'model_answer_raw': model_answer_raw,
+ 'correct': correct,
+ },
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ )
return output
@@ -145,8 +171,12 @@ if __name__ == '__main__':
default='YOUR_WOLFRAMALPHA_APPID',
)
args, _ = parser.parse_known_args()
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
dataset = ''
hardness = ''
@@ -168,14 +198,9 @@ if __name__ == '__main__':
if args.hardness not in ['easy', 'hard']:
raise ValueError('Please choose from easy and hard for hardness.')
- # workspace_mount_path = os.path.join(config.workspace_mount_path, '_eval_workspace')
- workspace_mount_path = config.workspace_mount_path
- pathlib.Path(workspace_mount_path).mkdir(parents=True, exist_ok=True)
toolqa_test = pd.DataFrame(get_data(dataset, hardness))
- toolqa_data_path = download_data(workspace_mount_path)
- toolqa_tool_path = download_tools(workspace_mount_path, args.wolfram_alpha_appid)
+ toolqa_test.rename(columns={'qid': 'instance_id'}, inplace=True)
- id_column = 'qid'
metadata = make_metadata(
llm_config,
f'toolqa-{args.dataset}-{args.hardness}',
@@ -184,12 +209,9 @@ if __name__ == '__main__':
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(toolqa_test, output_file, args.eval_n_limit, id_column)
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances = prepare_dataset(toolqa_test, output_file, args.eval_n_limit)
+ asyncio.run(
+ run_evaluation(
+ instances, metadata, output_file, args.eval_num_workers, process_instance
+ )
)
diff --git a/evaluation/toolqa/utils.py b/evaluation/toolqa/utils.py
index 4155b74381..c942de5cf8 100644
--- a/evaluation/toolqa/utils.py
+++ b/evaluation/toolqa/utils.py
@@ -4,11 +4,12 @@ import re
import string
import zipfile
-import gdown
import requests
def download_data(dir):
+ import gdown
+
data_path = os.path.join(dir, 'data/external_corpus')
if os.path.exists(data_path):
return data_path
@@ -19,6 +20,7 @@ def download_data(dir):
zip_ref.extractall(os.path.join(dir, 'data'))
if os.path.exists(zip_path):
os.remove(zip_path)
+ print(f'Data saved to {data_path}')
return data_path
@@ -42,6 +44,7 @@ def download_tools(dir, wolfram_alpha_appid='YOUR_WOLFRAMALPHA_APPID'):
output_file = os.path.join(tool_path, tool.split('/')[1])
with open(output_file, 'wb') as f:
f.write(response.content)
+ print(f'Tool saved to {output_file}')
with open(os.path.join(tool_path, 'calculator.py'), 'r') as f:
content = f.read()
new_content = content.replace('YOUR_WOLFRAMALPHA_APPID', wolfram_alpha_appid)
@@ -64,14 +67,29 @@ def download_tools(dir, wolfram_alpha_appid='YOUR_WOLFRAMALPHA_APPID'):
f.write(new_content)
+LOCAL_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+
+
def get_data(dataset, hardness):
- data = []
- url = f'https://raw.githubusercontent.com/night-chen/ToolQA/main/data/questions/{hardness}/{dataset}-{hardness}.jsonl'
- url = requests.get(url)
- if url.status_code == 200:
- lines = url.text.splitlines()
- for line in lines:
- data.append(json.loads(line))
+ data_path = os.path.join(LOCAL_DATA_DIR, f'{dataset}-{hardness}.jsonl')
+ if os.path.exists(data_path):
+ print(f'Loading data from {data_path}')
+ with open(data_path, 'r') as f:
+ return json.load(f)
+ else:
+ print(
+ f'Downloading data from https://raw.githubusercontent.com/night-chen/ToolQA/main/data/questions/{hardness}/{dataset}-{hardness}.jsonl'
+ )
+ data = []
+ url = f'https://raw.githubusercontent.com/night-chen/ToolQA/main/data/questions/{hardness}/{dataset}-{hardness}.jsonl'
+ url = requests.get(url)
+ if url.status_code == 200:
+ lines = url.text.splitlines()
+ for line in lines:
+ data.append(json.loads(line))
+ with open(data_path, 'w') as f:
+ json.dump(data, f)
+ print(f'Data saved to {data_path}')
return data
diff --git a/evaluation/utils/shared.py b/evaluation/utils/shared.py
index 8b3fe33b93..771157f155 100644
--- a/evaluation/utils/shared.py
+++ b/evaluation/utils/shared.py
@@ -1,12 +1,13 @@
+import asyncio
import json
+import logging
import multiprocessing as mp
import os
import pathlib
import subprocess
import time
-from asyncio.log import logger
from concurrent.futures import ProcessPoolExecutor
-from typing import Any, Callable
+from typing import Any, Awaitable, Callable
import pandas as pd
from pydantic import BaseModel
@@ -14,6 +15,8 @@ from tqdm import tqdm
from opendevin.controller.state.state import State
from opendevin.core.config import LLMConfig
+from opendevin.core.logger import get_console_handler
+from opendevin.core.logger import opendevin_logger as logger
from opendevin.events.action import Action
from opendevin.events.action.message import MessageAction
@@ -38,6 +41,31 @@ class EvalMetadata(BaseModel):
return json.dumps(dumped_dict)
+class EvalOutput(BaseModel):
+ # NOTE: User-specified
+ instance_id: str
+ instruction: str
+ # output of the evaluation
+ # store anything that is needed for the score calculation
+ test_result: dict[str, Any]
+
+ # Interaction info
+ metadata: EvalMetadata
+ history: list[tuple[dict[str, Any], dict[str, Any]]]
+ metrics: dict[str, Any]
+ error: str | None = None
+
+ # Optionally save the input test instance
+ instance: dict[str, Any] | None = None
+
+ def model_dump_json(self, *args, **kwargs):
+ dumped = super().model_dump_json(*args, **kwargs)
+ dumped_dict = json.loads(dumped)
+ # Apply custom serialization for metadata (to avoid leaking sensitive information)
+ dumped_dict['metadata'] = json.loads(self.metadata.model_dump_json())
+ return json.dumps(dumped_dict)
+
+
def codeact_user_response(
state: State,
encapsulate_solution: bool = False,
@@ -136,7 +164,11 @@ def make_metadata(
return metadata
-def prepare_dataset(dataset: pd.DataFrame, output_file, eval_n_limit, id_column):
+def prepare_dataset(dataset: pd.DataFrame, output_file: str, eval_n_limit: int):
+ assert (
+ 'instance_id' in dataset.columns
+ ), "Expected 'instance_id' column in the dataset. You should define your own unique identifier for each instance and use it as the 'instance_id' column."
+ id_column = 'instance_id'
logger.info(f'Writing evaluation output to {output_file}')
finished_ids = set()
if os.path.exists(output_file):
@@ -164,14 +196,16 @@ def prepare_dataset(dataset: pd.DataFrame, output_file, eval_n_limit, id_column)
return pd.DataFrame(new_dataset)
-def run_evaluation(
+async def run_evaluation(
dataset: pd.DataFrame,
metadata: EvalMetadata,
output_file: str,
num_workers: int,
- process_instance_func: Callable[[pd.Series, EvalMetadata, bool], Any],
- id_column: str,
+ process_instance_func: Callable[
+ [pd.Series, EvalMetadata, bool], Awaitable[EvalOutput]
+ ],
):
+ use_multiprocessing = num_workers > 1
logger.info(
f'Evaluation started with Agent {metadata.agent_class}, '
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.'
@@ -179,35 +213,77 @@ def run_evaluation(
pbar = tqdm(total=len(dataset))
output_fp = open(output_file, 'a')
- def update_progress(future):
+ async def update_progress(future):
pbar.update(1)
- output = future.result()
- pbar.set_description(f'Instance {output[id_column]}')
- pbar.set_postfix_str(f'Test Result: {output["test_result"]["result"]}')
+ output: EvalOutput = await future if use_multiprocessing else future
+
+ 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[id_column]}: {output["test_result"]["result"]}'
+ f'Finished evaluation for instance {output.instance_id}: {output.test_result}'
)
- output_fp.write(json.dumps(output) + '\n')
+ output_fp.write(json.dumps(output.model_dump()) + '\n')
output_fp.flush()
try:
- with ProcessPoolExecutor(num_workers) as executor:
- futures = []
- for _, instance in dataset.iterrows():
- future = executor.submit(
- process_instance_func,
- instance,
- metadata,
- bool(num_workers > 1),
- )
- future.add_done_callback(update_progress)
- futures.append(future)
+ if use_multiprocessing:
+ with ProcessPoolExecutor(num_workers) as executor:
+ loop = asyncio.get_event_loop()
+ futures = []
+ for _, instance in dataset.iterrows():
+ future = loop.run_in_executor(
+ executor,
+ process_instance_func,
+ instance,
+ metadata,
+ bool(num_workers > 1),
+ )
+ futures.append(update_progress(future))
+
+ await asyncio.gather(*futures)
+ # Use plain for loop for single process for easier debugging
+ else:
+ assert num_workers == 1
+ for _, instance in dataset.iterrows():
+ output = await process_instance_func(instance, metadata, False)
+ await update_progress(output)
- for future in futures:
- future.result()
except KeyboardInterrupt:
print('KeyboardInterrupt received. Cleaning up...')
cleanup()
output_fp.close()
logger.info('Evaluation finished.')
+
+
+def reset_logger_for_multiprocessing(
+ logger: logging.Logger, instance_id: str, log_dir: str
+):
+ """Reset the logger for multiprocessing.
+
+ Save logs to a separate file for each process, instead of trying to write to the
+ same file/console from multiple processes.
+ """
+ # Set up logger
+ log_file = os.path.join(
+ log_dir,
+ f'instance_{instance_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_id}.\n'
+ f'Hint: run "tail -f {log_file}" to see live logs in a separate shell'
+ )
+ # Remove all existing handlers from logger
+ for handler in logger.handlers[:]:
+ logger.removeHandler(handler)
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
+ file_handler = logging.FileHandler(log_file)
+ file_handler.setFormatter(
+ logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+ )
+ logger.addHandler(file_handler)
diff --git a/evaluation/utils/version_control.sh b/evaluation/utils/version_control.sh
index f58f9bdce9..2ab2ada484 100644
--- a/evaluation/utils/version_control.sh
+++ b/evaluation/utils/version_control.sh
@@ -1,13 +1,11 @@
checkout_eval_branch() {
if [ -z "$COMMIT_HASH" ]; then
echo "Commit hash not specified, use current git commit"
- build_sandbox
return 0
fi
if git diff --quiet $COMMIT_HASH HEAD; then
echo "The given hash is equivalent to the current HEAD"
- build_sandbox
return 0
fi
@@ -30,14 +28,8 @@ checkout_eval_branch() {
# Trap the EXIT signal to checkout original branch
trap checkout_original_branch EXIT
- build_sandbox
}
-build_sandbox() {
- echo "Build sandbox locally"
- docker build -t eval-sandbox -f containers/sandbox/Dockerfile /tmp
- export SANDBOX_CONTAINER_IMAGE="eval-sandbox"
-}
checkout_original_branch() {
if [ -z "$current_branch" ]; then
diff --git a/evaluation/webarena/README.md b/evaluation/webarena/README.md
index cb720a2ffc..2ed3f214ba 100644
--- a/evaluation/webarena/README.md
+++ b/evaluation/webarena/README.md
@@ -2,59 +2,14 @@
This folder contains evaluation for [WebArena](https://github.com/web-arena-x/webarena) benchmark, powered by [BrowserGym](https://github.com/ServiceNow/BrowserGym) for easy evaluation of how well an agent capable of browsing can perform on realistic web browsing tasks.
-## Setup OpenDevin Environment
+## Setup Environment and LLM Configuration
-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"
-
-[sandbox]
-box_type = "ssh"
-timeout = 120
-
-# 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
-```
+Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Setup WebArena Environment
WebArena requires you to set up websites containing pre-populated content that is accessible via URL to the machine running the OpenDevin agents.
Follow [this document](https://github.com/web-arena-x/webarena/blob/main/environment_docker/README.md) to set up your own WebArena environment through local servers or AWS EC2 instances.
-Take note of the base URL of the machine where the environment is installed.
-
-## Setup Environment Variables of WebArena Websites
-
-Create a script `webarena_env.sh` under `evaluation/webarena/scripts` with the following:
-
-```bash
-export BASE_URL=
-export SHOPPING="$BASE_URL:7770/"
-export SHOPPING_ADMIN="$BASE_URL:7780/admin"
-export REDDIT="$BASE_URL:9999"
-export GITLAB="$BASE_URL:8023"
-export WIKIPEDIA="$BASE_URL:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing"
-export MAP="$BASE_URL:3000"
-export HOMEPAGE="$BASE_URL:4399"
-export OPENAI_API_KEY="yourkey" # this key is required for some WebArena validators that utilize LLMs
-```
+Take note of the base URL (`$WEBARENA_BASE_URL`) of the machine where the environment is installed.
## Test if your environment works
@@ -65,7 +20,9 @@ Follow the WebArena environment setup guide carefully, and make sure the URL fie
## Run Evaluation
-```sh
+```bash
+export WEBARENA_BASE_URL=
+export OPENAI_API_KEY="yourkey" # this key is required for some WebArena validators that utilize LLMs
bash evaluation/webarena/scripts/run_infer.sh
```
diff --git a/evaluation/webarena/run_infer.py b/evaluation/webarena/run_infer.py
index ac391bf6c5..c0a3182f77 100644
--- a/evaluation/webarena/run_infer.py
+++ b/evaluation/webarena/run_infer.py
@@ -1,7 +1,7 @@
import asyncio
import json
-import logging
import os
+from typing import Any
import browsergym.webarena # noqa F401 register webarena tasks as gym environments
import gymnasium as gym
@@ -9,93 +9,146 @@ import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
+ EvalOutput,
make_metadata,
prepare_dataset,
+ reset_logger_for_multiprocessing,
run_evaluation,
)
-from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
-from opendevin.core.logger import get_console_handler
+from opendevin.core.config import (
+ AppConfig,
+ SandboxConfig,
+ get_llm_config_arg,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.main import run_agent_controller
-from opendevin.llm.llm import LLM
-from opendevin.runtime.docker.ssh_box import DockerSSHBox
-from opendevin.runtime.tools import RuntimeTool
-
-config = load_app_config()
+from opendevin.core.main import create_runtime, run_controller
+from opendevin.events.action import (
+ BrowseInteractiveAction,
+ CmdRunAction,
+ MessageAction,
+)
+from opendevin.events.observation import CmdOutputObservation
+from opendevin.runtime.browser.browser_env import (
+ BROWSER_EVAL_GET_GOAL_ACTION,
+ BROWSER_EVAL_GET_REWARDS_ACTION,
+)
+from opendevin.runtime.runtime import Runtime
SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
-docker_ssh_box: DockerSSHBox | None = None
+def get_config(
+ metadata: EvalMetadata,
+ env_id: str,
+) -> AppConfig:
+ base_url = os.environ.get('WEBARENA_BASE_URL', None)
+ openai_api_key = os.environ.get('OPENAI_API_KEY', None)
+ assert base_url is not None, 'WEBARENA_BASE_URL must be set'
+ assert openai_api_key is not None, 'OPENAI_API_KEY must be set'
+
+ config = AppConfig(
+ default_agent=metadata.agent_class,
+ run_as_devin=False,
+ runtime='eventstream',
+ max_iterations=metadata.max_iterations,
+ sandbox=SandboxConfig(
+ container_image='ubuntu:22.04',
+ enable_auto_lint=True,
+ use_host_network=False,
+ browsergym_eval_env=env_id,
+ od_runtime_startup_env_vars={
+ 'BASE_URL': base_url,
+ 'OPENAI_API_KEY': openai_api_key,
+ 'SHOPPING': f'{base_url}:7770/',
+ 'SHOPPING_ADMIN': f'{base_url}:7780/admin',
+ 'REDDIT': f'{base_url}:9999',
+ 'GITLAB': f'{base_url}:8023',
+ 'WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing',
+ 'MAP': f'{base_url}:3000',
+ 'HOMEPAGE': f'{base_url}:4399',
+ },
+ ),
+ # do not mount workspace
+ workspace_base=None,
+ workspace_mount_path=None,
+ )
+ config.set_llm_config(metadata.llm_config)
+ return config
-def get_sandbox():
- global docker_ssh_box
- if docker_ssh_box is None:
- docker_ssh_box = DockerSSHBox(
- config=config.sandbox,
- persist_sandbox=False,
- workspace_mount_path=config.workspace_mount_path,
- sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
- cache_dir=config.cache_dir,
- run_as_devin=config.run_as_devin,
- )
- return docker_ssh_box
+async def initialize_runtime(
+ runtime: Runtime,
+) -> dict:
+ """Initialize the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ # Set instance id
+ action = CmdRunAction(command='mkdir -p /workspace')
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ assert obs.exit_code == 0
+
+ action = BrowseInteractiveAction(browser_actions=BROWSER_EVAL_GET_GOAL_ACTION)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+ goal = obs.content
+
+ logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
+ return goal
-def process_instance(
+async def complete_runtime(
+ runtime: Runtime,
+) -> dict[str, Any]:
+ """Complete the runtime for the agent.
+
+ This function is called before the runtime is used to run the agent.
+ If you need to do something in the sandbox to get the correctness metric after
+ the agent has run, modify this function.
+ """
+ logger.info(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
+ obs: CmdOutputObservation
+
+ action = BrowseInteractiveAction(browser_actions=BROWSER_EVAL_GET_REWARDS_ACTION)
+ logger.info(action, extra={'msg_type': 'ACTION'})
+ obs = await runtime.run_action(action)
+ logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+
+ logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
+ return {
+ 'rewards': json.loads(obs.content),
+ }
+
+
+async def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
):
- # Create the agent
- agent = Agent.get_cls(metadata.agent_class)(llm=LLM(config=metadata.llm_config))
- env_id = instance.id
+ env_id = instance.instance_id
+ config = get_config(metadata, env_id)
+
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
- # Set up logger
- log_file = os.path.join(
- metadata.eval_output_dir, 'logs', f'instance_{env_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 {env_id}.\nHint: run "tail -f {log_file}" to see live logs in a separate shell'
- )
- # 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)
+ log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
+ reset_logger_for_multiprocessing(logger, env_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {env_id}.')
- # Here's how you can run the agent (similar to the `main` function) and get the final task state
- runtime_tools_config = {
- RuntimeTool.BROWSER: {
- 'browsergym_eval': env_id,
- 'browsergym_eval_save_dir': metadata.eval_output_dir,
- }
- }
+ runtime = await create_runtime(config, sid=env_id)
+ task_str = await initialize_runtime(runtime)
- state: State | None = asyncio.run(
- run_agent_controller(
- agent,
- 'PLACEHOLDER_GOAL',
- max_iterations=metadata.max_iterations,
- max_budget_per_task=config.max_budget_per_task,
- runtime_tools_config=runtime_tools_config,
- sandbox=get_sandbox(),
- sid=env_id,
- )
+ state: State | None = await run_controller(
+ config=config,
+ task_str=task_str,
+ runtime=runtime,
)
# ======= Attempt to evaluate the agent's environment impact =======
@@ -107,18 +160,17 @@ def process_instance(
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
- browsergym_eval_dir = os.path.join(metadata.eval_output_dir, env_id.split('/')[1])
- # read goal
- with open(
- os.path.join(browsergym_eval_dir, 'goal.txt'), 'r', encoding='utf-8'
- ) as f:
- instruction = f.read()
- # read reward
- with open(
- os.path.join(browsergym_eval_dir, 'rewards.json'), 'r', encoding='utf-8'
- ) as f:
- rewards = json.load(f)
- reward = max(rewards)
+
+ # Instruction is the first message from the USER
+ instruction = ''
+ for event in state.history.get_events():
+ if isinstance(event, MessageAction):
+ instruction = event.content
+ break
+
+ return_val = await complete_runtime(runtime)
+ logger.info(f'Return value from complete_runtime: {return_val}')
+ reward = max(return_val['rewards'])
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -126,39 +178,38 @@ def process_instance(
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
- output = {
- 'instance_id': env_id,
- 'instruction': instruction,
- 'metadata': metadata.model_dump(),
- 'history': histories,
- 'metrics': metrics,
- 'error': state.last_error if state and state.last_error else None,
- 'test_result': reward,
- }
-
+ output = EvalOutput(
+ instance_id=env_id,
+ instruction=instruction,
+ metadata=metadata,
+ history=histories,
+ metrics=metrics,
+ error=state.last_error if state and state.last_error else None,
+ test_result={
+ 'reward': reward,
+ },
+ )
return output
if __name__ == '__main__':
args = parse_arguments()
- env_ids = [
- id for id in gym.envs.registry.keys() if id.startswith('browsergym/webarena')
- ]
-
dataset = pd.DataFrame(
{
- 'id': [
+ 'instance_id': [
id
for id in gym.envs.registry.keys()
- if id.startswith('browsergym/miniwob')
+ if id.startswith('browsergym/webarena')
]
}
)
- id_column = 'id'
- llm_config = get_llm_config_arg(args.llm_config) if args.llm_config else config.llm
- logger.info(f'Config for evaluation: {config}')
+ llm_config = None
+ if args.llm_config:
+ llm_config = get_llm_config_arg(args.llm_config)
+ if llm_config is None:
+ raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
@@ -169,13 +220,14 @@ if __name__ == '__main__':
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
- instances = prepare_dataset(dataset, output_file, args.eval_n_limit, id_column)
- _ = get_sandbox() # Initialize the sandbox
- run_evaluation(
- instances,
- metadata,
- output_file,
- args.eval_num_workers,
- process_instance,
- id_column,
+ instances = prepare_dataset(dataset, output_file, args.eval_n_limit)
+
+ asyncio.run(
+ run_evaluation(
+ instances,
+ metadata,
+ output_file,
+ args.eval_num_workers,
+ process_instance,
+ )
)
diff --git a/frontend/README.md b/frontend/README.md
index 01530e63e2..4e6b58c79f 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,5 +1,11 @@
# Getting Started with the OpenDevin Frontend
+The frontend code can be run against the docker image defined in the [Main README](../README.md) as a backend
+
+## Prerequisites
+
+A recent version of NodeJS / NPM (`brew install node`)
+
## Available Scripts
In the project directory, you can run:
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 231afd8fad..e998984731 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -26,30 +26,30 @@
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
- "react-i18next": "^15.0.0",
+ "react-i18next": "^15.0.1",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.4.0",
- "vite": "^5.3.5",
+ "vite": "^5.4.0",
"web-vitals": "^3.5.2"
},
"devDependencies": {
- "@tailwindcss/typography": "^0.5.13",
+ "@tailwindcss/typography": "^0.5.14",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
- "@types/node": "^22.0.0",
+ "@types/node": "^22.1.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
- "@typescript-eslint/eslint-plugin": "^7.17.0",
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
- "autoprefixer": "^10.4.19",
+ "autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
@@ -61,10 +61,10 @@
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.4",
"jsdom": "^24.1.1",
- "lint-staged": "^15.2.7",
- "postcss": "^8.4.40",
+ "lint-staged": "^15.2.8",
+ "postcss": "^8.4.41",
"prettier": "^3.3.3",
- "tailwindcss": "^3.4.7",
+ "tailwindcss": "^3.4.9",
"typescript": "^5.5.4",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0"
@@ -73,15 +73,6 @@
"node": ">=14.8.0"
}
},
- "node_modules/@aashutoshrathi/word-wrap": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
- "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/@adobe/css-tools": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz",
@@ -112,11 +103,11 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
- "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
+ "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
"dependencies": {
- "@babel/highlight": "^7.24.6",
+ "@babel/highlight": "^7.24.7",
"picocolors": "^1.0.0"
},
"engines": {
@@ -124,28 +115,28 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz",
- "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
+ "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz",
- "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
+ "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.24.6",
- "@babel/generator": "^7.24.6",
- "@babel/helper-compilation-targets": "^7.24.6",
- "@babel/helper-module-transforms": "^7.24.6",
- "@babel/helpers": "^7.24.6",
- "@babel/parser": "^7.24.6",
- "@babel/template": "^7.24.6",
- "@babel/traverse": "^7.24.6",
- "@babel/types": "^7.24.6",
+ "@babel/code-frame": "^7.24.7",
+ "@babel/generator": "^7.25.0",
+ "@babel/helper-compilation-targets": "^7.25.2",
+ "@babel/helper-module-transforms": "^7.25.2",
+ "@babel/helpers": "^7.25.0",
+ "@babel/parser": "^7.25.0",
+ "@babel/template": "^7.25.0",
+ "@babel/traverse": "^7.25.2",
+ "@babel/types": "^7.25.2",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -169,11 +160,11 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz",
- "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
+ "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
"dependencies": {
- "@babel/types": "^7.24.6",
+ "@babel/types": "^7.25.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
@@ -183,13 +174,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz",
- "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
+ "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
"dependencies": {
- "@babel/compat-data": "^7.24.6",
- "@babel/helper-validator-option": "^7.24.6",
- "browserslist": "^4.22.2",
+ "@babel/compat-data": "^7.25.2",
+ "@babel/helper-validator-option": "^7.24.8",
+ "browserslist": "^4.23.1",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -205,58 +196,27 @@
"semver": "bin/semver.js"
}
},
- "node_modules/@babel/helper-environment-visitor": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz",
- "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-function-name": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz",
- "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==",
- "dependencies": {
- "@babel/template": "^7.24.6",
- "@babel/types": "^7.24.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-hoist-variables": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz",
- "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==",
- "dependencies": {
- "@babel/types": "^7.24.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/helper-module-imports": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
- "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
+ "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==",
"dependencies": {
- "@babel/types": "^7.24.6"
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz",
- "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
+ "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.24.6",
- "@babel/helper-module-imports": "^7.24.6",
- "@babel/helper-simple-access": "^7.24.6",
- "@babel/helper-split-export-declaration": "^7.24.6",
- "@babel/helper-validator-identifier": "^7.24.6"
+ "@babel/helper-module-imports": "^7.24.7",
+ "@babel/helper-simple-access": "^7.24.7",
+ "@babel/helper-validator-identifier": "^7.24.7",
+ "@babel/traverse": "^7.25.2"
},
"engines": {
"node": ">=6.9.0"
@@ -266,77 +226,67 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz",
- "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz",
+ "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-simple-access": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz",
- "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz",
+ "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==",
"dependencies": {
- "@babel/types": "^7.24.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-split-export-declaration": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz",
- "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==",
- "dependencies": {
- "@babel/types": "^7.24.6"
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz",
- "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+ "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz",
- "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+ "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz",
- "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
+ "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz",
- "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
+ "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
"dependencies": {
- "@babel/template": "^7.24.6",
- "@babel/types": "^7.24.6"
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz",
- "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
+ "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.24.6",
+ "@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
@@ -410,9 +360,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
- "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+ "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+ "dependencies": {
+ "@babel/types": "^7.25.2"
+ },
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -421,11 +374,11 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.6.tgz",
- "integrity": "sha512-FfZfHXtQ5jYPQsCRyLpOv2GeLIIJhs8aydpNh39vRDjhD411XcfWDni5i7OjP/Rs8GAtTn7sWFFELJSHqkIxYg==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz",
+ "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.6"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -435,11 +388,11 @@
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz",
- "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz",
+ "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.0"
+ "@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
@@ -449,9 +402,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.24.8",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
- "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
+ "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -460,31 +413,28 @@
}
},
"node_modules/@babel/template": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz",
- "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
+ "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
"dependencies": {
- "@babel/code-frame": "^7.24.6",
- "@babel/parser": "^7.24.6",
- "@babel/types": "^7.24.6"
+ "@babel/code-frame": "^7.24.7",
+ "@babel/parser": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz",
- "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
+ "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
"dependencies": {
- "@babel/code-frame": "^7.24.6",
- "@babel/generator": "^7.24.6",
- "@babel/helper-environment-visitor": "^7.24.6",
- "@babel/helper-function-name": "^7.24.6",
- "@babel/helper-hoist-variables": "^7.24.6",
- "@babel/helper-split-export-declaration": "^7.24.6",
- "@babel/parser": "^7.24.6",
- "@babel/types": "^7.24.6",
+ "@babel/code-frame": "^7.24.7",
+ "@babel/generator": "^7.25.0",
+ "@babel/parser": "^7.25.3",
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.2",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -493,12 +443,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz",
- "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+ "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
"dependencies": {
- "@babel/helper-string-parser": "^7.24.6",
- "@babel/helper-validator-identifier": "^7.24.6",
+ "@babel/helper-string-parser": "^7.24.8",
+ "@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -872,9 +822,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
- "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
+ "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
"dev": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -940,18 +890,6 @@
"node": "*"
}
},
- "node_modules/@eslint/eslintrc/node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/@eslint/js": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
@@ -1009,6 +947,7 @@
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "deprecated": "Use @eslint/config-array instead",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
@@ -1058,12 +997,13 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@internationalized/date": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.4.tgz",
- "integrity": "sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==",
+ "version": "3.5.5",
+ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.5.tgz",
+ "integrity": "sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -1228,9 +1168,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.15",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
@@ -3049,28 +2989,166 @@
}
},
"node_modules/@react-aria/grid": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.9.1.tgz",
- "integrity": "sha512-fGEZqAEaS8mqzV/II3N4ndoNWegIcbh+L3PmKbXdpKKUP8VgMs/WY5rYl5WAF0f5RoFwXqx3ibDLeR9tKj/bOg==",
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.10.1.tgz",
+ "integrity": "sha512-7dSgiYVQapBtPV4SIit+9fJ1qoEjtp+PXffJkWAPtGbg/jJ4b0jcVzykH7ARD4w/6jAJN/oVSfrKZqFPoLAd9w==",
"dependencies": {
- "@react-aria/focus": "^3.17.1",
- "@react-aria/i18n": "^3.11.1",
- "@react-aria/interactions": "^3.21.3",
+ "@react-aria/focus": "^3.18.1",
+ "@react-aria/i18n": "^3.12.1",
+ "@react-aria/interactions": "^3.22.1",
"@react-aria/live-announcer": "^3.3.4",
- "@react-aria/selection": "^3.18.1",
- "@react-aria/utils": "^3.24.1",
- "@react-stately/collections": "^3.10.7",
- "@react-stately/grid": "^3.8.7",
- "@react-stately/selection": "^3.15.1",
- "@react-stately/virtualizer": "^3.7.1",
- "@react-types/checkbox": "^3.8.1",
- "@react-types/grid": "^3.2.6",
- "@react-types/shared": "^3.23.1",
+ "@react-aria/selection": "^3.19.1",
+ "@react-aria/utils": "^3.25.1",
+ "@react-stately/collections": "^3.10.9",
+ "@react-stately/grid": "^3.9.1",
+ "@react-stately/selection": "^3.16.1",
+ "@react-types/checkbox": "^3.8.3",
+ "@react-types/grid": "^3.2.8",
+ "@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
- "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-aria/focus": {
+ "version": "3.18.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.1.tgz",
+ "integrity": "sha512-N0Cy61WCIv+57mbqC7hiZAsB+3rF5n4JKabxUmg/2RTJL6lq7hJ5N4gx75ymKxkN8GnVDwt4pKZah48Wopa5jw==",
+ "dependencies": {
+ "@react-aria/interactions": "^3.22.1",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-aria/i18n": {
+ "version": "3.12.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.1.tgz",
+ "integrity": "sha512-0q3gyogF9Ekah+9LOo6tcfshxsk2Ope+KdbtFHJVhznedMxn6RpHGcVur5ImbQ1dYafA5CmjBUGJW70b56+BGA==",
+ "dependencies": {
+ "@internationalized/date": "^3.5.5",
+ "@internationalized/message": "^3.1.4",
+ "@internationalized/number": "^3.5.3",
+ "@internationalized/string": "^3.2.3",
+ "@react-aria/ssr": "^3.9.5",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-aria/interactions": {
+ "version": "3.22.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.1.tgz",
+ "integrity": "sha512-5TLzQaDAQQ5C70yG8GInbO4wIylKY67RfTIIwQPGR/4n5OIjbUD8BOj3NuSsuZ/frUPaBXo1VEBBmSO23fxkjw==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-aria/selection": {
+ "version": "3.19.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.19.1.tgz",
+ "integrity": "sha512-mbExvq2Omi60sTWFGjwcNz1ja2P8VDsxWAqSypHRTyqXhtgqbv8V/v8Gp+7BmVPH1YHcbhztl6rvUZTDOSszzw==",
+ "dependencies": {
+ "@react-aria/focus": "^3.18.1",
+ "@react-aria/i18n": "^3.12.1",
+ "@react-aria/interactions": "^3.22.1",
+ "@react-aria/utils": "^3.25.1",
+ "@react-stately/selection": "^3.16.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-aria/ssr": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz",
+ "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-aria/utils": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.1.tgz",
+ "integrity": "sha512-5Uj864e7T5+yj78ZfLnfHqmypLiqW2mN+nsdslog2z5ssunTqjolVeM15ootXskjISlZ7MojLpq97kIC4nlnAw==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-stately/collections": {
+ "version": "3.10.9",
+ "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.9.tgz",
+ "integrity": "sha512-plyrng6hOQMG8LrjArMA6ts/DgWyXln3g90/hFNbqe/hdVYF53sDVsj8Jb+5LtoYTpiAlV6eOvy1XR0vPZUf8w==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-stately/utils": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz",
+ "integrity": "sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-types/checkbox": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.8.3.tgz",
+ "integrity": "sha512-f4c1mnLEt0iS1NMkyZXgT3q3AgcxzDk7w6MSONOKydcnh0xG5L2oefY14DhVDLkAuQS7jThlUFwiAs+MxiO3MA==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/grid/node_modules/@react-types/grid": {
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.8.tgz",
+ "integrity": "sha512-6PJrpukwMqlv3IhJSDkJuVbhHM8Oe6hd2supWqd9adMXrlSP7QHt9a8SgFcFblCCTx8JzUaA0PvY5sTudcEtOQ==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-aria/i18n": {
@@ -3282,20 +3360,89 @@
}
},
"node_modules/@react-aria/spinbutton": {
- "version": "3.6.5",
- "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.5.tgz",
- "integrity": "sha512-0aACBarF/Xr/7ixzjVBTQ0NBwwwsoGkf5v6AVFVMTC0uYMXHTALvRs+ULHjHMa5e/cX/aPlEvaVT7jfSs+Xy9Q==",
+ "version": "3.6.7",
+ "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.7.tgz",
+ "integrity": "sha512-OCimp4yXoFIgh6WAMOls5DDDRDRO75ZFic3YA6wLWTRNHxo1Lj8S90i1A6pakY6bi4hdBCKmj4DnFSNKAw1iWg==",
"dependencies": {
- "@react-aria/i18n": "^3.11.1",
+ "@react-aria/i18n": "^3.12.1",
"@react-aria/live-announcer": "^3.3.4",
- "@react-aria/utils": "^3.24.1",
- "@react-types/button": "^3.9.4",
- "@react-types/shared": "^3.23.1",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/button": "^3.9.6",
+ "@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
- "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/spinbutton/node_modules/@react-aria/i18n": {
+ "version": "3.12.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.1.tgz",
+ "integrity": "sha512-0q3gyogF9Ekah+9LOo6tcfshxsk2Ope+KdbtFHJVhznedMxn6RpHGcVur5ImbQ1dYafA5CmjBUGJW70b56+BGA==",
+ "dependencies": {
+ "@internationalized/date": "^3.5.5",
+ "@internationalized/message": "^3.1.4",
+ "@internationalized/number": "^3.5.3",
+ "@internationalized/string": "^3.2.3",
+ "@react-aria/ssr": "^3.9.5",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/spinbutton/node_modules/@react-aria/ssr": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz",
+ "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/spinbutton/node_modules/@react-aria/utils": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.1.tgz",
+ "integrity": "sha512-5Uj864e7T5+yj78ZfLnfHqmypLiqW2mN+nsdslog2z5ssunTqjolVeM15ootXskjISlZ7MojLpq97kIC4nlnAw==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/spinbutton/node_modules/@react-stately/utils": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz",
+ "integrity": "sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/spinbutton/node_modules/@react-types/button": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.9.6.tgz",
+ "integrity": "sha512-8lA+D5JLbNyQikf8M/cPP2cji91aVTcqjrGpDqI7sQnaLFikM8eFR6l1ZWGtZS5MCcbfooko77ha35SYplSQvw==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-aria/ssr": {
@@ -3392,19 +3539,113 @@
}
},
"node_modules/@react-aria/toggle": {
- "version": "3.10.4",
- "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.4.tgz",
- "integrity": "sha512-bRk+CdB8QzrSyGNjENXiTWxfzYKRw753iwQXsEAU7agPCUdB8cZJyrhbaUoD0rwczzTp2zDbZ9rRbUPdsBE2YQ==",
+ "version": "3.10.6",
+ "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.6.tgz",
+ "integrity": "sha512-AGlbtB1b8grrtjbiW5Au0LKYzxR83RHbHhaUkFwajyYRGyuEzr3Y03OiveoPB+DayA8Gz3H1ZVmW++8JZQOWHw==",
"dependencies": {
- "@react-aria/focus": "^3.17.1",
- "@react-aria/interactions": "^3.21.3",
- "@react-aria/utils": "^3.24.1",
- "@react-stately/toggle": "^3.7.4",
- "@react-types/checkbox": "^3.8.1",
+ "@react-aria/focus": "^3.18.1",
+ "@react-aria/interactions": "^3.22.1",
+ "@react-aria/utils": "^3.25.1",
+ "@react-stately/toggle": "^3.7.6",
+ "@react-types/checkbox": "^3.8.3",
+ "@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-aria/focus": {
+ "version": "3.18.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.1.tgz",
+ "integrity": "sha512-N0Cy61WCIv+57mbqC7hiZAsB+3rF5n4JKabxUmg/2RTJL6lq7hJ5N4gx75ymKxkN8GnVDwt4pKZah48Wopa5jw==",
+ "dependencies": {
+ "@react-aria/interactions": "^3.22.1",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-aria/interactions": {
+ "version": "3.22.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.1.tgz",
+ "integrity": "sha512-5TLzQaDAQQ5C70yG8GInbO4wIylKY67RfTIIwQPGR/4n5OIjbUD8BOj3NuSsuZ/frUPaBXo1VEBBmSO23fxkjw==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-aria/utils": "^3.25.1",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-aria/ssr": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz",
+ "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-aria/utils": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.1.tgz",
+ "integrity": "sha512-5Uj864e7T5+yj78ZfLnfHqmypLiqW2mN+nsdslog2z5ssunTqjolVeM15ootXskjISlZ7MojLpq97kIC4nlnAw==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.5",
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-stately/toggle": {
+ "version": "3.7.6",
+ "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.7.6.tgz",
+ "integrity": "sha512-xRZyrjNVu1VCd1xpg5RwmNYs9fXb+JHChoUaRcBmGCCjsPD0R5uR3iNuE17RXJtWS3/8o9IJVn90+/7NW7boOg==",
+ "dependencies": {
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/checkbox": "^3.8.3",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-stately/utils": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz",
+ "integrity": "sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/toggle/node_modules/@react-types/checkbox": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.8.3.tgz",
+ "integrity": "sha512-f4c1mnLEt0iS1NMkyZXgT3q3AgcxzDk7w6MSONOKydcnh0xG5L2oefY14DhVDLkAuQS7jThlUFwiAs+MxiO3MA==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-aria/tooltip": {
@@ -3553,18 +3794,41 @@
}
},
"node_modules/@react-stately/grid": {
- "version": "3.8.7",
- "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.8.7.tgz",
- "integrity": "sha512-he3TXCLAhF5C5z1/G4ySzcwyt7PEiWcVIupxebJQqRyFrNWemSuv+7tolnStmG8maMVIyV3P/3j4eRBbdSlOIg==",
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.9.1.tgz",
+ "integrity": "sha512-LSVIcXO/cqwG0IgDSk2juDbpARBS1IzGnsTp/8vSOejMxq5MXrwxL5hUcqNczL8Ss6aLpELm42tCS0kPm3cMKw==",
"dependencies": {
- "@react-stately/collections": "^3.10.7",
- "@react-stately/selection": "^3.15.1",
- "@react-types/grid": "^3.2.6",
- "@react-types/shared": "^3.23.1",
+ "@react-stately/collections": "^3.10.9",
+ "@react-stately/selection": "^3.16.1",
+ "@react-types/grid": "^3.2.8",
+ "@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/grid/node_modules/@react-stately/collections": {
+ "version": "3.10.9",
+ "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.9.tgz",
+ "integrity": "sha512-plyrng6hOQMG8LrjArMA6ts/DgWyXln3g90/hFNbqe/hdVYF53sDVsj8Jb+5LtoYTpiAlV6eOvy1XR0vPZUf8w==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/grid/node_modules/@react-types/grid": {
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.8.tgz",
+ "integrity": "sha512-6PJrpukwMqlv3IhJSDkJuVbhHM8Oe6hd2supWqd9adMXrlSP7QHt9a8SgFcFblCCTx8JzUaA0PvY5sTudcEtOQ==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-stately/list": {
@@ -3625,33 +3889,141 @@
}
},
"node_modules/@react-stately/select": {
- "version": "3.6.4",
- "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.6.4.tgz",
- "integrity": "sha512-whZgF1N53D0/dS8tOFdrswB0alsk5Q5620HC3z+5f2Hpi8gwgAZ8TYa+2IcmMYRiT+bxVuvEc/NirU9yPmqGbA==",
+ "version": "3.6.6",
+ "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.6.6.tgz",
+ "integrity": "sha512-JEpBosWNSXRexE/iReATei1EiVdTIwOWlLcCGw6K7oC/5/f+OHMsh2Kkt/c/RzM/to3vgR+Wbbqwrb712AWgYQ==",
"dependencies": {
- "@react-stately/form": "^3.0.3",
- "@react-stately/list": "^3.10.5",
- "@react-stately/overlays": "^3.6.7",
- "@react-types/select": "^3.9.4",
- "@react-types/shared": "^3.23.1",
+ "@react-stately/form": "^3.0.5",
+ "@react-stately/list": "^3.10.7",
+ "@react-stately/overlays": "^3.6.9",
+ "@react-types/select": "^3.9.6",
+ "@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-stately/collections": {
+ "version": "3.10.9",
+ "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.9.tgz",
+ "integrity": "sha512-plyrng6hOQMG8LrjArMA6ts/DgWyXln3g90/hFNbqe/hdVYF53sDVsj8Jb+5LtoYTpiAlV6eOvy1XR0vPZUf8w==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-stately/form": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.0.5.tgz",
+ "integrity": "sha512-J3plwJ63HQz109OdmaTqTA8Qhvl3gcYYK7DtgKyNP6mc/Me2Q4tl2avkWoA+22NRuv5m+J8TpBk4AVHUEOwqeQ==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-stately/list": {
+ "version": "3.10.7",
+ "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.10.7.tgz",
+ "integrity": "sha512-W5PG7uG5GQV2Q59vXJE7QLKHZIoUNEx+JmHrBUCMKUgyngSpKIIEDR/R/C1b6ZJ9jMqqZA68Zlnd5iK1/mBi1A==",
+ "dependencies": {
+ "@react-stately/collections": "^3.10.9",
+ "@react-stately/selection": "^3.16.1",
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-stately/overlays": {
+ "version": "3.6.9",
+ "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.9.tgz",
+ "integrity": "sha512-4chfyzKw7P2UEainm0yzjUgYwG1ovBejN88eTrn+O62x5huuMCwe0cbMxmYh4y7IhRFSee3jIJd0SP0u/+i39w==",
+ "dependencies": {
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/overlays": "^3.8.9",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-stately/utils": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz",
+ "integrity": "sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-types/overlays": {
+ "version": "3.8.9",
+ "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.9.tgz",
+ "integrity": "sha512-9ni9upQgXPnR+K9cWmbYWvm3ll9gH8P/XsEZprqIV5zNLMF334jADK48h4jafb1X9RFnj0WbHo6BqcSObzjTig==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/select/node_modules/@react-types/select": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.9.6.tgz",
+ "integrity": "sha512-cVSFR0eJLup/ht1Uto+y8uyLmHO89J6wNh65SIHb3jeVz9oLBAedP3YNI2qB+F9qFMUcA8PBSLXIIuT6gXzLgQ==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-stately/selection": {
- "version": "3.15.1",
- "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.15.1.tgz",
- "integrity": "sha512-6TQnN9L0UY9w19B7xzb1P6mbUVBtW840Cw1SjgNXCB3NPaCf59SwqClYzoj8O2ZFzMe8F/nUJtfU1NS65/OLlw==",
+ "version": "3.16.1",
+ "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.16.1.tgz",
+ "integrity": "sha512-qmnmYaXY7IhhzmIiInec1a/yPxlPSBHka6vrWddvt0S6zN7FU5cv6sm69ONUwYwLKSoaNHgOGvZhmsTzyV0O2A==",
"dependencies": {
- "@react-stately/collections": "^3.10.7",
- "@react-stately/utils": "^3.10.1",
- "@react-types/shared": "^3.23.1",
+ "@react-stately/collections": "^3.10.9",
+ "@react-stately/utils": "^3.10.2",
+ "@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/selection/node_modules/@react-stately/collections": {
+ "version": "3.10.9",
+ "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.9.tgz",
+ "integrity": "sha512-plyrng6hOQMG8LrjArMA6ts/DgWyXln3g90/hFNbqe/hdVYF53sDVsj8Jb+5LtoYTpiAlV6eOvy1XR0vPZUf8w==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/selection/node_modules/@react-stately/utils": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz",
+ "integrity": "sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-stately/slider": {
@@ -3849,15 +4221,26 @@
}
},
"node_modules/@react-types/dialog": {
- "version": "3.5.10",
- "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.10.tgz",
- "integrity": "sha512-S9ga+edOLNLZw7/zVOnZdT5T40etpzUYBXEKdFPbxyPYnERvRxJAsC1/ASuBU9fQAXMRgLZzADWV+wJoGS/X9g==",
+ "version": "3.5.12",
+ "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.12.tgz",
+ "integrity": "sha512-JmpQbSpXltqEyYfEwoqDolABIiojeExkqolHNdQlayIsfFuSxZxNwXZPOpz58Ri/iwv21JP7K3QF0Gb2Ohxl9w==",
"dependencies": {
- "@react-types/overlays": "^3.8.7",
- "@react-types/shared": "^3.23.1"
+ "@react-types/overlays": "^3.8.9",
+ "@react-types/shared": "^3.24.1"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-types/dialog/node_modules/@react-types/overlays": {
+ "version": "3.8.9",
+ "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.9.tgz",
+ "integrity": "sha512-9ni9upQgXPnR+K9cWmbYWvm3ll9gH8P/XsEZprqIV5zNLMF334jADK48h4jafb1X9RFnj0WbHo6BqcSObzjTig==",
+ "dependencies": {
+ "@react-types/shared": "^3.24.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-types/grid": {
@@ -3883,14 +4266,14 @@
}
},
"node_modules/@react-types/listbox": {
- "version": "3.4.9",
- "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.4.9.tgz",
- "integrity": "sha512-S5G+WmNKUIOPZxZ4svWwWQupP3C6LmVfnf8QQmPDvwYXGzVc0WovkqUWyhhjJirFDswTXRCO9p0yaTHHIlkdwQ==",
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.5.1.tgz",
+ "integrity": "sha512-n5bOgD9lgfK1qaLtag9WPnu151SwXBCNn/OgGY/Br9mWRl+nPUEYtFcPX+2VCld7uThf54kwrTmzlFnaraIlcw==",
"dependencies": {
- "@react-types/shared": "^3.23.1"
+ "@react-types/shared": "^3.24.1"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-types/menu": {
@@ -3958,25 +4341,25 @@
}
},
"node_modules/@react-types/slider": {
- "version": "3.7.3",
- "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.7.3.tgz",
- "integrity": "sha512-F8qFQaD2mqug2D0XeWMmjGBikiwbdERFlhFzdvNGbypPLz3AZICBKp1ZLPWdl0DMuy03G/jy6Gl4mDobl7RT2g==",
+ "version": "3.7.5",
+ "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.7.5.tgz",
+ "integrity": "sha512-bRitwQRQjQoOcKEdPMljnvm474dwrmsc6pdsVQDh/qynzr+KO9IHuYc3qPW53WVE2hMQJDohlqtCAWQXWQ5Vcg==",
"dependencies": {
- "@react-types/shared": "^3.23.1"
+ "@react-types/shared": "^3.24.1"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-types/switch": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.3.tgz",
- "integrity": "sha512-Nb6+J5MrPaFa8ZNFKGMzAsen/NNzl5UG/BbC65SLGPy7O0VDa/sUpn7dcu8V2xRpRwwIN/Oso4v63bt2sgdkgA==",
+ "version": "3.5.5",
+ "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.5.tgz",
+ "integrity": "sha512-SZx1Bd+COhAOs/RTifbZG+uq/llwba7VAKx7XBeX4LeIz1dtguy5bigOBgFTMQi4qsIVCpybSWEEl+daj4XFPw==",
"dependencies": {
- "@react-types/shared": "^3.23.1"
+ "@react-types/shared": "^3.24.1"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-types/table": {
@@ -4057,9 +4440,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz",
- "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz",
+ "integrity": "sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==",
"cpu": [
"arm"
],
@@ -4069,9 +4452,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz",
- "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.2.tgz",
+ "integrity": "sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==",
"cpu": [
"arm64"
],
@@ -4081,9 +4464,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz",
- "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.2.tgz",
+ "integrity": "sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==",
"cpu": [
"arm64"
],
@@ -4093,9 +4476,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz",
- "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.2.tgz",
+ "integrity": "sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==",
"cpu": [
"x64"
],
@@ -4105,9 +4488,21 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz",
- "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.2.tgz",
+ "integrity": "sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.2.tgz",
+ "integrity": "sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==",
"cpu": [
"arm"
],
@@ -4117,9 +4512,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz",
- "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.2.tgz",
+ "integrity": "sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==",
"cpu": [
"arm64"
],
@@ -4129,9 +4524,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz",
- "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.2.tgz",
+ "integrity": "sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==",
"cpu": [
"arm64"
],
@@ -4141,11 +4536,11 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz",
- "integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.2.tgz",
+ "integrity": "sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==",
"cpu": [
- "ppc64le"
+ "ppc64"
],
"optional": true,
"os": [
@@ -4153,9 +4548,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz",
- "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.2.tgz",
+ "integrity": "sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==",
"cpu": [
"riscv64"
],
@@ -4165,9 +4560,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz",
- "integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.2.tgz",
+ "integrity": "sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==",
"cpu": [
"s390x"
],
@@ -4177,9 +4572,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz",
- "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.2.tgz",
+ "integrity": "sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==",
"cpu": [
"x64"
],
@@ -4189,9 +4584,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz",
- "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.2.tgz",
+ "integrity": "sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==",
"cpu": [
"x64"
],
@@ -4201,9 +4596,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz",
- "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.2.tgz",
+ "integrity": "sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==",
"cpu": [
"arm64"
],
@@ -4213,9 +4608,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz",
- "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.2.tgz",
+ "integrity": "sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==",
"cpu": [
"ia32"
],
@@ -4225,9 +4620,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz",
- "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.2.tgz",
+ "integrity": "sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==",
"cpu": [
"x64"
],
@@ -4251,9 +4646,9 @@
}
},
"node_modules/@tailwindcss/typography": {
- "version": "0.5.13",
- "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
- "integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
+ "version": "0.5.14",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.14.tgz",
+ "integrity": "sha512-ZvOCjUbsJBjL9CxQBn+VEnFpouzuKhxh2dH8xMIWHILL+HfOYtlAkWcyoon8LlzE53d2Yo6YO6pahKKNW3q1YQ==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
@@ -4265,23 +4660,10 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
- "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
- "version": "6.0.10",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
- "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
- "dev": true,
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@testing-library/dom": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz",
- "integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
+ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"dev": true,
"peer": true,
"dependencies": {
@@ -4415,9 +4797,9 @@
}
},
"node_modules/@types/babel__traverse": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz",
- "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==",
+ "version": "7.20.6",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
+ "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
"dependencies": {
"@babel/types": "^7.20.7"
}
@@ -4444,11 +4826,11 @@
}
},
"node_modules/@types/hast": {
- "version": "2.3.10",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
- "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"dependencies": {
- "@types/unist": "^2"
+ "@types/unist": "*"
}
},
"node_modules/@types/json5": {
@@ -4471,9 +4853,9 @@
}
},
"node_modules/@types/mdast": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz",
- "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"dependencies": {
"@types/unist": "*"
}
@@ -4484,12 +4866,12 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
- "version": "22.0.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz",
- "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==",
+ "version": "22.1.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
+ "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
"devOptional": true,
"dependencies": {
- "undici-types": "~6.11.1"
+ "undici-types": "~6.13.0"
}
},
"node_modules/@types/prop-types": {
@@ -4534,9 +4916,9 @@
}
},
"node_modules/@types/unist": {
- "version": "2.0.10",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
- "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
+ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
@@ -4544,16 +4926,16 @@
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz",
- "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "7.17.0",
- "@typescript-eslint/type-utils": "7.17.0",
- "@typescript-eslint/utils": "7.17.0",
- "@typescript-eslint/visitor-keys": "7.17.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/type-utils": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -4604,7 +4986,7 @@
}
}
},
- "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+ "node_modules/@typescript-eslint/scope-manager": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
"integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
@@ -4621,7 +5003,34 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+ "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
"integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
@@ -4634,7 +5043,7 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
+ "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
"integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
@@ -4662,118 +5071,16 @@
}
}
},
- "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
- "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "7.18.0",
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz",
- "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "7.17.0",
- "@typescript-eslint/visitor-keys": "7.17.0"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/type-utils": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz",
- "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/typescript-estree": "7.17.0",
- "@typescript-eslint/utils": "7.17.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz",
- "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==",
- "dev": true,
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz",
- "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "7.17.0",
- "@typescript-eslint/visitor-keys": "7.17.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^1.3.0"
- },
- "engines": {
- "node": "^18.18.0 || >=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
"node_modules/@typescript-eslint/utils": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz",
- "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "7.17.0",
- "@typescript-eslint/types": "7.17.0",
- "@typescript-eslint/typescript-estree": "7.17.0"
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4787,12 +5094,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.17.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz",
- "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.17.0",
+ "@typescript-eslint/types": "7.18.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -4853,20 +5160,6 @@
"vitest": "1.6.0"
}
},
- "node_modules/@vitest/coverage-v8/node_modules/istanbul-lib-source-maps": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
- "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
- "dev": true,
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.23",
- "debug": "^4.1.1",
- "istanbul-lib-coverage": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@vitest/expect": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
@@ -4911,9 +5204,9 @@
}
},
"node_modules/@vitest/runner/node_modules/yocto-queue": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
- "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
+ "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
"dev": true,
"engines": {
"node": ">=12.20"
@@ -5041,9 +5334,9 @@
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
},
"node_modules/acorn": {
- "version": "8.11.3",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
- "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+ "version": "8.12.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
+ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -5062,10 +5355,13 @@
}
},
"node_modules/acorn-walk": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
- "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
+ "version": "8.3.3",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
+ "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
"dev": true,
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
"engines": {
"node": ">=0.4.0"
}
@@ -5098,6 +5394,21 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-escapes": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
+ "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "dev": true,
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -5338,9 +5649,9 @@
"dev": true
},
"node_modules/autoprefixer": {
- "version": "10.4.19",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
- "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
+ "version": "10.4.20",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"dev": true,
"funding": [
{
@@ -5357,11 +5668,11 @@
}
],
"dependencies": {
- "browserslist": "^4.23.0",
- "caniuse-lite": "^1.0.30001599",
+ "browserslist": "^4.23.3",
+ "caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
- "picocolors": "^1.0.0",
+ "picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -5390,9 +5701,9 @@
}
},
"node_modules/axe-core": {
- "version": "4.9.1",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz",
- "integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==",
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz",
+ "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==",
"dev": true,
"engines": {
"node": ">=4"
@@ -5452,9 +5763,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.23.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
- "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
+ "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"funding": [
{
"type": "opencollective",
@@ -5470,10 +5781,10 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001587",
- "electron-to-chromium": "^1.4.668",
- "node-releases": "^2.0.14",
- "update-browserslist-db": "^1.0.13"
+ "caniuse-lite": "^1.0.30001646",
+ "electron-to-chromium": "^1.5.4",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -5528,9 +5839,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001607",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001607.tgz",
- "integrity": "sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w==",
+ "version": "1.0.30001646",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz",
+ "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==",
"funding": [
{
"type": "opencollective",
@@ -5556,9 +5867,9 @@
}
},
"node_modules/chai": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
- "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
@@ -5567,7 +5878,7 @@
"get-func-name": "^2.0.2",
"loupe": "^2.3.6",
"pathval": "^1.1.1",
- "type-detect": "^4.0.8"
+ "type-detect": "^4.1.0"
},
"engines": {
"node": ">=4"
@@ -5590,9 +5901,9 @@
}
},
"node_modules/character-entities": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
- "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -5608,18 +5919,18 @@
}
},
"node_modules/character-entities-legacy": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
- "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-reference-invalid": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
- "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -5672,15 +5983,15 @@
}
},
"node_modules/cli-cursor": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
- "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"dependencies": {
- "restore-cursor": "^4.0.0"
+ "restore-cursor": "^5.0.0"
},
"engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -5771,9 +6082,9 @@
}
},
"node_modules/comma-separated-tokens": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
- "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -5799,6 +6110,12 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/confbox": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
+ "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==",
+ "dev": true
+ },
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -5860,6 +6177,12 @@
"node": ">=18"
}
},
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
+ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
+ "dev": true
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -5936,9 +6259,9 @@
}
},
"node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
+ "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": {
"ms": "2.1.2"
},
@@ -5969,19 +6292,10 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/decode-named-character-reference/node_modules/character-entities": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
- "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/deep-eql": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
- "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
+ "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
"dev": true,
"dependencies": {
"type-detect": "^4.0.0"
@@ -6160,9 +6474,9 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/electron-to-chromium": {
- "version": "1.4.729",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz",
- "integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA=="
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz",
+ "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -6181,6 +6495,18 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -6421,7 +6747,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -6936,18 +7261,6 @@
"node": "*"
}
},
- "node_modules/eslint/node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -6966,9 +7279,9 @@
}
},
"node_modules/esquery": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
- "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"dependencies": {
"estraverse": "^5.1.0"
@@ -7031,6 +7344,29 @@
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true
},
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -7183,9 +7519,9 @@
}
},
"node_modules/foreground-child": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
- "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
+ "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
@@ -7197,17 +7533,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/foreground-child/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -7244,9 +7569,9 @@
}
},
"node_modules/framer-motion": {
- "version": "11.3.8",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.8.tgz",
- "integrity": "sha512-1D+RDTsIp4Rz2dq/oToqSEc9idEQwgBRQyBq4rGpFba+0Z+GCbj9z1s0+ikFbanWe3YJ0SqkNlDe08GcpFGj5A==",
+ "version": "11.3.21",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.21.tgz",
+ "integrity": "sha512-D+hfIsvzV8eL/iycld4K+tKlg2Q2LdwnrcBEohtGw3cG1AIuNYATbT5RUqIM1ndsAk+EfGhoSGf0UaiFodc5Tw==",
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -7378,6 +7703,18 @@
"node": ">=6"
}
},
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-symbol-description": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -7399,6 +7736,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -7457,12 +7795,13 @@
}
},
"node_modules/globalthis": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
- "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
"dependencies": {
- "define-properties": "^1.1.3"
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -7638,46 +7977,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/hast-util-to-jsx-runtime/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
- "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
- "node_modules/hast-util-to-jsx-runtime/node_modules/comma-separated-tokens": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
- "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
- "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/hast-util-to-jsx-runtime/node_modules/space-separated-tokens": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
- "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@@ -7690,14 +7989,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/hast-util-whitespace/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
"node_modules/hastscript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
@@ -7714,6 +8005,49 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hastscript/node_modules/@types/hast": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
+ "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "node_modules/hastscript/node_modules/@types/unist": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
+ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+ },
+ "node_modules/hastscript/node_modules/comma-separated-tokens": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
+ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hastscript/node_modules/property-information": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
+ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hastscript/node_modules/space-separated-tokens": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
+ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
@@ -7783,6 +8117,15 @@
"node": ">= 14"
}
},
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
"node_modules/husky": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz",
@@ -7858,9 +8201,9 @@
}
},
"node_modules/immer": {
- "version": "10.0.4",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz",
- "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -7904,6 +8247,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
@@ -7955,21 +8299,21 @@
}
},
"node_modules/is-alphabetical": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
- "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
- "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"dependencies": {
- "is-alphabetical": "^1.0.0",
- "is-decimal": "^1.0.0"
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
@@ -8008,6 +8352,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
"node_modules/is-async-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
@@ -8075,11 +8424,14 @@
}
},
"node_modules/is-core-module": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
- "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
+ "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"dependencies": {
- "hasown": "^2.0.0"
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -8116,9 +8468,9 @@
}
},
"node_modules/is-decimal": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
- "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -8183,9 +8535,9 @@
}
},
"node_modules/is-hexadecimal": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
- "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -8307,6 +8659,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-string": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@@ -8426,6 +8790,20 @@
"node": ">=10"
}
},
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/istanbul-reports": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
@@ -8453,15 +8831,12 @@
}
},
"node_modules/jackspeak": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
- "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
- "engines": {
- "node": ">=14"
- },
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
@@ -8470,9 +8845,9 @@
}
},
"node_modules/jiti": {
- "version": "1.21.0",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
- "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+ "version": "1.21.6",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+ "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"bin": {
"jiti": "bin/jiti.js"
}
@@ -8542,12 +8917,6 @@
}
}
},
- "node_modules/jsdom/node_modules/rrweb-cssom": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
- "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
- "dev": true
- },
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -8588,12 +8957,6 @@
"node": ">=6"
}
},
- "node_modules/jsonc-parser": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
- "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
- "dev": true
- },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -8619,9 +8982,9 @@
}
},
"node_modules/language-subtag-registry": {
- "version": "0.3.22",
- "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
- "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
"dev": true
},
"node_modules/language-tags": {
@@ -8650,9 +9013,9 @@
}
},
"node_modules/lilconfig": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
- "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
+ "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"engines": {
"node": ">=14"
},
@@ -8666,21 +9029,21 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/lint-staged": {
- "version": "15.2.7",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz",
- "integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==",
+ "version": "15.2.8",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.8.tgz",
+ "integrity": "sha512-PUWFf2zQzsd9EFU+kM1d7UP+AZDbKFKuj+9JNVTBkhUFhbg4MAt6WfyMMwBfM4lYqd4D2Jwac5iuTu9rVj4zCQ==",
"dev": true,
"dependencies": {
"chalk": "~5.3.0",
"commander": "~12.1.0",
- "debug": "~4.3.4",
+ "debug": "~4.3.6",
"execa": "~8.0.1",
- "lilconfig": "~3.1.1",
- "listr2": "~8.2.1",
+ "lilconfig": "~3.1.2",
+ "listr2": "~8.2.4",
"micromatch": "~4.0.7",
"pidtree": "~0.6.0",
"string-argv": "~0.3.2",
- "yaml": "~2.4.2"
+ "yaml": "~2.5.0"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
@@ -8704,151 +9067,17 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/lint-staged/node_modules/execa": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
- "dev": true,
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/lint-staged/node_modules/get-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
- "dev": true,
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/human-signals": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
- "dev": true,
- "engines": {
- "node": ">=16.17.0"
- }
- },
- "node_modules/lint-staged/node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "dev": true,
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/npm-run-path": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
- "dev": true,
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
- "dev": true,
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/lint-staged/node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/listr2": {
- "version": "8.2.1",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.1.tgz",
- "integrity": "sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g==",
+ "version": "8.2.4",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz",
+ "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==",
"dev": true,
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
- "log-update": "^6.0.0",
- "rfdc": "^1.3.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
@@ -8941,14 +9170,14 @@
"integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg=="
},
"node_modules/log-update": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz",
- "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"dependencies": {
- "ansi-escapes": "^6.2.0",
- "cli-cursor": "^4.0.0",
- "slice-ansi": "^7.0.0",
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
@@ -8959,18 +9188,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/log-update/node_modules/ansi-escapes": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz",
- "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==",
- "dev": true,
- "engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/log-update/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@@ -9102,12 +9319,12 @@
}
},
"node_modules/magic-string": {
- "version": "0.30.10",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
- "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
+ "version": "0.30.11",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+ "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"dev": true,
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.15"
+ "@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/magicast": {
@@ -9137,9 +9354,9 @@
}
},
"node_modules/mdast-util-from-markdown": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz",
- "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz",
+ "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==",
"dependencies": {
"@types/mdast": "^4.0.0",
"@types/unist": "^3.0.0",
@@ -9159,11 +9376,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-from-markdown/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz",
@@ -9181,14 +9393,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-mdx-expression/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
"node_modules/mdast-util-mdx-jsx": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz",
@@ -9213,110 +9417,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-mdx-jsx/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/character-entities": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
- "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
- "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/character-reference-invalid": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
- "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
- "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
- "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
- "dependencies": {
- "is-alphabetical": "^2.0.0",
- "is-decimal": "^2.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
- "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
- "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz",
- "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "character-entities": "^2.0.0",
- "character-entities-legacy": "^3.0.0",
- "character-reference-invalid": "^2.0.0",
- "decode-named-character-reference": "^1.0.0",
- "is-alphanumerical": "^2.0.0",
- "is-decimal": "^2.0.0",
- "is-hexadecimal": "^2.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/node_modules/@types/unist": {
- "version": "2.0.10",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
- "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
- },
"node_modules/mdast-util-mdxjs-esm": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
@@ -9334,14 +9434,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-mdxjs-esm/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
"node_modules/mdast-util-phrasing": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
@@ -9356,9 +9448,9 @@
}
},
"node_modules/mdast-util-to-hast": {
- "version": "13.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz",
- "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==",
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
@@ -9375,14 +9467,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-to-hast/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
"node_modules/mdast-util-to-markdown": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz",
@@ -9402,11 +9486,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-to-markdown/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/mdast-util-to-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
@@ -9888,12 +9967,27 @@
}
},
"node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"dev": true,
"engines": {
- "node": ">=6"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
@@ -9906,9 +10000,9 @@
}
},
"node_modules/minimatch": {
- "version": "9.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
- "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -9929,23 +10023,23 @@
}
},
"node_modules/minipass": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
- "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mlly": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
- "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz",
+ "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"pathe": "^1.1.2",
- "pkg-types": "^1.0.3",
- "ufo": "^1.3.2"
+ "pkg-types": "^1.1.1",
+ "ufo": "^1.5.3"
}
},
"node_modules/monaco-editor": {
@@ -10030,9 +10124,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
- "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="
},
"node_modules/normalize-path": {
"version": "3.0.0",
@@ -10051,6 +10145,33 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz",
@@ -10074,10 +10195,13 @@
}
},
"node_modules/object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -10198,32 +10322,32 @@
}
},
"node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"dev": true,
"dependencies": {
- "mimic-fn": "^2.1.0"
+ "mimic-fn": "^4.0.0"
},
"engines": {
- "node": ">=6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
- "version": "0.9.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
- "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"dependencies": {
- "@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
- "type-check": "^0.4.0"
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
@@ -10259,6 +10383,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
+ "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -10272,22 +10401,29 @@
}
},
"node_modules/parse-entities": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
- "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz",
+ "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==",
"dependencies": {
- "character-entities": "^1.0.0",
- "character-entities-legacy": "^1.0.0",
- "character-reference-invalid": "^1.0.0",
- "is-alphanumerical": "^1.0.0",
- "is-decimal": "^1.0.0",
- "is-hexadecimal": "^1.0.0"
+ "@types/unist": "^2.0.0",
+ "character-entities": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
+ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+ },
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -10332,27 +10468,24 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
- "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
- "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
- "engines": {
- "node": "14 || >=16.14"
- }
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -10423,14 +10556,14 @@
}
},
"node_modules/pkg-types": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz",
- "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz",
+ "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==",
"dev": true,
"dependencies": {
- "jsonc-parser": "^3.2.0",
- "mlly": "^1.2.0",
- "pathe": "^1.1.0"
+ "confbox": "^0.1.7",
+ "mlly": "^1.7.1",
+ "pathe": "^1.1.2"
}
},
"node_modules/possible-typed-array-names": {
@@ -10443,9 +10576,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.40",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
- "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
+ "version": "8.4.41",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
+ "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"funding": [
{
"type": "opencollective",
@@ -10538,27 +10671,46 @@
}
},
"node_modules/postcss-nested": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
- "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"dependencies": {
- "postcss-selector-parser": "^6.0.11"
+ "postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
"peerDependencies": {
"postcss": "^8.2.14"
}
},
+ "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
+ "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/postcss-selector-parser": {
- "version": "6.0.16",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
- "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -10662,12 +10814,9 @@
"dev": true
},
"node_modules/property-information": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
- "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
- "dependencies": {
- "xtend": "^4.0.0"
- },
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
+ "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -10760,9 +10909,9 @@
}
},
"node_modules/react-i18next": {
- "version": "15.0.0",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.0.tgz",
- "integrity": "sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==",
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.1.tgz",
+ "integrity": "sha512-NwxLqNM6CLbeGA9xPsjits0EnXdKgCRSS6cgkgOdNcPXqL+1fYNl8fBg1wmnnHvFy812Bt4IWTPE9zjoPmFj3w==",
"dependencies": {
"@babel/runtime": "^7.24.8",
"html-parse-stringify": "^3.0.1"
@@ -10820,14 +10969,6 @@
"react": ">=18"
}
},
- "node_modules/react-markdown/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
"node_modules/react-redux": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
@@ -11066,6 +11207,90 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/refractor/node_modules/character-entities": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/character-entities-legacy": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/character-reference-invalid": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-alphabetical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-alphanumerical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+ "dependencies": {
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-decimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
+ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/is-hexadecimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/parse-entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
+ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "dependencies": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/refractor/node_modules/prismjs": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
@@ -11128,14 +11353,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/remark-rehype/node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -11143,9 +11360,9 @@
"dev": true
},
"node_modules/reselect": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz",
- "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg=="
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resolve": {
"version": "1.22.8",
@@ -11173,16 +11390,31 @@
}
},
"node_modules/restore-cursor": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
- "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
},
"engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -11198,15 +11430,16 @@
}
},
"node_modules/rfdc": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz",
- "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
@@ -11219,9 +11452,9 @@
}
},
"node_modules/rollup": {
- "version": "4.14.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz",
- "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==",
+ "version": "4.19.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.2.tgz",
+ "integrity": "sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==",
"dependencies": {
"@types/estree": "1.0.5"
},
@@ -11233,28 +11466,29 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.14.1",
- "@rollup/rollup-android-arm64": "4.14.1",
- "@rollup/rollup-darwin-arm64": "4.14.1",
- "@rollup/rollup-darwin-x64": "4.14.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.14.1",
- "@rollup/rollup-linux-arm64-gnu": "4.14.1",
- "@rollup/rollup-linux-arm64-musl": "4.14.1",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.14.1",
- "@rollup/rollup-linux-s390x-gnu": "4.14.1",
- "@rollup/rollup-linux-x64-gnu": "4.14.1",
- "@rollup/rollup-linux-x64-musl": "4.14.1",
- "@rollup/rollup-win32-arm64-msvc": "4.14.1",
- "@rollup/rollup-win32-ia32-msvc": "4.14.1",
- "@rollup/rollup-win32-x64-msvc": "4.14.1",
+ "@rollup/rollup-android-arm-eabi": "4.19.2",
+ "@rollup/rollup-android-arm64": "4.19.2",
+ "@rollup/rollup-darwin-arm64": "4.19.2",
+ "@rollup/rollup-darwin-x64": "4.19.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.19.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.19.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.19.2",
+ "@rollup/rollup-linux-arm64-musl": "4.19.2",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.19.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.19.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.19.2",
+ "@rollup/rollup-linux-x64-gnu": "4.19.2",
+ "@rollup/rollup-linux-x64-musl": "4.19.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.19.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.19.2",
+ "@rollup/rollup-win32-x64-msvc": "4.19.2",
"fsevents": "~2.3.2"
}
},
"node_modules/rrweb-cssom": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
- "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
"dev": true
},
"node_modules/run-parallel": {
@@ -11349,13 +11583,10 @@
}
},
"node_modules/semver": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
- "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
"bin": {
"semver": "bin/semver.js"
},
@@ -11363,24 +11594,6 @@
"node": ">=10"
}
},
- "node_modules/semver/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/semver/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
- },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -11457,10 +11670,15 @@
"dev": true
},
"node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "dev": true
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
@@ -11470,11 +11688,6 @@
"is-arrayish": "^0.3.1"
}
},
- "node_modules/simple-swizzle/node_modules/is-arrayish": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
- },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -11521,9 +11734,9 @@
}
},
"node_modules/space-separated-tokens": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
- "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -11568,9 +11781,9 @@
}
},
"node_modules/string-width": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz",
- "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
@@ -11752,15 +11965,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/stringify-entities/node_modules/character-entities-legacy": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
- "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -11784,6 +11988,27 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -11864,22 +12089,20 @@
}
},
"node_modules/sucrase/node_modules/glob": {
- "version": "10.3.12",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
- "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dependencies": {
"foreground-child": "^3.1.0",
- "jackspeak": "^2.3.6",
- "minimatch": "^9.0.1",
- "minipass": "^7.0.4",
- "path-scurry": "^1.10.2"
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
@@ -11963,9 +12186,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.7",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
- "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
+ "version": "3.4.9",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
+ "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -12006,6 +12229,18 @@
"node": ">=10"
}
},
+ "node_modules/tailwindcss/node_modules/postcss-selector-parser": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
+ "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/test-exclude": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
@@ -12068,9 +12303,9 @@
}
},
"node_modules/tinybench": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.7.0.tgz",
- "integrity": "sha512-Qgayeb106x2o4hNzNjsZEfFziw8IbKqtbXBjVh7VIZfBxfD5M4gWtpyx5+YTae2gJ6Y6Dz/KLepiv16RFeQWNA==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
+ "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
"dev": true
},
"node_modules/tinypool": {
@@ -12216,19 +12451,10 @@
"json5": "lib/cli.js"
}
},
- "node_modules/tsconfig-paths/node_modules/strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
+ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -12243,14 +12469,26 @@
}
},
"node_modules/type-detect": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
- "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/typed-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
@@ -12338,9 +12576,9 @@
}
},
"node_modules/ufo": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
- "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==",
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
+ "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
"dev": true
},
"node_modules/unbox-primitive": {
@@ -12359,15 +12597,15 @@
}
},
"node_modules/undici-types": {
- "version": "6.11.1",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz",
- "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==",
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
+ "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
"devOptional": true
},
"node_modules/unified": {
- "version": "11.0.4",
- "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz",
- "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==",
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
"dependencies": {
"@types/unist": "^3.0.0",
"bail": "^2.0.0",
@@ -12382,11 +12620,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unified/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
@@ -12399,11 +12632,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-is/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
@@ -12416,11 +12644,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-position/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/unist-util-remove-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
@@ -12434,11 +12657,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-remove-position/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@@ -12451,11 +12669,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-stringify-position/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
@@ -12483,16 +12696,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-visit-parents/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
- "node_modules/unist-util-visit/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
@@ -12503,9 +12706,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.13",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
- "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
+ "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"funding": [
{
"type": "opencollective",
@@ -12521,8 +12724,8 @@
}
],
"dependencies": {
- "escalade": "^3.1.1",
- "picocolors": "^1.0.0"
+ "escalade": "^3.1.2",
+ "picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -12629,9 +12832,9 @@
}
},
"node_modules/use-sync-external-store": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
- "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+ "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
@@ -12642,9 +12845,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vfile": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
- "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz",
+ "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0",
@@ -12668,23 +12871,13 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/vfile-message/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
- "node_modules/vfile/node_modules/@types/unist": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
- "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
- },
"node_modules/vite": {
- "version": "5.3.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
- "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
+ "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
"dependencies": {
"esbuild": "^0.21.3",
- "postcss": "^8.4.39",
+ "postcss": "^8.4.40",
"rollup": "^4.13.0"
},
"bin": {
@@ -12704,6 +12897,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
+ "sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -12721,6 +12915,9 @@
"sass": {
"optional": true
},
+ "sass-embedded": {
+ "optional": true
+ },
"stylus": {
"optional": true
},
@@ -12838,140 +13035,6 @@
}
}
},
- "node_modules/vitest/node_modules/execa": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
- "dev": true,
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/vitest/node_modules/get-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
- "dev": true,
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/vitest/node_modules/human-signals": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
- "dev": true,
- "engines": {
- "node": ">=16.17.0"
- }
- },
- "node_modules/vitest/node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "dev": true,
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/vitest/node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/vitest/node_modules/npm-run-path": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
- "dev": true,
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/vitest/node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
- "dev": true,
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/vitest/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/vitest/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/vitest/node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@@ -13071,13 +13134,13 @@
}
},
"node_modules/which-builtin-type": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
- "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz",
+ "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==",
"dev": true,
"dependencies": {
- "function.prototype.name": "^1.1.5",
- "has-tostringtag": "^1.0.0",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
"is-async-function": "^2.0.0",
"is-date-object": "^1.0.5",
"is-finalizationregistry": "^1.0.2",
@@ -13086,8 +13149,8 @@
"is-weakref": "^1.0.2",
"isarray": "^2.0.5",
"which-boxed-primitive": "^1.0.2",
- "which-collection": "^1.0.1",
- "which-typed-array": "^1.1.9"
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.15"
},
"engines": {
"node": ">= 0.4"
@@ -13134,9 +13197,9 @@
}
},
"node_modules/why-is-node-running": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz",
- "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"dependencies": {
"siginfo": "^2.0.0",
@@ -13149,6 +13212,15 @@
"node": ">=8"
}
},
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
@@ -13304,9 +13376,9 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/yaml": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
- "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
+ "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
"bin": {
"yaml": "bin.mjs"
},
diff --git a/frontend/package.json b/frontend/package.json
index abbdc218e9..3ec031b43b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -25,14 +25,14 @@
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
- "react-i18next": "^15.0.0",
+ "react-i18next": "^15.0.1",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.4.0",
- "vite": "^5.3.5",
+ "vite": "^5.4.0",
"web-vitals": "^3.5.2"
},
"scripts": {
@@ -60,19 +60,19 @@
]
},
"devDependencies": {
- "@tailwindcss/typography": "^0.5.13",
+ "@tailwindcss/typography": "^0.5.14",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
- "@types/node": "^22.0.0",
+ "@types/node": "^22.1.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
- "@typescript-eslint/eslint-plugin": "^7.17.0",
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
- "autoprefixer": "^10.4.19",
+ "autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
@@ -84,10 +84,10 @@
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.4",
"jsdom": "^24.1.1",
- "lint-staged": "^15.2.7",
- "postcss": "^8.4.40",
+ "lint-staged": "^15.2.8",
+ "postcss": "^8.4.41",
"prettier": "^3.3.3",
- "tailwindcss": "^3.4.7",
+ "tailwindcss": "^3.4.9",
"typescript": "^5.5.4",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0"
diff --git a/frontend/src/components/AgentControlBar.tsx b/frontend/src/components/AgentControlBar.tsx
index 04f1775b23..7c4437086d 100644
--- a/frontend/src/components/AgentControlBar.tsx
+++ b/frontend/src/components/AgentControlBar.tsx
@@ -8,6 +8,7 @@ import { changeAgentState } from "#/services/agentStateService";
import store, { RootState } from "#/store";
import AgentState from "#/types/AgentState";
import { clearMessages } from "#/state/chatSlice";
+import Session from "#/services/session";
const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
[AgentState.PAUSED]: [
@@ -83,6 +84,7 @@ function AgentControlBar() {
}
if (action === AgentState.STOPPED) {
+ Session._history = [];
store.dispatch(clearMessages());
} else {
setIsLoading(true);
diff --git a/frontend/src/components/Resizable.tsx b/frontend/src/components/Resizable.tsx
index 4d7663e459..5bdc98afb2 100644
--- a/frontend/src/components/Resizable.tsx
+++ b/frontend/src/components/Resizable.tsx
@@ -25,49 +25,58 @@ export function Container({
orientation,
initialSize,
}: ContainerProps): JSX.Element {
- const [firstSize, setFirstSize] = useState(initialSize);
- const [dividerPosition, setDividerPosition] = useState(
- undefined,
- );
+ const [firstSize, setFirstSize] = useState(initialSize);
+ const [dividerPosition, setDividerPosition] = useState(null);
const firstRef = useRef(null);
useEffect(() => {
- if (firstRef.current !== null) {
- if (orientation === Orientation.HORIZONTAL) {
- firstRef.current.style.width = `${firstSize}px`;
- } else {
- firstRef.current.style.height = `${firstSize}px`;
- }
+ if (dividerPosition == null || !firstRef.current) {
+ return undefined;
}
- }, [firstSize, orientation]);
-
- const onMouseMove = (e: MouseEvent) => {
- e.preventDefault();
- if (firstSize && dividerPosition) {
- if (orientation === Orientation.HORIZONTAL) {
- const newLeftWidth = firstSize + e.clientX - dividerPosition;
- setDividerPosition(e.clientX);
- setFirstSize(newLeftWidth);
- } else {
- const newTopHeight = firstSize + e.clientY - dividerPosition;
- setDividerPosition(e.clientY);
- setFirstSize(newTopHeight);
+ const getFirstSizeFromEvent = (e: MouseEvent) => {
+ const position =
+ orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY;
+ return firstSize + position - dividerPosition;
+ };
+ const onMouseMove = (e: MouseEvent) => {
+ e.preventDefault();
+ const newFirstSize = getFirstSizeFromEvent(e);
+ const { current } = firstRef;
+ if (current) {
+ if (orientation === Orientation.HORIZONTAL) {
+ current.style.width = `${newFirstSize}px`;
+ } else {
+ current.style.height = `${newFirstSize}px`;
+ }
}
- }
- };
-
- const onMouseUp = () => {
- document.removeEventListener("mousemove", onMouseMove);
- document.removeEventListener("mouseup", onMouseUp);
- };
+ };
+ const onMouseUp = (e: MouseEvent) => {
+ e.preventDefault();
+ setFirstSize(getFirstSizeFromEvent(e));
+ setDividerPosition(null);
+ document.removeEventListener("mousemove", onMouseMove);
+ document.removeEventListener("mouseup", onMouseUp);
+ };
+ document.addEventListener("mousemove", onMouseMove);
+ document.addEventListener("mouseup", onMouseUp);
+ return () => {
+ document.removeEventListener("mousemove", onMouseMove);
+ document.removeEventListener("mouseup", onMouseUp);
+ };
+ }, [dividerPosition, firstSize, orientation]);
const onMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
- setDividerPosition(
- orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY,
- );
- document.addEventListener("mousemove", onMouseMove);
- document.addEventListener("mouseup", onMouseUp);
+ const position =
+ orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY;
+ setDividerPosition(position);
+ };
+
+ const getStyleForFirst = () => {
+ if (orientation === Orientation.HORIZONTAL) {
+ return { width: `${firstSize}px` };
+ }
+ return { height: `${firstSize}px` };
};
return (
@@ -77,7 +86,7 @@ export function Container({
className,
)}
>
-
+
{firstChild}
{
diff --git a/frontend/src/components/chat/ChatInput.test.tsx b/frontend/src/components/chat/ChatInput.test.tsx
index 02b7863080..0947f282ac 100644
--- a/frontend/src/components/chat/ChatInput.test.tsx
+++ b/frontend/src/components/chat/ChatInput.test.tsx
@@ -55,7 +55,7 @@ describe("ChatInput", () => {
await user.type(textarea, "Hello, world!");
await user.click(button);
- expect(onSendMessage).toHaveBeenCalledWith("Hello, world!");
+ expect(onSendMessage).toHaveBeenCalledWith("Hello, world!", []);
// Additionally, check if it was called exactly once
expect(onSendMessage).toHaveBeenCalledTimes(1);
});
@@ -68,7 +68,7 @@ describe("ChatInput", () => {
await user.type(textarea, "Hello, world!");
await user.keyboard("{Enter}");
- expect(onSendMessage).toHaveBeenCalledWith("Hello, world!");
+ expect(onSendMessage).toHaveBeenCalledWith("Hello, world!", []);
});
it("should NOT send a message when shift + enter is pressed", async () => {
diff --git a/frontend/src/components/chat/ChatInput.tsx b/frontend/src/components/chat/ChatInput.tsx
index c92dddd1c1..7019c18607 100644
--- a/frontend/src/components/chat/ChatInput.tsx
+++ b/frontend/src/components/chat/ChatInput.tsx
@@ -1,26 +1,44 @@
import { Textarea } from "@nextui-org/react";
import React from "react";
import { useTranslation } from "react-i18next";
-import { VscArrowUp } from "react-icons/vsc";
+import { VscArrowUp, VscFileMedia } from "react-icons/vsc";
import { twMerge } from "tailwind-merge";
import { I18nKey } from "#/i18n/declaration";
interface ChatInputProps {
disabled?: boolean;
- onSendMessage: (message: string) => void;
+ onSendMessage: (message: string, image_urls: string[]) => void;
}
function ChatInput({ disabled = false, onSendMessage }: ChatInputProps) {
const { t } = useTranslation();
const [message, setMessage] = React.useState("");
+ const [files, setFiles] = React.useState
([]);
// This is true when the user is typing in an IME (e.g., Chinese, Japanese)
const [isComposing, setIsComposing] = React.useState(false);
- const handleSendChatMessage = () => {
+ const convertImageToBase64 = (file: File): Promise =>
+ new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ resolve(reader.result as string);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+
+ const handleSendChatMessage = async () => {
if (message.trim()) {
- onSendMessage(message);
+ let base64images: string[] = [];
+ if (files.length > 0) {
+ base64images = await Promise.all(
+ files.map((file) => convertImageToBase64(file)),
+ );
+ }
+ onSendMessage(message, base64images);
setMessage("");
+ setFiles([]);
}
};
@@ -33,6 +51,33 @@ function ChatInput({ disabled = false, onSendMessage }: ChatInputProps) {
}
};
+ const handleFileChange = (event: React.ChangeEvent) => {
+ if (event.target.files) {
+ setFiles((prev) => [...prev, ...Array.from(event.target.files!)]);
+ }
+ };
+
+ const removeFile = (index: number) => {
+ setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
+ };
+
+ const handlePaste = (event: React.ClipboardEvent) => {
+ const clipboardItems = Array.from(event.clipboardData.items);
+ const pastedFiles: File[] = [];
+ clipboardItems.forEach((item) => {
+ if (item.type.startsWith("image/")) {
+ const file = item.getAsFile();
+ if (file) {
+ pastedFiles.push(file);
+ }
+ }
+ });
+ if (pastedFiles.length > 0) {
+ setFiles((prevFiles) => [...prevFiles, ...pastedFiles]);
+ event.preventDefault();
+ }
+ };
+
return (
);
}
diff --git a/frontend/src/components/chat/ChatInterface.test.tsx b/frontend/src/components/chat/ChatInterface.test.tsx
index 5e982e1e71..cec7b98c8b 100644
--- a/frontend/src/components/chat/ChatInterface.test.tsx
+++ b/frontend/src/components/chat/ChatInterface.test.tsx
@@ -19,7 +19,7 @@ describe("ChatInterface", () => {
const userMessageEvent = {
action: ActionType.MESSAGE,
- args: { content: "my message" },
+ args: { content: "my message", images_urls: [] },
};
afterEach(() => {
@@ -35,7 +35,7 @@ describe("ChatInterface", () => {
const { store } = renderWithProviders(, {
preloadedState: {
chat: {
- messages: [{ sender: "user", content: "Hello" }],
+ messages: [{ sender: "user", content: "Hello", imageUrls: [] }],
},
},
});
diff --git a/frontend/src/components/chat/ChatInterface.tsx b/frontend/src/components/chat/ChatInterface.tsx
index b08bc23ebd..89b2012ec7 100644
--- a/frontend/src/components/chat/ChatInterface.tsx
+++ b/frontend/src/components/chat/ChatInterface.tsx
@@ -66,14 +66,14 @@ function ChatInterface() {
setFeedbackPolarity(polarity);
};
- const handleSendMessage = (content: string) => {
- dispatch(addUserMessage(content));
- sendChatMessage(content);
+ const handleSendMessage = (content: string, imageUrls: string[]) => {
+ dispatch(addUserMessage({ content, imageUrls }));
+ sendChatMessage(content, imageUrls);
};
const { t } = useTranslation();
const handleSendContinueMsg = () => {
- handleSendMessage(t(I18nKey.CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE));
+ handleSendMessage(t(I18nKey.CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE), []);
};
const scrollRef = useRef(null);
diff --git a/frontend/src/components/chat/ChatMessage.test.tsx b/frontend/src/components/chat/ChatMessage.test.tsx
index 9d902b8652..0f0d3becd2 100644
--- a/frontend/src/components/chat/ChatMessage.test.tsx
+++ b/frontend/src/components/chat/ChatMessage.test.tsx
@@ -9,7 +9,7 @@ describe("Message", () => {
it("should render a user message", () => {
render(
,
);
@@ -21,7 +21,7 @@ describe("Message", () => {
it("should render an assistant message", () => {
render(
,
);
@@ -36,6 +36,7 @@ describe("Message", () => {
message={{
sender: "user",
content: "```js\nconsole.log('Hello')\n```",
+ imageUrls: [],
}}
isLastMessage={false}
/>,
@@ -55,7 +56,7 @@ describe("Message", () => {
const user = userEvent.setup();
render(
,
);
@@ -79,7 +80,7 @@ describe("Message", () => {
const user = userEvent.setup();
render(
,
);
@@ -113,7 +114,11 @@ describe("Message", () => {
// it should not render buttons if the message is not the last one
const { rerender } = render(
,
@@ -123,7 +128,7 @@ describe("Message", () => {
// it should not render buttons if the message is not from the assistant
rerender(
,
@@ -133,7 +138,11 @@ describe("Message", () => {
// it should not render buttons if the message is not awaiting user confirmation
rerender(
,
@@ -143,7 +152,11 @@ describe("Message", () => {
// it should render buttons if all conditions are met
rerender(
,
diff --git a/frontend/src/components/chat/ChatMessage.tsx b/frontend/src/components/chat/ChatMessage.tsx
index be19b07f67..ed8a283d4c 100644
--- a/frontend/src/components/chat/ChatMessage.tsx
+++ b/frontend/src/components/chat/ChatMessage.tsx
@@ -77,6 +77,18 @@ function ChatMessage({
)}
{message.content}
+ {message.imageUrls.length > 0 && (
+
+ {message.imageUrls.map((url, index) => (
+

+ ))}
+
+ )}
{isLastMessage &&
message.sender === "assistant" &&
awaitingUserConfirmation && }
diff --git a/frontend/src/components/chat/message.d.ts b/frontend/src/components/chat/message.d.ts
index 6a3aa49167..e7431412fc 100644
--- a/frontend/src/components/chat/message.d.ts
+++ b/frontend/src/components/chat/message.d.ts
@@ -1,4 +1,5 @@
type Message = {
sender: "user" | "assistant";
content: string;
+ imageUrls: string[];
};
diff --git a/frontend/src/components/file-explorer/ExplorerTree.tsx b/frontend/src/components/file-explorer/ExplorerTree.tsx
index 0356f505e0..8ab18bb267 100644
--- a/frontend/src/components/file-explorer/ExplorerTree.tsx
+++ b/frontend/src/components/file-explorer/ExplorerTree.tsx
@@ -1,5 +1,7 @@
import React from "react";
+import { useTranslation } from "react-i18next";
import TreeNode from "./TreeNode";
+import { I18nKey } from "#/i18n/declaration";
interface ExplorerTreeProps {
files: string[];
@@ -7,6 +9,14 @@ interface ExplorerTreeProps {
}
function ExplorerTree({ files, defaultOpen = false }: ExplorerTreeProps) {
+ const { t } = useTranslation();
+ if (files.length === 0) {
+ return (
+
+ {t(I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE)}
+
+ );
+ }
return (
{files.map((file) => (
diff --git a/frontend/src/components/file-explorer/FileExplorer.tsx b/frontend/src/components/file-explorer/FileExplorer.tsx
index e63d7537c2..7691dc143e 100644
--- a/frontend/src/components/file-explorer/FileExplorer.tsx
+++ b/frontend/src/components/file-explorer/FileExplorer.tsx
@@ -108,11 +108,8 @@ function FileExplorer() {
}
dispatch(setRefreshID(Math.random()));
try {
- const fileList = await listFiles("/");
+ const fileList = await listFiles();
setFiles(fileList);
- if (fileList.length === 0) {
- toast.info(t(I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE));
- }
} catch (error) {
toast.error("refresh-error", t(I18nKey.EXPLORER$REFRESH_ERROR_MESSAGE));
}
@@ -188,10 +185,6 @@ function FileExplorer() {
};
}, []);
- if (!files.length) {
- return null;
- }
-
return (
{isDragging && (
@@ -219,19 +212,17 @@ function FileExplorer() {
isHidden ? "w-12" : "w-60",
)}
>
-
+
{!isHidden && (
-
-
- {t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
-
+
+ {t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
)}
{
if (message.source === "user") {
- store.dispatch(addUserMessage(message.args.content));
+ store.dispatch(
+ addUserMessage({ content: message.args.content, imageUrls: [] }),
+ );
} else {
store.dispatch(addAssistantMessage(message.args.content));
}
diff --git a/frontend/src/services/chatService.ts b/frontend/src/services/chatService.ts
index af1ab45ce8..d857fb603f 100644
--- a/frontend/src/services/chatService.ts
+++ b/frontend/src/services/chatService.ts
@@ -1,8 +1,11 @@
import ActionType from "#/types/ActionType";
import Session from "./session";
-export function sendChatMessage(message: string): void {
- const event = { action: ActionType.MESSAGE, args: { content: message } };
+export function sendChatMessage(message: string, images_urls: string[]): void {
+ const event = {
+ action: ActionType.MESSAGE,
+ args: { content: message, images_urls },
+ };
const eventString = JSON.stringify(event);
Session.send(eventString);
}
diff --git a/frontend/src/services/fileService.ts b/frontend/src/services/fileService.ts
index 734a933d1f..e9020373a4 100644
--- a/frontend/src/services/fileService.ts
+++ b/frontend/src/services/fileService.ts
@@ -72,10 +72,14 @@ export async function uploadFiles(files: FileList): Promise {
};
}
-export async function listFiles(path: string = "/"): Promise {
- const data = await request(
- `/api/list-files?path=${encodeURIComponent(path)}`,
- );
+export async function listFiles(
+ path: string | undefined = undefined,
+): Promise {
+ let url = "/api/list-files";
+ if (path) {
+ url = `/api/list-files?path=${encodeURIComponent(path)}`;
+ }
+ const data = await request(url);
if (!Array.isArray(data)) {
throw new Error("Invalid response format: data is not an array");
}
diff --git a/frontend/src/state/chatSlice.ts b/frontend/src/state/chatSlice.ts
index 757806e1e0..a1b01fa776 100644
--- a/frontend/src/state/chatSlice.ts
+++ b/frontend/src/state/chatSlice.ts
@@ -10,12 +10,15 @@ export const chatSlice = createSlice({
name: "chat",
initialState,
reducers: {
- addUserMessage(state, action: PayloadAction) {
+ addUserMessage(
+ state,
+ action: PayloadAction<{ content: string; imageUrls: string[] }>,
+ ) {
const message: Message = {
sender: "user",
- content: action.payload,
+ content: action.payload.content,
+ imageUrls: action.payload.imageUrls,
};
-
state.messages.push(message);
},
@@ -23,8 +26,8 @@ export const chatSlice = createSlice({
const message: Message = {
sender: "assistant",
content: action.payload,
+ imageUrls: [],
};
-
state.messages.push(message);
},
diff --git a/opendevin/controller/state/state.py b/opendevin/controller/state/state.py
index e8fca1ffaa..9200f91b72 100644
--- a/opendevin/controller/state/state.py
+++ b/opendevin/controller/state/state.py
@@ -2,6 +2,7 @@ import base64
import pickle
from dataclasses import dataclass, field
from enum import Enum
+from typing import Any
from opendevin.controller.state.task import RootTask
from opendevin.core.logger import opendevin_logger as logger
@@ -108,6 +109,9 @@ class State:
start_id: int = -1
end_id: int = -1
almost_stuck: int = 0
+ # NOTE: This will never be used by the controller, but it can be used by different
+ # evaluation tasks to store extra data needed to track the progress/state of the task.
+ extra_data: dict[str, Any] = field(default_factory=dict)
def save_to_session(self, sid: str, file_store: FileStore):
pickled = pickle.dumps(self)
@@ -169,13 +173,15 @@ class State:
# remove the restored data from the state if any
def get_current_user_intent(self):
- """Returns the latest user message that appears after a FinishAction, or the first (the task) if nothing was finished yet."""
+ """Returns the latest user message and image(if provided) that appears after a FinishAction, or the first (the task) if nothing was finished yet."""
last_user_message = None
+ last_user_message_image_urls: list[str] | None = []
for event in self.history.get_events(reverse=True):
if isinstance(event, MessageAction) and event.source == 'user':
last_user_message = event.content
+ last_user_message_image_urls = event.images_urls
elif isinstance(event, AgentFinishAction):
if last_user_message is not None:
return last_user_message
- return last_user_message
+ return last_user_message, last_user_message_image_urls
diff --git a/opendevin/core/config.py b/opendevin/core/config.py
index 86a21a9d82..46217d3784 100644
--- a/opendevin/core/config.py
+++ b/opendevin/core/config.py
@@ -111,6 +111,12 @@ class LLMConfig:
ret[k] = '******' if v else None
return ret
+ def set_missing_attributes(self):
+ """Set any missing attributes to their default values."""
+ for field_name, field_obj in self.__dataclass_fields__.items():
+ if not hasattr(self, field_name):
+ setattr(self, field_name, field_obj.default)
+
@dataclass
class AgentConfig:
@@ -139,22 +145,29 @@ class SandboxConfig(metaclass=Singleton):
"""Configuration for the sandbox.
Attributes:
- box_type: The type of sandbox to use. Options are: ssh, e2b, local.
+ api_hostname: The hostname for the EventStream Runtime API.
container_image: The container image to use for the sandbox.
user_id: The user ID for the sandbox.
timeout: The timeout for the sandbox.
enable_auto_lint: Whether to enable auto-lint.
use_host_network: Whether to use the host network.
initialize_plugins: Whether to initialize plugins.
- update_source_code: Whether to update the source code in the EventStreamRuntime.
- Used for development of EventStreamRuntime.
+ od_runtime_extra_deps: The extra dependencies to install in the runtime image (typically used for evaluation).
+ This will be rendered into the end of the Dockerfile that builds the runtime image.
+ It can contain any valid shell commands (e.g., pip install numpy).
+ The path to the interpreter is available as $OD_INTERPRETER_PATH,
+ which can be used to install dependencies for the OD-specific Python interpreter.
+ od_runtime_startup_env_vars: The environment variables to set at the launch of the runtime.
+ This is a dictionary of key-value pairs.
+ This is useful for setting environment variables that are needed by the runtime.
+ For example, for specifying the base url of website for browsergym evaluation.
+ browsergym_eval_env: The BrowserGym environment to use for evaluation.
+ Default is None for general purpose browsing. Check evaluation/miniwob and evaluation/webarena for examples.
"""
- box_type: str = 'ssh'
- container_image: str = 'ghcr.io/opendevin/sandbox' + (
- f':{os.getenv("OPEN_DEVIN_BUILD_VERSION")}'
- if os.getenv('OPEN_DEVIN_BUILD_VERSION')
- else ':main'
+ api_hostname: str = 'localhost'
+ container_image: str = (
+ 'ubuntu:22.04' # default to ubuntu:22.04 for eventstream runtime
)
user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
timeout: int = 120
@@ -163,7 +176,9 @@ class SandboxConfig(metaclass=Singleton):
)
use_host_network: bool = False
initialize_plugins: bool = True
- update_source_code: bool = False
+ od_runtime_extra_deps: str | None = None
+ od_runtime_startup_env_vars: dict[str, str] = field(default_factory=dict)
+ browsergym_eval_env: str | None = None
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
@@ -211,7 +226,6 @@ class AppConfig(metaclass=Singleton):
max_iterations: The maximum number of iterations.
max_budget_per_task: The maximum budget allowed per task, beyond which the agent will stop.
e2b_api_key: The E2B API key.
- ssh_hostname: The SSH hostname.
disable_color: Whether to disable color. For terminals that don't support color.
debug: Whether to enable debugging.
enable_cli_session: Whether to enable saving and restoring the session when run from CLI.
@@ -224,13 +238,14 @@ class AppConfig(metaclass=Singleton):
agents: dict = field(default_factory=dict)
default_agent: str = _DEFAULT_AGENT
sandbox: SandboxConfig = field(default_factory=SandboxConfig)
- runtime: str = 'server'
+ runtime: str = 'eventstream'
file_store: str = 'memory'
file_store_path: str = '/tmp/file_store'
+ # TODO: clean up workspace path after the removal of ServerRuntime
workspace_base: str = os.path.join(os.getcwd(), 'workspace')
- workspace_mount_path: str = (
+ workspace_mount_path: str | None = (
UndefinedString.UNDEFINED # this path should always be set when config is fully loaded
- )
+ ) # when set to None, do not mount the workspace
workspace_mount_path_in_sandbox: str = '/workspace'
workspace_mount_rewrite: str | None = None
cache_dir: str = '/tmp/cache'
@@ -239,11 +254,7 @@ class AppConfig(metaclass=Singleton):
max_iterations: int = _MAX_ITERATIONS
max_budget_per_task: float | None = None
e2b_api_key: str = ''
- ssh_hostname: str = 'localhost'
disable_color: bool = False
- persist_sandbox: bool = False
- ssh_port: int = 63710
- ssh_password: str | None = None
jwt_secret: str = uuid.uuid4().hex
debug: bool = False
enable_cli_session: bool = False
@@ -315,7 +326,6 @@ class AppConfig(metaclass=Singleton):
'e2b_api_key',
'github_token',
'jwt_secret',
- 'ssh_password',
]:
attr_value = '******' if attr_value else None
@@ -390,6 +400,11 @@ def load_from_env(cfg: AppConfig, env_or_toml_dict: dict | MutableMapping[str, s
elif env_var_name in env_or_toml_dict:
# convert the env var to the correct type and set it
value = env_or_toml_dict[env_var_name]
+
+ # skip empty config values (fall back to default)
+ if not value:
+ continue
+
try:
# if it's an optional type, get the non-None type
if get_origin(field_type) is UnionType:
@@ -406,11 +421,6 @@ def load_from_env(cfg: AppConfig, env_or_toml_dict: dict | MutableMapping[str, s
f'Error setting env var {env_var_name}={value}: check that the value is of the right type'
)
- if 'SANDBOX_TYPE' in env_or_toml_dict:
- logger.opendevin_logger.error(
- 'SANDBOX_TYPE is deprecated. Please use SANDBOX_BOX_TYPE instead.'
- )
- env_or_toml_dict['SANDBOX_BOX_TYPE'] = env_or_toml_dict.pop('SANDBOX_TYPE')
# Start processing from the root of the config object
set_attr_from_env(cfg)
@@ -500,8 +510,6 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
keys_to_migrate = [key for key in core_config if key.startswith('sandbox_')]
for key in keys_to_migrate:
new_key = key.replace('sandbox_', '')
- if new_key == 'type':
- new_key = 'box_type'
if new_key in sandbox_config.__annotations__:
# read the key in sandbox and remove it from core
setattr(sandbox_config, new_key, core_config.pop(key))
@@ -528,10 +536,6 @@ def finalize_config(cfg: AppConfig):
cfg.workspace_mount_path = os.path.abspath(cfg.workspace_base)
cfg.workspace_base = os.path.abspath(cfg.workspace_base)
- # In local there is no sandbox, the workspace will have the same pwd as the host
- if cfg.sandbox.box_type == 'local':
- cfg.workspace_mount_path_in_sandbox = cfg.workspace_mount_path
-
if cfg.workspace_mount_rewrite: # and not config.workspace_mount_path:
# TODO why do we need to check if workspace_mount_path is None?
base = cfg.workspace_base or os.getcwd()
diff --git a/opendevin/core/exceptions.py b/opendevin/core/exceptions.py
index fe97308396..c7e9da7614 100644
--- a/opendevin/core/exceptions.py
+++ b/opendevin/core/exceptions.py
@@ -67,3 +67,8 @@ class LLMNoActionError(Exception):
class LLMResponseError(Exception):
def __init__(self, message='Failed to retrieve action from LLM response'):
super().__init__(message)
+
+
+class UserCancelledError(Exception):
+ def __init__(self, message='User cancelled the request'):
+ super().__init__(message)
diff --git a/opendevin/core/logger.py b/opendevin/core/logger.py
index a39530befc..f765cdff99 100644
--- a/opendevin/core/logger.py
+++ b/opendevin/core/logger.py
@@ -87,7 +87,6 @@ class SensitiveDataFilter(logging.Filter):
'e2b_api_key',
'github_token',
'jwt_secret',
- 'ssh_password',
]
# add env var names
diff --git a/opendevin/core/main.py b/opendevin/core/main.py
index 9676948662..9e89d5385b 100644
--- a/opendevin/core/main.py
+++ b/opendevin/core/main.py
@@ -1,13 +1,18 @@
import asyncio
-import os
import sys
+import uuid
from typing import Callable, Type
import agenthub # noqa F401 (we import this to get the agents registered)
from opendevin.controller import AgentController
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
-from opendevin.core.config import get_llm_config_arg, load_app_config, parse_arguments
+from opendevin.core.config import (
+ AppConfig,
+ get_llm_config_arg,
+ load_app_config,
+ parse_arguments,
+)
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import AgentState
from opendevin.events import EventSource, EventStream, EventStreamSubscriber
@@ -16,11 +21,9 @@ from opendevin.events.event import Event
from opendevin.events.observation import AgentStateChangedObservation
from opendevin.llm.llm import LLM
from opendevin.runtime import get_runtime_cls
-from opendevin.runtime.sandbox import Sandbox
+from opendevin.runtime.runtime import Runtime
from opendevin.storage import get_file_store
-config = load_app_config()
-
def read_task_from_file(file_path: str) -> str:
"""Read task from the specified file."""
@@ -33,82 +36,98 @@ def read_task_from_stdin() -> str:
return sys.stdin.read()
-async def run_agent_controller(
- agent: Agent,
+async def create_runtime(
+ config: AppConfig,
+ sid: str | None = None,
+ runtime_tools_config: dict | None = None,
+) -> Runtime:
+ """Create a runtime for the agent to run on.
+
+ config: The app config.
+ sid: The session id.
+ runtime_tools_config: (will be deprecated) The runtime tools config.
+ """
+ # set up the event stream
+ file_store = get_file_store(config.file_store, config.file_store_path)
+ session_id = 'main' + ('_' + sid if sid else str(uuid.uuid4()))
+ event_stream = EventStream(session_id, file_store)
+
+ # agent class
+ agent_cls = agenthub.Agent.get_cls(config.default_agent)
+
+ # runtime and tools
+ runtime_cls = get_runtime_cls(config.runtime)
+ logger.info(f'Initializing runtime: {runtime_cls}')
+ runtime: Runtime = runtime_cls(
+ config=config,
+ event_stream=event_stream,
+ sid=session_id,
+ plugins=agent_cls.sandbox_plugins,
+ )
+ await runtime.ainit()
+
+ return runtime
+
+
+async def run_controller(
+ config: AppConfig,
task_str: str,
- max_iterations: int,
- max_budget_per_task: float | None = None,
+ runtime: Runtime | None = None,
+ agent: Agent | None = None,
exit_on_message: bool = False,
fake_user_response_fn: Callable[[State | None], str] | None = None,
- sandbox: Sandbox | None = None,
- runtime_tools_config: dict | None = None,
- sid: str | None = None,
headless_mode: bool = True,
) -> State | None:
"""Main coroutine to run the agent controller with task input flexibility.
It's only used when you launch opendevin backend directly via cmdline.
Args:
- task_str: The task to run.
+ config: The app config.
+ task_str: The task to run. It can be a string.
+ runtime: (optional) A runtime for the agent to run on.
+ agent: (optional) A agent to run.
exit_on_message: quit if agent asks for a message from user (optional)
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.
headless_mode: Whether the agent is run in headless mode.
"""
- # Logging
- logger.info(
- f'Running agent {agent.name}, model {agent.llm.config.model}, with task: "{task_str}"'
- )
+ # Create the agent
+ if agent is None:
+ agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
+ agent = agent_cls(
+ llm=LLM(config=config.get_llm_config_from_agent(config.default_agent))
+ )
- # set up the event stream
- file_store = get_file_store(config.file_store, config.file_store_path)
- cli_session = 'main' + ('_' + sid if sid else '')
- event_stream = EventStream(cli_session, file_store)
+ if runtime is None:
+ runtime = await create_runtime(config)
+ event_stream = runtime.event_stream
# restore cli session if enabled
initial_state = None
if config.enable_cli_session:
try:
logger.info('Restoring agent state from cli session')
- initial_state = State.restore_from_session(cli_session, file_store)
+ initial_state = State.restore_from_session(
+ event_stream.sid, event_stream.file_store
+ )
except Exception as e:
- print('Error restoring state', e)
+ logger.info('Error restoring state', e)
# init controller with this initial state
controller = AgentController(
agent=agent,
- max_iterations=max_iterations,
- max_budget_per_task=max_budget_per_task,
+ max_iterations=config.max_iterations,
+ max_budget_per_task=config.max_budget_per_task,
agent_to_llm_config=config.get_agent_to_llm_config_map(),
event_stream=event_stream,
initial_state=initial_state,
headless_mode=headless_mode,
)
- # runtime and tools
- runtime_cls = get_runtime_cls(config.runtime)
- runtime = runtime_cls(
- config=config,
- event_stream=event_stream,
- sandbox=sandbox,
- plugins=controller.agent.sandbox_plugins,
+ assert isinstance(task_str, str), f'task_str must be a string, got {type(task_str)}'
+ # Logging
+ logger.info(
+ f'Agent Controller Initialized: Running agent {agent.name}, model {agent.llm.config.model}, with task: "{task_str}"'
)
- await runtime.ainit()
- runtime.init_runtime_tools(
- controller.agent.runtime_tools,
- is_async=False,
- runtime_tools_config=runtime_tools_config,
- )
-
- # browser eval specific
- # TODO: move to a better place
- if runtime.browser and runtime.browser.eval_dir:
- logger.info(f'Evaluation directory: {runtime.browser.eval_dir}')
- with open(
- os.path.join(runtime.browser.eval_dir, 'goal.txt'), 'r', encoding='utf-8'
- ) as f:
- task_str = f.read()
- logger.info(f'Dynamic Eval task: {task_str}')
# start event is a MessageAction with the task, either resumed or new
if config.enable_cli_session and initial_state is not None:
@@ -148,12 +167,13 @@ async def run_agent_controller(
# save session when we're about to close
if config.enable_cli_session:
end_state = controller.get_state()
- end_state.save_to_session(cli_session, file_store)
+ end_state.save_to_session(event_stream.sid, event_stream.file_store)
# close when done
await controller.close()
- await runtime.close()
- return controller.get_state()
+ state = controller.get_state()
+
+ return state
if __name__ == '__main__':
@@ -169,30 +189,30 @@ if __name__ == '__main__':
else:
raise ValueError('No task provided. Please specify a task through -t, -f.')
+ # Load the app config
+ # this will load config from config.toml in the current directory
+ # as well as from the environment variables
+ config = load_app_config()
+
# Override default LLM configs ([llm] section in config.toml)
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
if llm_config is None:
raise ValueError(f'Invalid toml file, cannot read {args.llm_config}')
config.set_llm_config(llm_config)
- llm = LLM(config=config.get_llm_config_from_agent(args.agent_cls))
- # Create the agent
- AgentCls: Type[Agent] = Agent.get_cls(args.agent_cls)
- agent = AgentCls(llm=llm)
+ # Set default agent
+ config.default_agent = args.agent_cls
# if max budget per task is not sent on the command line, use the config value
- max_budget_per_task = (
- args.max_budget_per_task
- if args.max_budget_per_task
- else config.max_budget_per_task
- )
+ if args.max_budget_per_task is not None:
+ config.max_budget_per_task = args.max_budget_per_task
+ if args.max_iterations is not None:
+ config.max_iterations = args.max_iterations
asyncio.run(
- run_agent_controller(
- agent=agent,
+ run_controller(
+ config=config,
task_str=task_str,
- max_iterations=args.max_iterations,
- max_budget_per_task=args.max_budget_per_task,
)
)
diff --git a/opendevin/core/message.py b/opendevin/core/message.py
new file mode 100644
index 0000000000..c1e5cdf834
--- /dev/null
+++ b/opendevin/core/message.py
@@ -0,0 +1,59 @@
+from enum import Enum
+
+from pydantic import BaseModel, Field, model_serializer
+from typing_extensions import Literal
+
+
+class ContentType(Enum):
+ TEXT = 'text'
+ IMAGE_URL = 'image_url'
+
+
+class Content(BaseModel):
+ type: ContentType
+
+ @model_serializer
+ def serialize_model(self):
+ raise NotImplementedError('Subclasses should implement this method.')
+
+
+class TextContent(Content):
+ type: ContentType = ContentType.TEXT
+ text: str
+
+ @model_serializer
+ def serialize_model(self):
+ return {'type': self.type.value, 'text': self.text}
+
+
+class ImageContent(Content):
+ type: ContentType = ContentType.IMAGE_URL
+ image_urls: list[str]
+
+ @model_serializer
+ def serialize_model(self):
+ images: list[dict[str, str | dict[str, str]]] = []
+ for url in self.image_urls:
+ images.append({'type': self.type.value, 'image_url': {'url': url}})
+ return images
+
+
+class Message(BaseModel):
+ role: Literal['user', 'system', 'assistant']
+ content: list[TextContent | ImageContent] = Field(default=list)
+
+ @property
+ def contains_image(self) -> bool:
+ return any(isinstance(content, ImageContent) for content in self.content)
+
+ @model_serializer
+ def serialize_model(self) -> dict:
+ content: list[dict[str, str | dict[str, str]]] = []
+
+ for item in self.content:
+ if isinstance(item, TextContent):
+ content.append(item.model_dump())
+ elif isinstance(item, ImageContent):
+ content.extend(item.model_dump())
+
+ return {'role': self.role, 'content': content}
diff --git a/opendevin/core/schema/config.py b/opendevin/core/schema/config.py
index b10ebe7ad0..f7c4f25b55 100644
--- a/opendevin/core/schema/config.py
+++ b/opendevin/core/schema/config.py
@@ -36,11 +36,9 @@ class ConfigType(str, Enum):
MAX_ITERATIONS = 'MAX_ITERATIONS'
AGENT = 'AGENT'
E2B_API_KEY = 'E2B_API_KEY'
- SANDBOX_BOX_TYPE = 'SANDBOX_BOX_TYPE'
SANDBOX_USER_ID = 'SANDBOX_USER_ID'
SANDBOX_TIMEOUT = 'SANDBOX_TIMEOUT'
USE_HOST_NETWORK = 'USE_HOST_NETWORK'
- SSH_HOSTNAME = 'SSH_HOSTNAME'
DISABLE_COLOR = 'DISABLE_COLOR'
DEBUG = 'DEBUG'
FILE_UPLOADS_MAX_FILE_SIZE_MB = 'FILE_UPLOADS_MAX_FILE_SIZE_MB'
diff --git a/opendevin/events/action/commands.py b/opendevin/events/action/commands.py
index 0edccf8298..1ca5c6e847 100644
--- a/opendevin/events/action/commands.py
+++ b/opendevin/events/action/commands.py
@@ -10,6 +10,14 @@ from .action import Action, ActionConfirmationStatus
class CmdRunAction(Action):
command: str
thought: str = ''
+ keep_prompt: bool = True
+ # if True, the command prompt will be kept in the command output observation
+ # Example of command output:
+ # root@sandbox:~# ls
+ # file1.txt
+ # file2.txt
+ # root@sandbox:~# <-- this is the command prompt
+
action: str = ActionType.RUN
runnable: ClassVar[bool] = True
is_confirmed: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
diff --git a/opendevin/events/action/message.py b/opendevin/events/action/message.py
index 724821c243..b235dd8687 100644
--- a/opendevin/events/action/message.py
+++ b/opendevin/events/action/message.py
@@ -8,6 +8,7 @@ from .action import Action
@dataclass
class MessageAction(Action):
content: str
+ images_urls: list | None = None
wait_for_response: bool = False
action: str = ActionType.MESSAGE
@@ -18,4 +19,7 @@ class MessageAction(Action):
def __str__(self) -> str:
ret = f'**MessageAction** (source={self.source})\n'
ret += f'CONTENT: {self.content}'
+ if self.images_urls:
+ for url in self.images_urls:
+ ret += f'\nIMAGE_URL: {url}'
return ret
diff --git a/opendevin/events/event.py b/opendevin/events/event.py
index 7cf6d4accd..d4a7f915e0 100644
--- a/opendevin/events/event.py
+++ b/opendevin/events/event.py
@@ -39,3 +39,13 @@ class Event:
if hasattr(self, '_cause'):
return self._cause # type: ignore[attr-defined]
return None
+
+ @property
+ def timeout(self) -> int | None:
+ if hasattr(self, '_timeout'):
+ return self._timeout # type: ignore[attr-defined]
+ return None
+
+ @timeout.setter
+ def timeout(self, value: int | None) -> None:
+ self._timeout = value
diff --git a/opendevin/events/observation/browse.py b/opendevin/events/observation/browse.py
index eaf44ac101..901b9ffb75 100644
--- a/opendevin/events/observation/browse.py
+++ b/opendevin/events/observation/browse.py
@@ -11,7 +11,6 @@ class BrowserOutputObservation(Observation):
url: str
screenshot: str = field(repr=False) # don't show in repr
- status_code: int = 200
error: bool = False
observation: str = ObservationType.BROWSE
# do not include in the memory
@@ -34,12 +33,12 @@ class BrowserOutputObservation(Observation):
return (
'**BrowserOutputObservation**\n'
f'URL: {self.url}\n'
- f'Status code: {self.status_code}\n'
f'Error: {self.error}\n'
f'Open pages: {self.open_pages_urls}\n'
f'Active page index: {self.active_page_index}\n'
f'Last browser action: {self.last_browser_action}\n'
f'Last browser action error: {self.last_browser_action_error}\n'
f'Focused element bid: {self.focused_element_bid}\n'
+ f'axTree: {self.axtree_object}\n'
f'CONTENT: {self.content}\n'
)
diff --git a/opendevin/events/serialization/action.py b/opendevin/events/serialization/action.py
index f8051842ee..2465f0b5d6 100644
--- a/opendevin/events/serialization/action.py
+++ b/opendevin/events/serialization/action.py
@@ -54,6 +54,8 @@ def action_from_dict(action: dict) -> Action:
args = action.get('args', {})
try:
decoded_action = action_class(**args)
+ if 'timeout' in action:
+ decoded_action.timeout = action['timeout']
except TypeError:
raise LLMMalformedActionError(f'action={action} has the wrong arguments')
return decoded_action
diff --git a/opendevin/events/serialization/event.py b/opendevin/events/serialization/event.py
index d50c1f06eb..f670315fca 100644
--- a/opendevin/events/serialization/event.py
+++ b/opendevin/events/serialization/event.py
@@ -61,6 +61,8 @@ def event_to_dict(event: 'Event') -> dict:
props.pop(key, None)
if 'action' in d:
d['args'] = props
+ if event.timeout is not None:
+ d['timeout'] = event.timeout
elif 'observation' in d:
d['content'] = props.pop('content', '')
d['extras'] = props
@@ -75,6 +77,7 @@ def event_to_memory(event: 'Event', max_message_chars: int) -> dict:
d.pop('cause', None)
d.pop('timestamp', None)
d.pop('message', None)
+ d.pop('images_urls', None)
if 'extras' in d:
remove_fields(d['extras'], DELETE_FROM_MEMORY_EXTRAS)
if isinstance(event, Observation) and 'content' in d:
diff --git a/opendevin/events/stream.py b/opendevin/events/stream.py
index 5a6f90998b..054ca40af2 100644
--- a/opendevin/events/stream.py
+++ b/opendevin/events/stream.py
@@ -22,16 +22,16 @@ class EventStreamSubscriber(str, Enum):
class EventStream:
sid: str
+ file_store: FileStore
# For each subscriber ID, there is a stack of callback functions - useful
# when there are agent delegates
_subscribers: dict[str, list[Callable]]
_cur_id: int
_lock: threading.Lock
- _file_store: FileStore
def __init__(self, sid: str, file_store: FileStore):
self.sid = sid
- self._file_store = file_store
+ self.file_store = file_store
self._subscribers = {}
self._cur_id = 0
self._lock = threading.Lock()
@@ -39,7 +39,7 @@ class EventStream:
def _reinitialize_from_file_store(self) -> None:
try:
- events = self._file_store.list(f'sessions/{self.sid}/events')
+ events = self.file_store.list(f'sessions/{self.sid}/events')
except FileNotFoundError:
logger.debug(f'No events found for session {self.sid}')
self._cur_id = 0
@@ -100,7 +100,7 @@ class EventStream:
def get_event(self, id: int) -> Event:
filename = self._get_filename_for_id(id)
- content = self._file_store.read(filename)
+ content = self.file_store.read(filename)
data = json.loads(content)
return event_from_dict(data)
@@ -136,9 +136,7 @@ class EventStream:
event._source = source # type: ignore [attr-defined]
data = event_to_dict(event)
if event.id is not None:
- self._file_store.write(
- self._get_filename_for_id(event.id), json.dumps(data)
- )
+ self.file_store.write(self._get_filename_for_id(event.id), json.dumps(data))
for stack in self._subscribers.values():
callback = stack[-1]
asyncio.create_task(callback(event))
@@ -149,7 +147,7 @@ class EventStream:
yield event
def clear(self):
- self._file_store.delete(f'sessions/{self.sid}')
+ self.file_store.delete(f'sessions/{self.sid}')
self._cur_id = 0
# self._subscribers = {}
self._reinitialize_from_file_store()
diff --git a/opendevin/llm/llm.py b/opendevin/llm/llm.py
index 5d4e8b55ab..5ae4a43858 100644
--- a/opendevin/llm/llm.py
+++ b/opendevin/llm/llm.py
@@ -1,3 +1,4 @@
+import asyncio
import copy
import warnings
from functools import partial
@@ -13,6 +14,7 @@ from litellm.exceptions import (
APIConnectionError,
ContentPolicyViolationError,
InternalServerError,
+ OpenAIError,
RateLimitError,
ServiceUnavailableError,
)
@@ -24,6 +26,7 @@ from tenacity import (
wait_random_exponential,
)
+from opendevin.core.exceptions import UserCancelledError
from opendevin.core.logger import llm_prompt_logger, llm_response_logger
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.metrics import Metrics
@@ -56,6 +59,9 @@ class LLM:
self.metrics = metrics if metrics is not None else Metrics()
self.cost_metric_supported = True
+ # Set up config attributes with default values to prevent AttributeError
+ LLMConfig.set_missing_attributes(self.config)
+
# litellm actually uses base Exception here for unknown model
self.model_info = None
try:
@@ -66,11 +72,11 @@ class LLM:
self.config.model.split(':')[0]
)
# noinspection PyBroadException
- except Exception:
- logger.warning(f'Could not get model info for {config.model}')
+ except Exception as e:
+ logger.warning(f'Could not get model info for {config.model}:\n{e}')
# Set the max tokens in an LM-specific way if not set
- if config.max_input_tokens is None:
+ if self.config.max_input_tokens is None:
if (
self.model_info is not None
and 'max_input_tokens' in self.model_info
@@ -81,7 +87,7 @@ class LLM:
# Max input tokens for gpt3.5, so this is a safe fallback for any potentially viable model
self.config.max_input_tokens = 4096
- if config.max_output_tokens is None:
+ if self.config.max_output_tokens is None:
if (
self.model_info is not None
and 'max_output_tokens' in self.model_info
@@ -119,11 +125,11 @@ class LLM:
@retry(
reraise=True,
- stop=stop_after_attempt(config.num_retries),
+ stop=stop_after_attempt(self.config.num_retries),
wait=wait_random_exponential(
- multiplier=config.retry_multiplier,
- min=config.retry_min_wait,
- max=config.retry_max_wait,
+ multiplier=self.config.retry_multiplier,
+ min=self.config.retry_min_wait,
+ max=self.config.retry_max_wait,
),
retry=retry_if_exception_type(
(
@@ -147,11 +153,35 @@ class LLM:
# log the prompt
debug_message = ''
for message in messages:
- debug_message += message_separator + message['content']
+ content = message['content']
+
+ if isinstance(content, list):
+ for element in content:
+ if isinstance(element, dict):
+ if 'text' in element:
+ content_str = element['text'].strip()
+ elif (
+ 'image_url' in element and 'url' in element['image_url']
+ ):
+ content_str = element['image_url']['url']
+ else:
+ content_str = str(element)
+ else:
+ content_str = str(element)
+
+ debug_message += message_separator + content_str
+ else:
+ content_str = str(content)
+
+ debug_message += message_separator + content_str
+
llm_prompt_logger.debug(debug_message)
- # call the completion function
- resp = completion_unwrapped(*args, **kwargs)
+ # skip if messages is empty (thus debug_message is empty)
+ if debug_message:
+ resp = completion_unwrapped(*args, **kwargs)
+ else:
+ resp = {'choices': [{'message': {'content': ''}}]}
# log the response
message_back = resp['choices'][0]['message']['content']
@@ -159,10 +189,212 @@ class LLM:
# post-process to log costs
self._post_completion(resp)
+
return resp
self._completion = wrapper # type: ignore
+ # Async version
+ self._async_completion = partial(
+ self._call_acompletion,
+ model=self.config.model,
+ api_key=self.config.api_key,
+ base_url=self.config.base_url,
+ api_version=self.config.api_version,
+ custom_llm_provider=self.config.custom_llm_provider,
+ max_tokens=self.config.max_output_tokens,
+ timeout=self.config.timeout,
+ temperature=self.config.temperature,
+ top_p=self.config.top_p,
+ drop_params=True,
+ )
+
+ async_completion_unwrapped = self._async_completion
+
+ @retry(
+ reraise=True,
+ stop=stop_after_attempt(self.config.num_retries),
+ wait=wait_random_exponential(
+ multiplier=self.config.retry_multiplier,
+ min=self.config.retry_min_wait,
+ max=self.config.retry_max_wait,
+ ),
+ retry=retry_if_exception_type(
+ (
+ RateLimitError,
+ APIConnectionError,
+ ServiceUnavailableError,
+ InternalServerError,
+ ContentPolicyViolationError,
+ )
+ ),
+ after=attempt_on_error,
+ )
+ async def async_completion_wrapper(*args, **kwargs):
+ """Async wrapper for the litellm acompletion function."""
+ # some callers might just send the messages directly
+ if 'messages' in kwargs:
+ messages = kwargs['messages']
+ else:
+ messages = args[1]
+
+ # log the prompt
+ debug_message = ''
+ for message in messages:
+ content = message['content']
+
+ if isinstance(content, list):
+ for element in content:
+ if isinstance(element, dict):
+ if 'text' in element:
+ content_str = element['text']
+ elif (
+ 'image_url' in element and 'url' in element['image_url']
+ ):
+ content_str = element['image_url']['url']
+ else:
+ content_str = str(element)
+ else:
+ content_str = str(element)
+
+ debug_message += message_separator + content_str
+ else:
+ content_str = str(content)
+
+ debug_message += message_separator + content_str
+
+ llm_prompt_logger.debug(debug_message)
+
+ async def check_stopped():
+ while True:
+ if (
+ hasattr(self.config, 'on_cancel_requested_fn')
+ and self.config.on_cancel_requested_fn is not None
+ and await self.config.on_cancel_requested_fn()
+ ):
+ raise UserCancelledError('LLM request cancelled by user')
+ await asyncio.sleep(0.1)
+
+ stop_check_task = asyncio.create_task(check_stopped())
+
+ try:
+ # Directly call and await litellm_acompletion
+ resp = await async_completion_unwrapped(*args, **kwargs)
+
+ # skip if messages is empty (thus debug_message is empty)
+ if debug_message:
+ message_back = resp['choices'][0]['message']['content']
+ llm_response_logger.debug(message_back)
+ else:
+ resp = {'choices': [{'message': {'content': ''}}]}
+ self._post_completion(resp)
+
+ # We do not support streaming in this method, thus return resp
+ return resp
+
+ except UserCancelledError:
+ logger.info('LLM request cancelled by user.')
+ raise
+ except OpenAIError as e:
+ logger.error(f'OpenAIError occurred:\n{e}')
+ raise
+ except (
+ RateLimitError,
+ APIConnectionError,
+ ServiceUnavailableError,
+ InternalServerError,
+ ) as e:
+ logger.error(f'Completion Error occurred:\n{e}')
+ raise
+
+ finally:
+ await asyncio.sleep(0.1)
+ stop_check_task.cancel()
+ try:
+ await stop_check_task
+ except asyncio.CancelledError:
+ pass
+
+ @retry(
+ reraise=True,
+ stop=stop_after_attempt(self.config.num_retries),
+ wait=wait_random_exponential(
+ multiplier=self.config.retry_multiplier,
+ min=self.config.retry_min_wait,
+ max=self.config.retry_max_wait,
+ ),
+ retry=retry_if_exception_type(
+ (
+ RateLimitError,
+ APIConnectionError,
+ ServiceUnavailableError,
+ InternalServerError,
+ ContentPolicyViolationError,
+ )
+ ),
+ after=attempt_on_error,
+ )
+ async def async_acompletion_stream_wrapper(*args, **kwargs):
+ """Async wrapper for the litellm acompletion with streaming function."""
+ # some callers might just send the messages directly
+ if 'messages' in kwargs:
+ messages = kwargs['messages']
+ else:
+ messages = args[1]
+
+ # log the prompt
+ debug_message = ''
+ for message in messages:
+ debug_message += message_separator + message['content']
+ llm_prompt_logger.debug(debug_message)
+
+ try:
+ # Directly call and await litellm_acompletion
+ resp = await async_completion_unwrapped(*args, **kwargs)
+
+ # For streaming we iterate over the chunks
+ async for chunk in resp:
+ # Check for cancellation before yielding the chunk
+ if (
+ hasattr(self.config, 'on_cancel_requested_fn')
+ and self.config.on_cancel_requested_fn is not None
+ and await self.config.on_cancel_requested_fn()
+ ):
+ raise UserCancelledError(
+ 'LLM request cancelled due to CANCELLED state'
+ )
+ # with streaming, it is "delta", not "message"!
+ message_back = chunk['choices'][0]['delta']['content']
+ llm_response_logger.debug(message_back)
+ self._post_completion(chunk)
+
+ yield chunk
+
+ except UserCancelledError:
+ logger.info('LLM request cancelled by user.')
+ raise
+ except OpenAIError as e:
+ logger.error(f'OpenAIError occurred:\n{e}')
+ raise
+ except (
+ RateLimitError,
+ APIConnectionError,
+ ServiceUnavailableError,
+ InternalServerError,
+ ) as e:
+ logger.error(f'Completion Error occurred:\n{e}')
+ raise
+
+ finally:
+ if kwargs.get('stream', False):
+ await asyncio.sleep(0.1)
+
+ self._async_completion = async_completion_wrapper # type: ignore
+ self._async_streaming_completion = async_acompletion_stream_wrapper # type: ignore
+
+ async def _call_acompletion(self, *args, **kwargs):
+ return await litellm.acompletion(*args, **kwargs)
+
@property
def completion(self):
"""Decorator for the litellm completion function.
@@ -171,6 +403,25 @@ class LLM:
"""
return self._completion
+ @property
+ def async_completion(self):
+ """Decorator for the async litellm acompletion function.
+
+ Check the complete documentation at https://litellm.vercel.app/docs/providers/ollama#example-usage---streaming--acompletion
+ """
+ return self._async_completion
+
+ @property
+ def async_streaming_completion(self):
+ """Decorator for the async litellm acompletion function with streaming.
+
+ Check the complete documentation at https://litellm.vercel.app/docs/providers/ollama#example-usage---streaming--acompletion
+ """
+ return self._async_streaming_completion
+
+ def supports_vision(self):
+ return litellm.supports_vision(self.config.model)
+
def _post_completion(self, response: str) -> None:
"""Post-process the completion response."""
try:
diff --git a/opendevin/runtime/__init__.py b/opendevin/runtime/__init__.py
index f11157e8f0..26d690826f 100644
--- a/opendevin/runtime/__init__.py
+++ b/opendevin/runtime/__init__.py
@@ -1,16 +1,10 @@
-from .docker.local_box import LocalBox
-from .docker.ssh_box import DockerSSHBox
from .e2b.sandbox import E2BBox
from .sandbox import Sandbox
def get_runtime_cls(name: str):
# Local imports to avoid circular imports
- if name == 'server':
- from .server.runtime import ServerRuntime
-
- return ServerRuntime
- elif name == 'client':
+ if name == 'eventstream':
from .client.runtime import EventStreamRuntime
return EventStreamRuntime
diff --git a/opendevin/runtime/browser/browser_env.py b/opendevin/runtime/browser/browser_env.py
index ed0611f6b2..7d060580bd 100644
--- a/opendevin/runtime/browser/browser_env.py
+++ b/opendevin/runtime/browser/browser_env.py
@@ -3,8 +3,6 @@ import base64
import io
import json
import multiprocessing
-import os
-import threading
import time
import uuid
@@ -12,55 +10,32 @@ import browsergym.core # noqa F401 (we register the openended task as a gym env
import gymnasium as gym
import html2text
import numpy as np
+import tenacity
from browsergym.utils.obs import flatten_dom_to_str
from PIL import Image
from opendevin.core.exceptions import BrowserInitException
from opendevin.core.logger import opendevin_logger as logger
+BROWSER_EVAL_GET_GOAL_ACTION = 'GET_EVAL_GOAL'
+BROWSER_EVAL_GET_REWARDS_ACTION = 'GET_EVAL_REWARDS'
+
class BrowserEnv:
- def __init__(
- self,
- is_async: bool = True,
- browsergym_eval: str = '',
- browsergym_eval_save_dir: str = '',
- ):
+ def __init__(self, browsergym_eval_env: str | None = None):
self.html_text_converter = self.get_html_text_converter()
self.eval_mode = False
self.eval_dir = ''
- # EVAL only: browsergym_eval and browsergym_eval_save_dir must be provided for evaluation
- self.browsergym_eval = browsergym_eval
- self.browsergym_eval_save_dir = browsergym_eval_save_dir
- if self.browsergym_eval:
- assert (
- self.browsergym_eval_save_dir
- ), 'browsergym_eval_save_dir must be provided for evaluation.'
- self.eval_mode = True
- self.eval_dir = os.path.join(
- self.browsergym_eval_save_dir, self.browsergym_eval.split('/')[1]
- )
- os.makedirs(self.eval_dir, exist_ok=True)
+
+ # EVAL only: browsergym_eval_env must be provided for evaluation
+ self.browsergym_eval_env = browsergym_eval_env
+ self.eval_mode = bool(browsergym_eval_env)
+
# Initialize browser environment process
multiprocessing.set_start_method('spawn', force=True)
self.browser_side, self.agent_side = multiprocessing.Pipe()
- self.process = multiprocessing.Process(
- target=self.browser_process,
- )
- try:
- self.original_cwd = os.getcwd()
- except FileNotFoundError:
- logger.warning(
- 'Current working directory does not exist. Using /tmp as fallback.'
- )
- self.original_cwd = '/tmp'
- os.chdir('/tmp')
-
- if is_async:
- threading.Thread(target=self.init_browser).start()
- else:
- self.init_browser()
+ self.init_browser()
atexit.register(self.close)
def get_html_text_converter(self):
@@ -74,20 +49,15 @@ class BrowserEnv:
html_text_converter.body_width = 0
return html_text_converter
+ @tenacity.retry(
+ wait=tenacity.wait_fixed(1),
+ stop=tenacity.stop_after_attempt(5),
+ retry=tenacity.retry_if_exception_type(BrowserInitException),
+ )
def init_browser(self):
logger.info('Starting browser env...')
-
- # Ensure we're in a valid directory before starting the process
- try:
- os.chdir(self.original_cwd)
- logger.debug(f'Changed back to original directory: {self.original_cwd}')
- except Exception as e:
- logger.error(f'Failed to change to original directory: {e}')
- # If we can't change to the original directory, try to use a known valid directory
- os.chdir('/tmp')
- logger.debug('Changed to /tmp directory as fallback')
-
try:
+ self.process = multiprocessing.Process(target=self.browser_process)
self.process.start()
except Exception as e:
logger.error(f'Failed to start browser process: {e}')
@@ -99,8 +69,17 @@ class BrowserEnv:
def browser_process(self):
if self.eval_mode:
- logger.info('Creating browser env for evaluation purpose.')
- env = gym.make(self.browsergym_eval)
+ assert self.browsergym_eval_env is not None
+ logger.info('Initializing browser env for web browsing evaluation.')
+ if 'webarena' in self.browsergym_eval_env:
+ import browsergym.webarena # noqa F401 register webarena tasks as gym environments
+ elif 'miniwob' in self.browsergym_eval_env:
+ import browsergym.miniwob # noqa F401 register miniwob tasks as gym environments
+ else:
+ raise ValueError(
+ f'Unsupported browsergym eval env: {self.browsergym_eval_env}'
+ )
+ env = gym.make(self.browsergym_eval_env)
else:
env = gym.make(
'browsergym/openended',
@@ -109,20 +88,22 @@ class BrowserEnv:
headless=True,
disable_env_checker=True,
)
+
obs, info = env.reset()
- # EVAL only: save the goal into file for evaluation
+
+ # EVAL ONLY: save the goal into file for evaluation
+ self.eval_goal = None
+ self.eval_rewards: list[float] = []
if self.eval_mode:
- rewards = [] # store rewards if in eval mode
- logger.info(obs['goal'])
- with open(
- os.path.join(self.eval_dir, 'goal.txt'), 'w', encoding='utf-8'
- ) as f:
- f.write(obs['goal'])
+ logger.info(f"Browsing goal: {obs['goal']}")
+ self.eval_goal = obs['goal']
+
logger.info('Browser env started.')
while True:
try:
if self.browser_side.poll(timeout=0.01):
unique_request_id, action_data = self.browser_side.recv()
+
# shutdown the browser environment
if unique_request_id == 'SHUTDOWN':
logger.info('SHUTDOWN recv, shutting down browser env...')
@@ -131,17 +112,29 @@ class BrowserEnv:
elif unique_request_id == 'IS_ALIVE':
self.browser_side.send(('ALIVE', None))
continue
+
+ # EVAL ONLY: Get evaluation info
+ if action_data['action'] == BROWSER_EVAL_GET_GOAL_ACTION:
+ self.browser_side.send(
+ (unique_request_id, {'text_content': self.eval_goal})
+ )
+ continue
+ elif action_data['action'] == BROWSER_EVAL_GET_REWARDS_ACTION:
+ self.browser_side.send(
+ (
+ unique_request_id,
+ {'text_content': json.dumps(self.eval_rewards)},
+ )
+ )
+ continue
+
action = action_data['action']
obs, reward, terminated, truncated, info = env.step(action)
- # EVAL only: save the rewards into file for evaluation
+
+ # EVAL ONLY: Save the rewards into file for evaluation
if self.eval_mode:
- rewards.append(reward)
- with open(
- os.path.join(self.eval_dir, 'rewards.json'),
- 'w',
- encoding='utf-8',
- ) as f:
- f.write(json.dumps(rewards))
+ self.eval_rewards.append(reward)
+
# add text content of the page
html_str = flatten_dom_to_str(obs['dom_object'])
obs['text_content'] = self.html_text_converter.handle(html_str)
@@ -159,6 +152,7 @@ class BrowserEnv:
return
def step(self, action_str: str, timeout: float = 30) -> dict:
+ """Execute an action in the browser environment and return the observation."""
unique_request_id = str(uuid.uuid4())
self.agent_side.send((unique_request_id, {'action': action_str}))
start_time = time.time()
diff --git a/opendevin/runtime/browser/utils.py b/opendevin/runtime/browser/utils.py
index 8ca73eab0c..b456874da0 100644
--- a/opendevin/runtime/browser/utils.py
+++ b/opendevin/runtime/browser/utils.py
@@ -32,21 +32,23 @@ async def browse(
obs = browser.step(action_str)
return BrowserOutputObservation(
content=obs['text_content'], # text content of the page
- open_pages_urls=obs['open_pages_urls'], # list of open pages
- active_page_index=obs['active_page_index'], # index of the active page
- dom_object=obs['dom_object'], # DOM object
- axtree_object=obs['axtree_object'], # accessibility tree object
- extra_element_properties=obs[
- 'extra_element_properties'
- ], # extra element properties
- last_browser_action=obs['last_action'], # last browser env action performed
- focused_element_bid=obs['focused_element_bid'], # focused element bid
- screenshot=obs['screenshot'], # base64-encoded screenshot, png
- url=obs['url'], # URL of the page
- error=True if obs['last_action_error'] else False, # error flag
- last_browser_action_error=obs[
- 'last_action_error'
- ], # last browser env action error
+ url=obs.get('url', ''), # URL of the page
+ screenshot=obs.get('screenshot', None), # base64-encoded screenshot, png
+ open_pages_urls=obs.get('open_pages_urls', []), # list of open pages
+ active_page_index=obs.get(
+ 'active_page_index', -1
+ ), # index of the active page
+ dom_object=obs.get('dom_object', {}), # DOM object
+ axtree_object=obs.get('axtree_object', {}), # accessibility tree object
+ extra_element_properties=obs.get('extra_element_properties', {}),
+ focused_element_bid=obs.get(
+ 'focused_element_bid', None
+ ), # focused element bid
+ last_browser_action=obs.get(
+ 'last_action', ''
+ ), # last browser env action performed
+ last_browser_action_error=obs.get('last_action_error', ''),
+ error=True if obs.get('last_action_error', '') else False, # error flag
)
except Exception as e:
return BrowserOutputObservation(
diff --git a/opendevin/runtime/client/README.md b/opendevin/runtime/client/README.md
deleted file mode 100644
index 9690c65a15..0000000000
--- a/opendevin/runtime/client/README.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# OpenDevin Runtime
-
-This README provides an overview of the OpenDevin Runtime, a crucial component of the OpenDevin system. It covers two main aspects:
-
-1. How the Runtime Image is Built: Explains the layered approach to creating Docker images for both production and development environments.
-2. How the Runtime Client Works: Details the functionality and architecture of the Runtime Client, which executes actions within the Docker sandbox.
-
-The following sections dive deeper into these topics, providing a comprehensive understanding of the OpenDevin Runtime system.
-
-## Architecture Diagram
-
-```
-+-------------------+ +-------------------+
-| OpenDevin | | Docker Host |
-| Backend | | |
-| | | +-------------+ |
-| +-------------+ | | | Runtime | |
-| | EventStream | | | | Container | |
-| | Runtime |<-|-----|->| | |
-| +-------------+ | | | +-------+ | |
-| | | | |Runtime| | |
-| | | | |Client | | |
-| | | | +-------+ | |
-| | | | | | |
-| | | | +-------+ | |
-| | | | |Plugins| | |
-| | | | +-------+ | |
-| | | +-------------+ |
-+-------------------+ +-------------------+
-```
-
-This diagram illustrates the high-level architecture of the OpenDevin Runtime system:
-
-1. The OpenDevin Backend communicates with the Docker Host through the EventStreamRuntime.
-2. The Docker Host runs a Runtime Container, which includes:
- - The Runtime Client: Handles incoming actions and generates observations.
- - Plugins: Extend the functionality of the Runtime Client.
-3. The Runtime Client executes actions within the sandboxed environment of the Docker container.
-
-This architecture ensures a secure and flexible environment for executing AI-driven development tasks, allowing OpenDevin to execute a wide range of actions safely and efficiently.
-
-## How the Runtime Image is Built
-
-The OpenDevin runtime uses a layered approach for building Docker images:
-
-1. **Original Image**: `ubuntu:22.04`
- - This is the base image used for all subsequent layers.
-
-2. **Runtime Image**: `od_runtime:od_v{OPENDEVIN_VERSION}_image_ubuntu__22.04`
-
-Example image name: `od_runtime:od_v0.8.1_image_ubuntu__22.04`
- - Built from the stable release of OpenDevin.
- - This is the primary runtime image that users will interact with.
- - Created by copying all OpenDevin code into the original image and installing dependencies using Poetry.
-
-1. **Dev Runtime Image**: `od_runtime_dev:od_v{OPENDEVIN_VERSION}_image_ubuntu__22.04`
- - Built from local source code for development purposes.
-
-### Build Process
-
-#### Production Build (if environment variable `SANDBOX_UPDATE_SOURCE_CODE` is not set)
-By default, when `SANDBOX_UPDATE_SOURCE_CODE` is unset OR set to false, the build process only needs to run once:
-- The Runtime Image (`od_runtime:od_v{OPENDEVIN_VERSION}_image_ubuntu__22.04`) is created by copying OpenDevin code into the original Ubuntu image and installing all dependencies.
-- This pre-built image is then used for running the OpenDevin environment.
-
-#### Development Build (env var `SANDBOX_UPDATE_SOURCE_CODE=True`)
-When developing or modifying code that runs inside the container, you can set env var `SANDBOX_UPDATE_SOURCE_CODE=True` to enable a more dynamic build process:
-- Every time you run the code, the existing image will be updated with the latest changes.
-- The Dev Runtime Image (`od_runtime_dev:od_v{OPENDEVIN_VERSION}_image_ubuntu__22.04`) is rebuilt from the Runtime Image (`od_runtime:od_v{OPENDEVIN_VERSION}_image_ubuntu__22.04`).
-- Most dependencies are already installed in the Runtime Image, so this process mainly updates the code and any new dependencies.
-- The rebuild process typically takes around 10 seconds, allowing for quick iterations during development.
-
-This approach allows developers to easily test changes to the OpenDevin codebase, including modifications to files like client.py, without needing to rebuild the entire image from scratch each time.
-
-## How the Runtime Client Works
-
-The Runtime Client is a crucial component of the OpenDevin system, responsible for executing actions within the Docker sandbox environment and producing observations. Here's an overview of its functionality:
-
-1. **Initialization**:
- - The `EventStreamRuntime` class in `runtime.py` initializes the Docker container and sets up the runtime environment.
-
-2. **Communication**:
- - The Runtime Client uses FastAPI to create a web server inside the Docker container.
- - It listens for incoming action requests from the OpenDevin backend.
-
-3. **Action Execution**:
- - When an action is received, the Runtime Client processes it based on its type:
- - `CmdRunAction`: Executes shell commands using a pexpect-spawned bash shell.
- - `FileReadAction` and `FileWriteAction`: Perform file operations within the sandbox.
- - `IPythonRunCellAction`: Executes Python code in an IPython environment.
- - `BrowseURLAction` and `BrowseInteractiveAction`: Handle web browsing tasks using a browser environment.
-
-4. **Plugin System**:
- - The Runtime Client supports a plugin system for extending functionality.
- - Plugins like JupyterPlugin can be loaded to provide additional features.
-
-5. **Observation Generation**:
- - After executing an action, the Runtime Client generates an appropriate observation.
- - Observations include command outputs, file contents, error messages, etc.
-
-6. **Asynchronous Operation**:
- - The Runtime Client uses asyncio for avoid concurrent requests.
- - It ensures that only one action is executed at a time using a semaphore.
-
-7. **Security**:
- - All actions are executed within the confined Docker environment, providing a sandbox for safe execution.
-
-8. **Flexibility**:
- - The system supports both production (`SANDBOX_UPDATE_SOURCE_CODE=False`) and development (`SANDBOX_UPDATE_SOURCE_CODE=True`) modes.
- - In development mode, the runtime image can be updated with the latest code changes for testing and debugging.
diff --git a/opendevin/runtime/client/client.py b/opendevin/runtime/client/client.py
index 63c3526406..a1883bb829 100644
--- a/opendevin/runtime/client/client.py
+++ b/opendevin/runtime/client/client.py
@@ -3,22 +3,22 @@ This is the main file for the runtime client.
It is responsible for executing actions received from OpenDevin backend and producing observations.
NOTE: this will be executed inside the docker sandbox.
-
-If you already have pre-build docker image yet you changed the code in this file OR dependencies, you need to rebuild the docker image to update the source code.
-
-You should add SANDBOX_UPDATE_SOURCE_CODE=True to any `python XXX.py` command you run to update the source code.
"""
import argparse
import asyncio
import os
import re
+import shutil
import subprocess
from contextlib import asynccontextmanager
from pathlib import Path
import pexpect
-from fastapi import FastAPI, HTTPException, Request
+from fastapi import FastAPI, HTTPException, Request, UploadFile
+from fastapi.responses import JSONResponse
+from pathspec import PathSpec
+from pathspec.patterns import GitWildMatchPattern
from pydantic import BaseModel
from uvicorn import run
@@ -48,31 +48,50 @@ from opendevin.runtime.plugins import (
JupyterPlugin,
Plugin,
)
-from opendevin.runtime.server.files import insert_lines, read_lines
from opendevin.runtime.utils import split_bash_commands
+from opendevin.runtime.utils.files import insert_lines, read_lines
class ActionRequest(BaseModel):
action: dict
+ROOT_GID = 0
+INIT_COMMANDS = [
+ 'git config --global user.name "opendevin"',
+ 'git config --global user.email "opendevin@all-hands.dev"',
+ "alias git='git --no-pager'",
+]
+
+
class RuntimeClient:
"""RuntimeClient is running inside docker sandbox.
It is responsible for executing actions received from OpenDevin backend and producing observations.
"""
def __init__(
- self, plugins_to_load: list[Plugin], work_dir: str, username: str, user_id: int
+ self,
+ plugins_to_load: list[Plugin],
+ work_dir: str,
+ username: str,
+ user_id: int,
+ browsergym_eval_env: str | None,
) -> None:
self.plugins_to_load = plugins_to_load
self.username = username
self.user_id = user_id
self.pwd = work_dir # current PWD
+ self._initial_pwd = work_dir
self._init_user(self.username, self.user_id)
self._init_bash_shell(self.pwd, self.username)
self.lock = asyncio.Lock()
self.plugins: dict[str, Plugin] = {}
- self.browser = BrowserEnv()
+ self.browser = BrowserEnv(browsergym_eval_env)
+ self._initial_pwd = work_dir
+
+ @property
+ def initial_pwd(self):
+ return self._initial_pwd
async def ainit(self):
for plugin in self.plugins_to_load:
@@ -90,16 +109,12 @@ class RuntimeClient:
# AFTER ServerRuntime is deprecated
if 'agent_skills' in self.plugins and 'jupyter' in self.plugins:
obs = await self.run_ipython(
- IPythonRunCellAction(
- code=(
- 'import sys\n'
- 'sys.path.insert(0, "/opendevin/code/opendevin/runtime/plugins/agent_skills")\n'
- 'from agentskills import *'
- )
- )
+ IPythonRunCellAction(code='from agentskills import *')
)
logger.info(f'AgentSkills initialized: {obs}')
+ await self._init_bash_commands()
+
def _init_user(self, username: str, user_id: int) -> None:
"""Create user if not exists."""
# Skip root since it is already created
@@ -113,12 +128,19 @@ class RuntimeClient:
raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}')
logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]')
- # Add user
+ # Add user and change ownership of the initial working directory if it doesn't exist
+ command = (
+ f'useradd -rm -d /home/{username} -s /bin/bash '
+ f'-g root -G sudo -u {user_id} {username}'
+ )
+
+ if not os.path.exists(self.initial_pwd):
+ command += f' && mkdir -p {self.initial_pwd}'
+ command += f' && chown -R {username}:root {self.initial_pwd}'
+ command += f' && chmod g+s {self.initial_pwd}'
+
output = subprocess.run(
- (
- f'useradd -rm -d /home/{username} -s /bin/bash '
- f'-g root -G sudo -g root -G sudo -u {user_id} {username}'
- ),
+ command,
shell=True,
capture_output=True,
)
@@ -126,6 +148,7 @@ class RuntimeClient:
raise RuntimeError(
f'Failed to create user {username}: {output.stderr.decode()}'
)
+
logger.debug(
f'Added user {username} successfully. Output: [{output.stdout.decode()}]'
)
@@ -152,6 +175,20 @@ class RuntimeClient:
f'Bash initialized. Working directory: {work_dir}. Output: {self.shell.before}'
)
+ async def _init_bash_commands(self):
+ logger.info(f'Initializing by running {len(INIT_COMMANDS)} bash commands...')
+ for command in INIT_COMMANDS:
+ action = CmdRunAction(command=command)
+ action.timeout = 300
+ logger.debug(f'Executing init command: {command}')
+ obs: CmdOutputObservation = await self.run(action)
+ logger.debug(
+ f'Init command outputs (exit code: {obs.exit_code}): {obs.content}'
+ )
+ assert obs.exit_code == 0
+
+ logger.info('Bash init commands completed')
+
def _get_bash_prompt_and_update_pwd(self):
ps1 = self.shell.after
@@ -169,8 +206,7 @@ class RuntimeClient:
matched is not None
), f'Failed to parse bash prompt: {ps1}. This should not happen.'
username, hostname, working_dir = matched.groups()
- self._prev_pwd = self.pwd
- self.pwd = working_dir
+ self.pwd = os.path.expanduser(working_dir)
# re-assemble the prompt
prompt = f'{username}@{hostname}:{working_dir} '
@@ -183,25 +219,41 @@ class RuntimeClient:
def _execute_bash(
self,
command: str,
+ timeout: int | None,
keep_prompt: bool = True,
- timeout: int = 300,
) -> tuple[str, int]:
logger.debug(f'Executing command: {command}')
- self.shell.sendline(command)
- self.shell.expect(self.__bash_expect_regex, timeout=timeout)
+ try:
+ self.shell.sendline(command)
+ self.shell.expect(self.__bash_expect_regex, timeout=timeout)
- output = self.shell.before
- if keep_prompt:
- output += '\r\n' + self._get_bash_prompt_and_update_pwd()
- logger.debug(f'Command output: {output}')
+ output = self.shell.before
+
+ # Get exit code
+ self.shell.sendline('echo $?')
+ logger.debug(f'Executing command for exit code: {command}')
+ self.shell.expect(self.__bash_expect_regex, timeout=timeout)
+ _exit_code_output = self.shell.before
+ logger.debug(f'Exit code Output: {_exit_code_output}')
+ exit_code = int(_exit_code_output.strip().split()[0])
+
+ except pexpect.TIMEOUT as e:
+ self.shell.sendintr() # send SIGINT to the shell
+ self.shell.expect(self.__bash_expect_regex, timeout=timeout)
+ output = self.shell.before
+ output += (
+ '\r\n\r\n'
+ + f'[Command timed out after {timeout} seconds. SIGINT was sent to interrupt it.]'
+ )
+ exit_code = 130 # SIGINT
+ logger.error(f'Failed to execute command: {command}. Error: {e}')
+
+ finally:
+ bash_prompt = self._get_bash_prompt_and_update_pwd()
+ if keep_prompt:
+ output += '\r\n' + bash_prompt
+ logger.debug(f'Command output: {output}')
- # Get exit code
- self.shell.sendline('echo $?')
- logger.debug(f'Executing command for exit code: {command}')
- self.shell.expect(self.__bash_expect_regex, timeout=timeout)
- _exit_code_output = self.shell.before
- logger.debug(f'Exit code Output: {_exit_code_output}')
- exit_code = int(_exit_code_output.strip().split()[0])
return output, exit_code
async def run_action(self, action) -> Observation:
@@ -211,10 +263,17 @@ class RuntimeClient:
async def run(self, action: CmdRunAction) -> CmdOutputObservation:
try:
+ assert (
+ action.timeout is not None
+ ), f'Timeout argument is required for CmdRunAction: {action}'
commands = split_bash_commands(action.command)
all_output = ''
for command in commands:
- output, exit_code = self._execute_bash(command)
+ output, exit_code = self._execute_bash(
+ command,
+ timeout=action.timeout,
+ keep_prompt=action.keep_prompt,
+ )
if all_output:
# previous output already exists with prompt "user@hostname:working_dir #""
# we need to add the command to the previous output,
@@ -236,15 +295,19 @@ class RuntimeClient:
async def run_ipython(self, action: IPythonRunCellAction) -> Observation:
if 'jupyter' in self.plugins:
_jupyter_plugin: JupyterPlugin = self.plugins['jupyter'] # type: ignore
-
# This is used to make AgentSkills in Jupyter aware of the
# current working directory in Bash
- if not hasattr(self, '_prev_pwd') or self.pwd != self._prev_pwd:
- reset_jupyter_pwd_code = (
- f'import os; os.environ["JUPYTER_PWD"] = "{self.pwd}"\n\n'
+ if self.pwd != getattr(self, '_jupyter_pwd', None):
+ logger.debug(
+ f"{self.pwd} != {getattr(self, '_jupyter_pwd', None)} -> reset Jupyter PWD"
)
+ reset_jupyter_pwd_code = f'import os; os.environ["JUPYTER_PWD"] = os.path.abspath("{self.pwd}")'
_aux_action = IPythonRunCellAction(code=reset_jupyter_pwd_code)
- _ = await _jupyter_plugin.run(_aux_action)
+ _reset_obs = await _jupyter_plugin.run(_aux_action)
+ logger.debug(
+ f'Changed working directory in IPython to: {self.pwd}. Output: {_reset_obs}'
+ )
+ self._jupyter_pwd = self.pwd
obs: IPythonRunCellObservation = await _jupyter_plugin.run(action)
return obs
@@ -253,8 +316,9 @@ class RuntimeClient:
'JupyterRequirement not found. Unable to run IPython action.'
)
- def get_working_directory(self):
- result, exit_code = self._execute_bash('pwd', keep_prompt=False)
+ def _get_working_directory(self):
+ # NOTE: this is part of initialization, so we hard code the timeout
+ result, exit_code = self._execute_bash('pwd', timeout=60, keep_prompt=False)
if exit_code != 0:
raise RuntimeError('Failed to get working directory')
return result.strip()
@@ -268,7 +332,7 @@ class RuntimeClient:
async def read(self, action: FileReadAction) -> Observation:
# NOTE: the client code is running inside the sandbox,
# so there's no need to check permission
- working_dir = self.get_working_directory()
+ working_dir = self._get_working_directory()
filepath = self._resolve_path(action.path, working_dir)
try:
with open(filepath, 'r', encoding='utf-8') as file:
@@ -288,14 +352,21 @@ class RuntimeClient:
return FileReadObservation(path=filepath, content=code_view)
async def write(self, action: FileWriteAction) -> Observation:
- working_dir = self.get_working_directory()
+ working_dir = self._get_working_directory()
filepath = self._resolve_path(action.path, working_dir)
insert = action.content.split('\n')
try:
if not os.path.exists(os.path.dirname(filepath)):
os.makedirs(os.path.dirname(filepath))
- mode = 'w' if not os.path.exists(filepath) else 'r+'
+
+ file_exists = os.path.exists(filepath)
+ if file_exists:
+ file_stat = os.stat(filepath)
+ else:
+ file_stat = None
+
+ mode = 'w' if not file_exists else 'r+'
try:
with open(filepath, mode, encoding='utf-8') as file:
if mode != 'w':
@@ -309,6 +380,18 @@ class RuntimeClient:
file.seek(0)
file.writelines(new_file)
file.truncate()
+
+ # Handle file permissions
+ if file_exists:
+ assert file_stat is not None
+ # restore the original file permissions if the file already exists
+ os.chmod(filepath, file_stat.st_mode)
+ os.chown(filepath, file_stat.st_uid, file_stat.st_gid)
+ else:
+ # set the new file permissions if the file is new
+ os.chmod(filepath, 0o644)
+ os.chown(filepath, self.user_id, self.user_id)
+
except FileNotFoundError:
return ErrorObservation(f'File not found: {filepath}')
except IsADirectoryError:
@@ -334,22 +417,6 @@ class RuntimeClient:
self.browser.close()
-# def test_run_commond():
-# client = RuntimeClient()
-# command = CmdRunAction(command='ls -l')
-# obs = client.run_action(command)
-# print(obs)
-
-# def test_shell(message):
-# shell = pexpect.spawn('/bin/bash', encoding='utf-8')
-# shell.expect(r'[$#] ')
-# print(f'Received command: {message}')
-# shell.sendline(message)
-# shell.expect(r'[$#] ')
-# output = shell.before.strip().split('\r\n', 1)[1].strip()
-# print(f'Output: {output}')
-# shell.close()
-
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('port', type=int, help='Port to listen on')
@@ -359,6 +426,12 @@ if __name__ == '__main__':
'--username', type=str, help='User to run as', default='opendevin'
)
parser.add_argument('--user-id', type=int, help='User ID to run as', default=1000)
+ parser.add_argument(
+ '--browsergym-eval-env',
+ type=str,
+ help='BrowserGym environment used for browser evaluation',
+ default=None,
+ )
# example: python client.py 8000 --working-dir /workspace --plugins JupyterRequirement
args = parser.parse_args()
@@ -379,6 +452,7 @@ if __name__ == '__main__':
work_dir=args.working_dir,
username=args.username,
user_id=args.user_id,
+ browsergym_eval_env=args.browsergym_eval_env,
)
await client.ainit()
yield
@@ -407,10 +481,188 @@ if __name__ == '__main__':
logger.error(f'Error processing command: {str(e)}')
raise HTTPException(status_code=500, detail=str(e))
+ @app.post('/upload_file')
+ async def upload_file(
+ file: UploadFile, destination: str = '/', recursive: bool = False
+ ):
+ assert client is not None
+
+ try:
+ # Ensure the destination directory exists
+ if not os.path.isabs(destination):
+ raise HTTPException(
+ status_code=400, detail='Destination must be an absolute path'
+ )
+
+ full_dest_path = destination
+ if not os.path.exists(full_dest_path):
+ os.makedirs(full_dest_path, exist_ok=True)
+
+ if recursive:
+ # For recursive uploads, we expect a zip file
+ if not file.filename.endswith('.zip'):
+ raise HTTPException(
+ status_code=400, detail='Recursive uploads must be zip files'
+ )
+
+ zip_path = os.path.join(full_dest_path, file.filename)
+ with open(zip_path, 'wb') as buffer:
+ shutil.copyfileobj(file.file, buffer)
+
+ # Extract the zip file
+ shutil.unpack_archive(zip_path, full_dest_path)
+ os.remove(zip_path) # Remove the zip file after extraction
+
+ logger.info(
+ f'Uploaded file {file.filename} and extracted to {destination}'
+ )
+ else:
+ # For single file uploads
+ file_path = os.path.join(full_dest_path, file.filename)
+ with open(file_path, 'wb') as buffer:
+ shutil.copyfileobj(file.file, buffer)
+ logger.info(f'Uploaded file {file.filename} to {destination}')
+
+ return JSONResponse(
+ content={
+ 'filename': file.filename,
+ 'destination': destination,
+ 'recursive': recursive,
+ },
+ status_code=200,
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
@app.get('/alive')
async def alive():
return {'status': 'ok'}
+ # ================================
+ # File-specific operations for UI
+ # ================================
+
+ @app.post('/list_files')
+ async def list_files(request: Request):
+ """List files in the specified path.
+
+ This function retrieves a list of files from the agent's runtime file store,
+ excluding certain system and hidden files/directories.
+
+ To list files:
+ ```sh
+ curl http://localhost:3000/api/list-files
+ ```
+
+ Args:
+ request (Request): The incoming request object.
+ path (str, optional): The path to list files from. Defaults to '/'.
+
+ Returns:
+ list: A list of file names in the specified path.
+
+ Raises:
+ HTTPException: If there's an error listing the files.
+ """
+ assert client is not None
+
+ # get request as dict
+ request_dict = await request.json()
+ path = request_dict.get('path', None)
+
+ # Get the full path of the requested directory
+ if path is None:
+ full_path = client.initial_pwd
+ elif os.path.isabs(path):
+ full_path = path
+ else:
+ full_path = os.path.join(client.initial_pwd, path)
+
+ if not os.path.exists(full_path):
+ return JSONResponse(
+ content={'error': f'Directory {full_path} does not exist'},
+ status_code=400,
+ )
+
+ try:
+ # Check if the directory exists
+ if not os.path.exists(full_path) or not os.path.isdir(full_path):
+ return []
+
+ # Check if .gitignore exists
+ gitignore_path = os.path.join(full_path, '.gitignore')
+ if os.path.exists(gitignore_path):
+ # Use PathSpec to parse .gitignore
+ with open(gitignore_path, 'r') as f:
+ spec = PathSpec.from_lines(GitWildMatchPattern, f.readlines())
+ else:
+ # Fallback to default exclude list if .gitignore doesn't exist
+ default_exclude = [
+ '.git',
+ '.DS_Store',
+ '.svn',
+ '.hg',
+ '.idea',
+ '.vscode',
+ '.settings',
+ '.pytest_cache',
+ '__pycache__',
+ 'node_modules',
+ 'vendor',
+ 'build',
+ 'dist',
+ 'bin',
+ 'logs',
+ 'log',
+ 'tmp',
+ 'temp',
+ 'coverage',
+ 'venv',
+ 'env',
+ ]
+ spec = PathSpec.from_lines(GitWildMatchPattern, default_exclude)
+
+ entries = os.listdir(full_path)
+
+ # Filter entries using PathSpec
+ filtered_entries = [
+ os.path.join(full_path, entry)
+ for entry in entries
+ if not spec.match_file(os.path.relpath(entry, str(full_path)))
+ ]
+
+ # Separate directories and files
+ directories = []
+ files = []
+ for entry in filtered_entries:
+ # Remove leading slash and any parent directory components
+ entry_relative = entry.lstrip('/').split('/')[-1]
+
+ # Construct the full path by joining the base path with the relative entry path
+ full_entry_path = os.path.join(full_path, entry_relative)
+ if os.path.exists(full_entry_path):
+ is_dir = os.path.isdir(full_entry_path)
+ if is_dir:
+ # add trailing slash to directories
+ # required by FE to differentiate directories and files
+ entry = entry.rstrip('/') + '/'
+ directories.append(entry)
+ else:
+ files.append(entry)
+
+ # Sort directories and files separately
+ directories.sort(key=lambda s: s.lower())
+ files.sort(key=lambda s: s.lower())
+
+ # Combine sorted directories and files
+ sorted_entries = directories + files
+ return sorted_entries
+
+ except Exception as e:
+ logger.error(f'Error listing files: {e}', exc_info=True)
+ return []
+
logger.info(f'Starting action execution API on port {args.port}')
print(f'Starting action execution API on port {args.port}')
run(app, host='0.0.0.0', port=args.port)
diff --git a/opendevin/runtime/client/runtime.py b/opendevin/runtime/client/runtime.py
index 9116b9f90d..e8733a7134 100644
--- a/opendevin/runtime/client/runtime.py
+++ b/opendevin/runtime/client/runtime.py
@@ -1,6 +1,9 @@
import asyncio
+import os
+import tempfile
import uuid
from typing import Optional
+from zipfile import ZipFile
import aiohttp
import docker
@@ -8,7 +11,7 @@ import tenacity
from opendevin.core.config import AppConfig
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.events import EventSource, EventStream
+from opendevin.events import EventStream
from opendevin.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
@@ -18,7 +21,6 @@ from opendevin.events.action import (
IPythonRunCellAction,
)
from opendevin.events.action.action import Action
-from opendevin.events.event import Event
from opendevin.events.observation import (
ErrorObservation,
NullObservation,
@@ -51,7 +53,7 @@ class EventStreamRuntime(Runtime):
config, event_stream, sid, plugins
) # will initialize the event stream
self._port = find_available_tcp_port()
- self.api_url = f'http://localhost:{self._port}'
+ self.api_url = f'http://{self.config.sandbox.api_hostname}:{self._port}'
self.session: Optional[aiohttp.ClientSession] = None
self.instance_id = (
@@ -66,18 +68,20 @@ class EventStreamRuntime(Runtime):
)
self.container_name = self.container_name_prefix + self.instance_id
- self.plugins = plugins if plugins is not None else []
self.container = None
self.action_semaphore = asyncio.Semaphore(1) # Ensure one action at a time
+ logger.debug(f'EventStreamRuntime `{sid}` config:\n{self.config}')
async def ainit(self, env_vars: dict[str, str] | None = None):
+ if self.config.sandbox.od_runtime_extra_deps:
+ logger.info(
+ f'Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.od_runtime_extra_deps}'
+ )
+
self.container_image = build_runtime_image(
self.container_image,
self.docker_client,
- # NOTE: You can need set DEBUG=true to update the source code
- # inside the container. This is useful when you want to test/debug the
- # latest code in the runtime docker container.
- update_source_code=self.config.sandbox.update_source_code,
+ extra_deps=self.config.sandbox.od_runtime_extra_deps,
)
self.container = await self._init_container(
self.sandbox_workspace_dir,
@@ -88,6 +92,11 @@ class EventStreamRuntime(Runtime):
# AND the ones in env vars!
await super().ainit(env_vars)
+ logger.info(
+ f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}'
+ )
+ logger.info(f'Container initialized with env vars: {env_vars}')
+
@staticmethod
def _init_docker_client() -> docker.DockerClient:
try:
@@ -112,9 +121,11 @@ class EventStreamRuntime(Runtime):
logger.info(
f'Starting container with image: {self.container_image} and name: {self.container_name}'
)
- if plugins is None:
- plugins = []
- plugin_names = ' '.join([plugin.name for plugin in plugins])
+ plugin_arg = ''
+ if plugins is not None and len(plugins) > 0:
+ plugin_arg = (
+ f'--plugins {" ".join([plugin.name for plugin in plugins])} '
+ )
network_mode: str | None = None
port_mapping: dict[str, int] | None = None
@@ -128,12 +139,19 @@ class EventStreamRuntime(Runtime):
if mount_dir is not None:
volumes = {mount_dir: {'bind': sandbox_workspace_dir, 'mode': 'rw'}}
+ logger.info(f'Mount dir: {sandbox_workspace_dir}')
else:
logger.warn(
'Mount dir is not set, will not mount the workspace directory to the container.'
)
volumes = None
+ if self.config.sandbox.browsergym_eval_env is not None:
+ browsergym_arg = (
+ f'--browsergym-eval-env {self.config.sandbox.browsergym_eval_env}'
+ )
+ else:
+ browsergym_arg = ''
container = self.docker_client.containers.run(
self.container_image,
command=(
@@ -141,9 +159,10 @@ class EventStreamRuntime(Runtime):
'PYTHONUNBUFFERED=1 poetry run '
f'python -u -m opendevin.runtime.client.client {self._port} '
f'--working-dir {sandbox_workspace_dir} '
- f'--plugins {plugin_names} '
+ f'{plugin_arg}'
f'--username {"opendevin" if self.config.run_as_devin else "root"} '
- f'--user-id {self.config.sandbox.user_id}'
+ f'--user-id {self.config.sandbox.user_id} '
+ f'{browsergym_arg}'
),
network_mode=network_mode,
ports=port_mapping,
@@ -162,26 +181,39 @@ class EventStreamRuntime(Runtime):
raise e
async def _ensure_session(self):
+ await asyncio.sleep(1)
if self.session is None or self.session.closed:
self.session = aiohttp.ClientSession()
return self.session
@tenacity.retry(
stop=tenacity.stop_after_attempt(10),
- wait=tenacity.wait_exponential(multiplier=2, min=4, max=600),
+ wait=tenacity.wait_exponential(multiplier=2, min=4, max=60),
)
async def _wait_until_alive(self):
+ logger.info('Reconnecting session')
+ container = self.docker_client.containers.get(self.container_name)
+ # print logs
+ _logs = container.logs(tail=10).decode('utf-8').split('\n')
+ # add indent
+ _logs = '\n'.join([f' |{log}' for log in _logs])
+ logger.info(
+ '\n'
+ + '-' * 30
+ + 'Container logs (last 10 lines):'
+ + '-' * 30
+ + f'\n{_logs}'
+ + '\n'
+ + '-' * 90
+ )
async with aiohttp.ClientSession() as session:
async with session.get(f'{self.api_url}/alive') as response:
if response.status == 200:
return
else:
- logger.error(
- f'Action execution API is not alive. Response: {response}'
- )
- raise RuntimeError(
- f'Action execution API is not alive. Response: {response}'
- )
+ msg = f'Action execution API is not alive. Response: {response}'
+ logger.error(msg)
+ raise RuntimeError(msg)
@property
def sandbox_workspace_dir(self):
@@ -205,17 +237,11 @@ class EventStreamRuntime(Runtime):
if close_client:
self.docker_client.close()
- async def on_event(self, event: Event) -> None:
- logger.info(f'EventStreamRuntime: on_event triggered: {event}')
- if isinstance(event, Action):
- logger.info(event, extra={'msg_type': 'ACTION'})
- observation = await self.run_action(event)
- observation._cause = event.id # type: ignore[attr-defined]
- logger.info(observation, extra={'msg_type': 'OBSERVATION'})
- source = event.source if event.source else EventSource.AGENT
- await self.event_stream.add_event(observation, source)
+ async def run_action(self, action: Action) -> Observation:
+ # set timeout to default if not set
+ if action.timeout is None:
+ action.timeout = self.config.sandbox.timeout
- async def run_action(self, action: Action, timeout: int = 600) -> Observation:
async with self.action_semaphore:
if not action.runnable:
return NullObservation('')
@@ -227,13 +253,18 @@ class EventStreamRuntime(Runtime):
f'Action {action_type} is not supported in the current runtime.'
)
+ logger.info('Awaiting session')
session = await self._ensure_session()
await self._wait_until_alive()
+
+ assert action.timeout is not None
+
try:
+ logger.info('Executing command')
async with session.post(
f'{self.api_url}/execute_action',
json={'action': event_to_dict(action)},
- timeout=timeout,
+ timeout=action.timeout,
) as response:
if response.status == 200:
output = await response.json()
@@ -272,11 +303,83 @@ class EventStreamRuntime(Runtime):
async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
return await self.run_action(action)
- ############################################################################
- # Keep the same with other runtimes
- ############################################################################
+ # ====================================================================
+ # Implement these methods (for file operations) in the subclass
+ # ====================================================================
- def get_working_directory(self):
- raise NotImplementedError(
- 'This method is not implemented in the runtime client.'
- )
+ async def copy_to(
+ self, host_src: str, sandbox_dest: str, recursive: bool = False
+ ) -> None:
+ if not os.path.exists(host_src):
+ raise FileNotFoundError(f'Source file {host_src} does not exist')
+
+ session = await self._ensure_session()
+ await self._wait_until_alive()
+ try:
+ if recursive:
+ # For recursive copy, create a zip file
+ with tempfile.NamedTemporaryFile(
+ suffix='.zip', delete=False
+ ) as temp_zip:
+ temp_zip_path = temp_zip.name
+
+ with ZipFile(temp_zip_path, 'w') as zipf:
+ for root, _, files in os.walk(host_src):
+ for file in files:
+ file_path = os.path.join(root, file)
+ arcname = os.path.relpath(
+ file_path, os.path.dirname(host_src)
+ )
+ zipf.write(file_path, arcname)
+
+ upload_data = {'file': open(temp_zip_path, 'rb')}
+ else:
+ # For single file copy
+ upload_data = {'file': open(host_src, 'rb')}
+
+ params = {'destination': sandbox_dest, 'recursive': str(recursive).lower()}
+
+ async with session.post(
+ f'{self.api_url}/upload_file', data=upload_data, params=params
+ ) as response:
+ if response.status == 200:
+ return
+ else:
+ error_message = await response.text()
+ raise Exception(f'Copy operation failed: {error_message}')
+
+ except asyncio.TimeoutError:
+ raise TimeoutError('Copy operation timed out')
+ except Exception as e:
+ raise RuntimeError(f'Copy operation failed: {str(e)}')
+ finally:
+ if recursive:
+ os.unlink(temp_zip_path)
+ logger.info(f'Copy completed: host:{host_src} -> runtime:{sandbox_dest}')
+
+ async def list_files(self, path: str | None = None) -> list[str]:
+ """List files in the sandbox.
+
+ If path is None, list files in the sandbox's initial working directory (e.g., /workspace).
+ """
+ session = await self._ensure_session()
+ await self._wait_until_alive()
+ try:
+ data = {}
+ if path is not None:
+ data['path'] = path
+
+ async with session.post(
+ f'{self.api_url}/list_files', json=data
+ ) as response:
+ if response.status == 200:
+ response_json = await response.json()
+ assert isinstance(response_json, list)
+ return response_json
+ else:
+ error_message = await response.text()
+ raise Exception(f'List files operation failed: {error_message}')
+ except asyncio.TimeoutError:
+ raise TimeoutError('List files operation timed out')
+ except Exception as e:
+ raise RuntimeError(f'List files operation failed: {str(e)}')
diff --git a/opendevin/runtime/docker/__init__.py b/opendevin/runtime/docker/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/opendevin/runtime/docker/local_box.py b/opendevin/runtime/docker/local_box.py
deleted file mode 100644
index 5da1707939..0000000000
--- a/opendevin/runtime/docker/local_box.py
+++ /dev/null
@@ -1,122 +0,0 @@
-import atexit
-import os
-import subprocess
-import sys
-
-from opendevin.core.config import SandboxConfig
-from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.schema import CancellableStream
-from opendevin.runtime.sandbox import Sandbox
-
-# ===============================================================================
-# ** WARNING **
-#
-# This sandbox should only be used when OpenDevin is running inside a container
-#
-# Sandboxes are generally isolated so that they cannot affect the host machine.
-# This Sandbox implementation does not provide isolation, and can inadvertently
-# run dangerous commands on the host machine, potentially rendering the host
-# machine unusable.
-#
-# This sandbox is meant for use with OpenDevin Quickstart
-#
-# DO NOT USE THIS SANDBOX IN A PRODUCTION ENVIRONMENT
-# ===============================================================================
-
-
-class LocalBox(Sandbox):
- def __init__(
- self,
- config: SandboxConfig,
- workspace_base: str,
- ):
- self.config = config
- os.makedirs(workspace_base, exist_ok=True)
- self.workspace_base = workspace_base
- atexit.register(self.cleanup)
- super().__init__(config)
-
- def execute(
- self, cmd: str, stream: bool = False, timeout: int | None = None
- ) -> tuple[int, str | CancellableStream]:
- try:
- completed_process = subprocess.run(
- cmd,
- shell=True,
- text=True,
- capture_output=True,
- timeout=self.config.timeout,
- cwd=self.workspace_base,
- env=self._env,
- )
- return completed_process.returncode, completed_process.stdout.strip()
- except subprocess.TimeoutExpired:
- return -1, 'Command timed out'
-
- def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
- # mkdir -p sandbox_dest if it doesn't exist
- res = subprocess.run(
- f'mkdir -p {sandbox_dest}',
- shell=True,
- text=True,
- cwd=self.workspace_base,
- env=self._env,
- )
- if res.returncode != 0:
- raise RuntimeError(f'Failed to create directory {sandbox_dest} in sandbox')
-
- if recursive:
- res = subprocess.run(
- f'cp -r {host_src} {sandbox_dest}',
- shell=True,
- text=True,
- cwd=self.workspace_base,
- env=self._env,
- )
- if res.returncode != 0:
- raise RuntimeError(
- f'Failed to copy {host_src} to {sandbox_dest} in sandbox'
- )
- else:
- res = subprocess.run(
- f'cp {host_src} {sandbox_dest}',
- shell=True,
- text=True,
- cwd=self.workspace_base,
- env=self._env,
- )
- if res.returncode != 0:
- raise RuntimeError(
- f'Failed to copy {host_src} to {sandbox_dest} in sandbox'
- )
-
- def close(self):
- pass
-
- def cleanup(self):
- self.close()
-
- def get_working_directory(self):
- return self.workspace_base
-
-
-if __name__ == '__main__':
- local_box = LocalBox(SandboxConfig(), '/tmp/opendevin')
- sys.stdout.flush()
- try:
- while True:
- try:
- user_input = input('>>> ')
- except EOFError:
- logger.info('Exiting...')
- break
- if user_input.lower() == 'exit':
- logger.info('Exiting...')
- break
- exit_code, output = local_box.execute(user_input)
- logger.info('exit code: %d', exit_code)
- logger.info(output)
- sys.stdout.flush()
- except KeyboardInterrupt:
- logger.info('Exiting...')
- local_box.close()
diff --git a/opendevin/runtime/docker/ssh_box.py b/opendevin/runtime/docker/ssh_box.py
deleted file mode 100644
index b691749d22..0000000000
--- a/opendevin/runtime/docker/ssh_box.py
+++ /dev/null
@@ -1,683 +0,0 @@
-import atexit
-import os
-import re
-import sys
-import tarfile
-import tempfile
-import time
-import uuid
-from glob import glob
-
-import docker
-from pexpect import exceptions, pxssh
-from tenacity import retry, stop_after_attempt, wait_fixed
-
-from opendevin.core.config import SandboxConfig
-from opendevin.core.const.guide_url import TROUBLESHOOTING_URL
-from opendevin.core.logger import opendevin_logger as logger
-from opendevin.core.schema import CancellableStream
-from opendevin.runtime.plugins import AgentSkillsRequirement, JupyterRequirement
-from opendevin.runtime.plugins.requirement import PluginRequirement
-from opendevin.runtime.sandbox import Sandbox
-from opendevin.runtime.utils import find_available_tcp_port, split_bash_commands
-from opendevin.runtime.utils.image_agnostic import get_od_sandbox_image
-
-
-class SSHExecCancellableStream(CancellableStream):
- def __init__(self, ssh, cmd, timeout):
- super().__init__(self.read_output())
- self.ssh = ssh
- self.cmd = cmd
- self.timeout = timeout
-
- def close(self):
- self.closed = True
-
- def exit_code(self):
- marker = f'EXIT_CODE_MARKER_{uuid.uuid4().hex}'
- self.ssh.sendline(f'echo "{marker}$?{marker}"')
-
- if not self.ssh.prompt(timeout=self.timeout):
- return None # Timeout occurred
-
- output = self.ssh.before
- match = re.search(f'{marker}(\\d+){marker}', output)
-
- if match:
- try:
- return int(match.group(1))
- except ValueError:
- # Log the unexpected format
- logger.error(f'Unexpected exit code format: {match.group(1)}')
- return None
- else:
- # If we can't find our marked exit code, log the output and return None
- logger.error(f'Could not find exit code in output: {output}')
- return None
-
- def read_output(self):
- st = time.time()
- buf = ''
- crlf = '\r\n'
- lf = '\n'
- prompt_len = len(self.ssh.PROMPT)
- while True:
- try:
- if self.closed:
- break
- _output = self.ssh.read_nonblocking(timeout=1)
- if not _output:
- continue
-
- buf += _output
-
- if len(buf) < prompt_len:
- continue
-
- match = re.search(self.ssh.PROMPT, buf)
- if match:
- idx, _ = match.span()
- yield buf[:idx].replace(crlf, lf)
- buf = ''
- break
-
- res = buf[:-prompt_len]
- if len(res) == 0 or res.find(crlf) == -1:
- continue
- buf = buf[-prompt_len:]
- yield res.replace(crlf, lf)
- except exceptions.TIMEOUT:
- if time.time() - st < self.timeout:
- match = re.search(self.ssh.PROMPT, buf)
- if match:
- idx, _ = match.span()
- yield buf[:idx].replace(crlf, lf)
- break
- continue
- else:
- yield buf.replace(crlf, lf)
- break
- except exceptions.EOF:
- break
-
-
-class DockerSSHBox(Sandbox):
- instance_id: str
- container_image: str
- container_name_prefix = 'opendevin-sandbox-'
- container_name: str
- container: docker.models.containers.Container
- docker_client: docker.DockerClient
-
- _ssh_password: str
- _ssh_port: int
- ssh: pxssh.pxssh | None = None
-
- def __init__(
- self,
- config: SandboxConfig,
- persist_sandbox: bool,
- workspace_mount_path: str,
- sandbox_workspace_dir: str,
- cache_dir: str,
- run_as_devin: bool,
- ssh_hostname: str = 'host.docker.internal',
- ssh_password: str | None = None,
- ssh_port: int = 22,
- sid: str | None = None,
- ):
- self.config = config
- self.workspace_mount_path = workspace_mount_path
- self.sandbox_workspace_dir = sandbox_workspace_dir
- self.cache_dir = cache_dir
- self.use_host_network = config.use_host_network
- self.run_as_devin = run_as_devin
- logger.info(
- f'SSHBox is running as {"opendevin" if self.run_as_devin else "root"} user with USER_ID={config.user_id} in the sandbox'
- )
- # Initialize docker client. Throws an exception if Docker is not reachable.
- try:
- self.docker_client = docker.from_env()
- except Exception as ex:
- logger.exception(
- f'Error creating controller. Please check Docker is running and visit `{TROUBLESHOOTING_URL}` for more debugging information.',
- exc_info=False,
- )
- raise ex
-
- if persist_sandbox:
- if not self.run_as_devin:
- raise Exception(
- 'Persistent sandbox is currently designed for opendevin user only. Please set run_as_devin=True in your config.toml'
- )
- self.instance_id = 'persisted'
- else:
- self.instance_id = (sid or '') + str(uuid.uuid4())
-
- self.container_image = get_od_sandbox_image(
- config.container_image, self.docker_client
- )
- self.container_name = self.container_name_prefix + self.instance_id
-
- # set up random user password
- self.persist_sandbox = persist_sandbox
- self.ssh_hostname = ssh_hostname
- if persist_sandbox:
- if not ssh_password:
- raise ValueError('ssh_password is required for persistent sandbox')
- self._ssh_password = ssh_password
- self._ssh_port = ssh_port
- else:
- self._ssh_password = str(uuid.uuid4())
- self._ssh_port = find_available_tcp_port()
- try:
- docker.DockerClient().containers.get(self.container_name)
- self.is_initial_session = False
- except docker.errors.NotFound:
- self.is_initial_session = True
- logger.info('Detected initial session.')
- if not persist_sandbox or self.is_initial_session:
- logger.info('Creating new Docker container')
- n_tries = 5
- while n_tries > 0:
- try:
- self.restart_docker_container()
- break
- except Exception as e:
- logger.exception(
- 'Failed to start Docker container, retrying...', exc_info=False
- )
- n_tries -= 1
- if n_tries == 0:
- raise e
- time.sleep(5)
- self.setup_user()
- else:
- self.container = self.docker_client.containers.get(self.container_name)
- logger.info('Using existing Docker container')
- self.start_docker_container()
- try:
- self.start_ssh_session()
- except Exception as e:
- self.close()
- raise e
- time.sleep(1)
-
- # make sure /tmp always exists
- self.execute('mkdir -p /tmp')
- # set git config
- self.execute('git config --global user.name "OpenDevin"')
- self.execute('git config --global user.email "opendevin@all-hands.dev"')
- atexit.register(self.close)
- super().__init__(config)
-
- def setup_user(self):
- # Make users sudoers passwordless
- # TODO(sandbox): add this line in the Dockerfile for next minor version of docker image
- exit_code, logs = self.container.exec_run(
- ['/bin/bash', '-c', r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(
- f'Failed to make all users passwordless sudoers in sandbox: {logs}'
- )
-
- # Check if the opendevin user exists
- exit_code, logs = self.container.exec_run(
- ['/bin/bash', '-c', 'id -u opendevin'],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code == 0:
- # User exists, delete it
- exit_code, logs = self.container.exec_run(
- ['/bin/bash', '-c', 'userdel -r opendevin'],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(f'Failed to remove opendevin user in sandbox: {logs}')
-
- if self.run_as_devin:
- # Create the opendevin user
- exit_code, logs = self.container.exec_run(
- [
- '/bin/bash',
- '-c',
- f'useradd -rm -d /home/opendevin -s /bin/bash -g root -G sudo -u {self.config.user_id} opendevin',
- ],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(f'Failed to create opendevin user in sandbox: {logs}')
- exit_code, logs = self.container.exec_run(
- [
- '/bin/bash',
- '-c',
- f"echo 'opendevin:{self._ssh_password}' | chpasswd",
- ],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(f'Failed to set password in sandbox: {logs}')
-
- # chown the home directory
- exit_code, logs = self.container.exec_run(
- ['/bin/bash', '-c', 'chown opendevin:root /home/opendevin'],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(
- f'Failed to chown home directory for opendevin in sandbox: {logs}'
- )
- # check the miniforge3 directory exist
- exit_code, logs = self.container.exec_run(
- [
- '/bin/bash',
- '-c',
- '[ -d "/opendevin/miniforge3" ] && exit 0 || exit 1',
- ],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- if exit_code == 1:
- raise Exception(
- 'OPENDEVIN_PYTHON_INTERPRETER is not usable. Please pull the latest Docker image: docker pull ghcr.io/opendevin/sandbox:main'
- )
- else:
- raise Exception(
- f'An error occurred while checking if miniforge3 directory exists: {logs}'
- )
- exit_code, logs = self.container.exec_run(
- [
- '/bin/bash',
- '-c',
- f'chown opendevin:root {self.sandbox_workspace_dir}',
- ],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- # This is not a fatal error, just a warning
- logger.warning(
- f'Failed to chown workspace directory for opendevin in sandbox: {logs}. But this should be fine if the {self.sandbox_workspace_dir=} is mounted by the app docker container.'
- )
- else:
- exit_code, logs = self.container.exec_run(
- # change password for root
- ['/bin/bash', '-c', f"echo 'root:{self._ssh_password}' | chpasswd"],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(f'Failed to set password for root in sandbox: {logs}')
- exit_code, logs = self.container.exec_run(
- ['/bin/bash', '-c', "echo 'opendevin-sandbox' > /etc/hostname"],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
-
- # Use the retry decorator, with a maximum of 5 attempts and a fixed wait time of 5 seconds between attempts
- @retry(stop=stop_after_attempt(5), wait=wait_fixed(5))
- def __ssh_login(self):
- try:
- self.ssh = pxssh.pxssh(
- echo=False,
- timeout=self.config.timeout,
- encoding='utf-8',
- codec_errors='replace',
- )
- hostname = self.ssh_hostname
- username = 'opendevin' if self.run_as_devin else 'root'
- if self.persist_sandbox:
- password_msg = 'using your SSH password'
- else:
- password_msg = f"using the password '{self._ssh_password}'"
- logger.info('Connecting to SSH session...')
- hostname_to_log = hostname.replace('host.docker.internal', 'localhost')
- ssh_cmd = f'`ssh -v -p {self._ssh_port} {username}@{hostname_to_log}`'
- logger.info(
- f'You can debug the SSH connection by running: {ssh_cmd} {password_msg}'
- )
- self.ssh.login(hostname, username, self._ssh_password, port=self._ssh_port)
- logger.info('Connected to SSH session')
- except pxssh.ExceptionPxssh as e:
- logger.exception(
- 'Failed to login to SSH session, retrying...', exc_info=False
- )
- raise e
-
- def start_ssh_session(self):
- time.sleep(1)
- self.__ssh_login()
- assert self.ssh is not None
-
- # Fix: https://github.com/pexpect/pexpect/issues/669
- self.ssh.sendline("bind 'set enable-bracketed-paste off'")
- self.ssh.prompt()
- time.sleep(1)
-
- # cd to workspace
- self.ssh.sendline(f'cd {self.sandbox_workspace_dir}')
- self.ssh.prompt()
-
- def get_exec_cmd(self, cmd: str) -> list[str]:
- if self.run_as_devin:
- return ['su', 'opendevin', '-c', cmd]
- else:
- return ['/bin/bash', '-c', cmd]
-
- def _send_interrupt(
- self,
- cmd: str,
- prev_output: str = '',
- ignore_last_output: bool = False,
- ) -> tuple[int, str]:
- assert self.ssh is not None
- logger.exception(
- f'Command "{cmd}" timed out, killing process...', exc_info=False
- )
- # send a SIGINT to the process
- self.ssh.sendintr()
- self.ssh.prompt()
- command_output = prev_output
- if not ignore_last_output:
- command_output += '\n' + self.ssh.before
- return (
- -1,
- f'Command: "{cmd}" timed out. Sent SIGINT to the process: {command_output}',
- )
-
- def execute(
- self, cmd: str, stream: bool = False, timeout: int | None = None
- ) -> tuple[int, str | CancellableStream]:
- assert self.ssh is not None
- timeout = timeout or self.config.timeout
- commands = split_bash_commands(cmd)
- if len(commands) > 1:
- all_output = ''
- for command in commands:
- exit_code, output = self.execute(command)
- if all_output:
- all_output += '\r\n'
- all_output += str(output)
- if exit_code != 0:
- return exit_code, all_output
- return 0, all_output
-
- self.ssh.sendline(cmd)
- if stream:
- return 0, SSHExecCancellableStream(self.ssh, cmd, self.config.timeout)
- success = self.ssh.prompt(timeout=timeout)
- if not success:
- return self._send_interrupt(cmd)
- command_output = self.ssh.before
-
- # once out, make sure that we have *every* output, we while loop until we get an empty output
- while True:
- self.ssh.sendline('\n')
- timeout_not_reached = self.ssh.prompt(timeout=1)
- if not timeout_not_reached:
- logger.debug('TIMEOUT REACHED')
- break
- output = self.ssh.before
- if isinstance(output, str) and output.strip() == '':
- break
- command_output += output
- command_output = command_output.removesuffix('\r\n')
-
- # get the exit code
- self.ssh.sendline('echo $?')
- self.ssh.prompt()
- exit_code_str = self.ssh.before.strip()
- _start_time = time.time()
- while not exit_code_str:
- self.ssh.prompt(timeout=1)
- exit_code_str = self.ssh.before.strip()
- if time.time() - _start_time > timeout:
- return self._send_interrupt(
- cmd, command_output, ignore_last_output=True
- )
- cleaned_exit_code_str = exit_code_str.replace('echo $?', '').strip().split()[0]
-
- try:
- exit_code = int(cleaned_exit_code_str)
- except ValueError:
- logger.error(f'Invalid exit code: {cleaned_exit_code_str}')
- # Handle the invalid exit code appropriately (e.g., raise an exception or set a default value)
- exit_code = -1 # or some other appropriate default value
-
- return exit_code, command_output
-
- def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
- # mkdir -p sandbox_dest if it doesn't exist
- exit_code, logs = self.container.exec_run(
- ['/bin/bash', '-c', f'mkdir -p {sandbox_dest}'],
- workdir=self.sandbox_workspace_dir,
- environment=self._env,
- )
- if exit_code != 0:
- raise Exception(
- f'Failed to create directory {sandbox_dest} in sandbox: {logs}'
- )
-
- # use temp directory to store the tar file to avoid
- # conflict of filename when running multi-processes
- with tempfile.TemporaryDirectory() as tmp_dir:
- if recursive:
- assert os.path.isdir(
- host_src
- ), 'Source must be a directory when recursive is True'
- files = glob(host_src + '/**/*', recursive=True)
- srcname = os.path.basename(host_src)
- tar_filename = os.path.join(tmp_dir, srcname + '.tar')
- with tarfile.open(tar_filename, mode='w') as tar:
- for file in files:
- tar.add(
- file,
- arcname=os.path.relpath(file, os.path.dirname(host_src)),
- )
- else:
- assert os.path.isfile(
- host_src
- ), 'Source must be a file when recursive is False'
- srcname = os.path.basename(host_src)
- tar_filename = os.path.join(tmp_dir, srcname + '.tar')
- with tarfile.open(tar_filename, mode='w') as tar:
- tar.add(host_src, arcname=srcname)
-
- with open(tar_filename, 'rb') as f:
- data = f.read()
- self.container.put_archive(os.path.dirname(sandbox_dest), data)
-
- def start_docker_container(self):
- try:
- container = self.docker_client.containers.get(self.container_name)
- logger.info('Container status: %s', container.status)
- if container.status != 'running':
- container.start()
- logger.info('Container started')
- elapsed = 0
- while container.status != 'running':
- time.sleep(1)
- elapsed += 1
- if elapsed > self.config.timeout:
- break
- container = self.docker_client.containers.get(self.container_name)
- except Exception:
- logger.exception('Failed to start container')
-
- def remove_docker_container(self):
- try:
- container = self.docker_client.containers.get(self.container_name)
- container.stop()
- logger.info('Container stopped')
- container.remove()
- logger.info('Container removed')
- elapsed = 0
- while container.status != 'exited':
- time.sleep(1)
- elapsed += 1
- if elapsed > self.config.timeout:
- break
- container = self.docker_client.containers.get(self.container_name)
- except docker.errors.NotFound:
- pass
-
- def get_working_directory(self):
- exit_code, result = self.execute('pwd')
- if exit_code != 0:
- raise Exception('Failed to get working directory')
- return str(result).strip()
-
- def is_container_running(self):
- try:
- container = self.docker_client.containers.get(self.container_name)
- if container.status == 'running':
- self.container = container
- return True
- return False
- except docker.errors.NotFound:
- return False
-
- @property
- def volumes(self):
- mount_dir = self.workspace_mount_path
- return {
- mount_dir: {'bind': self.sandbox_workspace_dir, 'mode': 'rw'},
- # mount cache directory to /home/opendevin/.cache for pip cache reuse
- self.cache_dir: {
- 'bind': (
- '/home/opendevin/.cache' if self.run_as_devin else '/root/.cache'
- ),
- 'mode': 'rw',
- },
- }
-
- def restart_docker_container(self):
- try:
- self.remove_docker_container()
- except docker.errors.DockerException as ex:
- logger.exception('Failed to remove container', exc_info=False)
- raise ex
-
- try:
- network_kwargs: dict[str, str | dict[str, int]] = {}
- if self.use_host_network:
- network_kwargs['network_mode'] = 'host'
- else:
- # FIXME: This is a temporary workaround for Windows where host network mode has bugs.
- # FIXME: Docker Desktop for Mac OS has experimental support for host network mode
- network_kwargs['ports'] = {f'{self._ssh_port}/tcp': self._ssh_port}
- logger.warning(
- (
- 'Using port forwarding till the enable host network mode of Docker is out of experimental mode.'
- 'Check the 897th issue on https://github.com/OpenDevin/OpenDevin/issues/ for more information.'
- )
- )
-
- # start the container
- logger.info(f'Mounting volumes: {self.volumes}')
- self.container = self.docker_client.containers.run(
- self.container_image,
- # allow root login
- command=f"/usr/sbin/sshd -D -p {self._ssh_port} -o 'PermitRootLogin=yes'",
- **network_kwargs,
- working_dir=self.sandbox_workspace_dir,
- name=self.container_name,
- detach=True,
- volumes=self.volumes,
- )
- logger.info('Container started')
- except Exception as ex:
- logger.exception('Failed to start container: ' + str(ex), exc_info=False)
- raise ex
-
- # wait for container to be ready
- elapsed = 0
- while self.container.status != 'running':
- if self.container.status == 'exited':
- logger.info('container exited')
- logger.info('container logs:')
- logger.info(self.container.logs())
- break
- time.sleep(1)
- elapsed += 1
- self.container = self.docker_client.containers.get(self.container_name)
- logger.info(
- f'waiting for container to start: {elapsed}, container status: {self.container.status}'
- )
- if elapsed > self.config.timeout:
- break
- if self.container.status != 'running':
- raise Exception('Failed to start container')
-
- # clean up the container, cannot do it in __del__ because the python interpreter is already shutting down
- def close(self):
- containers = self.docker_client.containers.list(all=True)
- for container in containers:
- try:
- if container.name.startswith(self.container_name):
- if self.persist_sandbox:
- container.stop()
- else:
- # only remove the container we created
- # otherwise all other containers with the same prefix will be removed
- # which will mess up with parallel evaluation
- container.remove(force=True)
- except docker.errors.NotFound:
- pass
- self.docker_client.close()
-
-
-if __name__ == '__main__':
- try:
- ssh_box = DockerSSHBox(
- config=SandboxConfig(),
- run_as_devin=False,
- workspace_mount_path='/path/to/workspace',
- cache_dir='/path/to/cache',
- sandbox_workspace_dir='/sandbox',
- persist_sandbox=False,
- )
- except Exception as e:
- logger.exception('Failed to start Docker container: %s', e)
- sys.exit(1)
-
- logger.info(
- "Interactive Docker container started. Type 'exit' or use Ctrl+C to exit."
- )
-
- # Initialize required plugins
- plugins: list[PluginRequirement] = [AgentSkillsRequirement(), JupyterRequirement()]
- ssh_box.init_plugins(plugins)
- logger.info(
- '--- AgentSkills COMMAND DOCUMENTATION ---\n'
- f'{AgentSkillsRequirement().documentation}\n'
- '---'
- )
-
- sys.stdout.flush()
- try:
- while True:
- try:
- user_input = input('$ ')
- except EOFError:
- logger.info('Exiting...')
- break
- if user_input.lower() == 'exit':
- logger.info('Exiting...')
- break
- exit_code, output = ssh_box.execute(user_input)
- logger.info('exit code: %d', exit_code)
- logger.info(output)
- sys.stdout.flush()
- except KeyboardInterrupt:
- logger.info('Exiting...')
- ssh_box.close()
diff --git a/opendevin/runtime/e2b/runtime.py b/opendevin/runtime/e2b/runtime.py
index 733e9f757e..5d90408b55 100644
--- a/opendevin/runtime/e2b/runtime.py
+++ b/opendevin/runtime/e2b/runtime.py
@@ -12,14 +12,14 @@ from opendevin.events.observation import (
from opendevin.events.stream import EventStream
from opendevin.runtime import Sandbox
from opendevin.runtime.plugins import PluginRequirement
-from opendevin.runtime.server.files import insert_lines, read_lines
-from opendevin.runtime.server.runtime import ServerRuntime
+from opendevin.runtime.runtime import Runtime
+from ..utils.files import insert_lines, read_lines
from .filestore import E2BFileStore
from .sandbox import E2BSandbox
-class E2BRuntime(ServerRuntime):
+class E2BRuntime(Runtime):
def __init__(
self,
config: AppConfig,
@@ -28,7 +28,9 @@ class E2BRuntime(ServerRuntime):
plugins: list[PluginRequirement] | None = None,
sandbox: Sandbox | None = None,
):
- super().__init__(config, event_stream, sid, plugins, sandbox)
+ super().__init__(config, event_stream, sid, plugins)
+ if sandbox is None:
+ self.sandbox = E2BSandbox()
if not isinstance(self.sandbox, E2BSandbox):
raise ValueError('E2BRuntime requires an E2BSandbox')
self.file_store = E2BFileStore(self.sandbox.filesystem)
diff --git a/opendevin/runtime/plugins/agent_skills/__init__.py b/opendevin/runtime/plugins/agent_skills/__init__.py
index 8b1a3c7335..8fed5bdb05 100644
--- a/opendevin/runtime/plugins/agent_skills/__init__.py
+++ b/opendevin/runtime/plugins/agent_skills/__init__.py
@@ -11,7 +11,7 @@ class AgentSkillsRequirement(PluginRequirement):
host_src: str = os.path.dirname(
os.path.abspath(__file__)
) # The directory of this file (opendevin/runtime/plugins/agent_skills)
- sandbox_dest: str = '/opendevin/plugins/agent_skills'
+ sandbox_dest: str = '/opendevin/plugins/'
bash_script_path: str = 'setup.sh'
documentation: str = DOCUMENTATION
diff --git a/opendevin/runtime/plugins/jupyter/__init__.py b/opendevin/runtime/plugins/jupyter/__init__.py
index 891909fd00..74239efbee 100644
--- a/opendevin/runtime/plugins/jupyter/__init__.py
+++ b/opendevin/runtime/plugins/jupyter/__init__.py
@@ -18,7 +18,7 @@ class JupyterRequirement(PluginRequirement):
host_src: str = os.path.dirname(
os.path.abspath(__file__)
) # The directory of this file (opendevin/runtime/plugins/jupyter)
- sandbox_dest: str = '/opendevin/plugins/jupyter'
+ sandbox_dest: str = '/opendevin/plugins/'
bash_script_path: str = 'setup.sh'
# ================================================================
@@ -38,6 +38,7 @@ class JupyterPlugin(Plugin):
f"su - {username} -s /bin/bash << 'EOF'\n"
'cd /opendevin/code\n'
'export POETRY_VIRTUALENVS_PATH=/opendevin/poetry;\n'
+ 'export PYTHONPATH=/opendevin/code/opendevin/runtime/plugins/agent_skills:$PYTHONPATH;\n'
'/opendevin/miniforge3/bin/mamba run -n base '
'poetry run jupyter kernelgateway '
'--KernelGatewayApp.ip=0.0.0.0 '
@@ -61,7 +62,8 @@ class JupyterPlugin(Plugin):
f'Jupyter kernel gateway started at port {self.kernel_gateway_port}. Output: {output}'
)
- async def run(self, action: Action) -> IPythonRunCellObservation:
+ async def _run(self, action: Action) -> IPythonRunCellObservation:
+ """Internal method to run a code cell in the jupyter kernel."""
if not isinstance(action, IPythonRunCellAction):
raise ValueError(
f'Jupyter plugin only supports IPythonRunCellAction, but got {action}'
@@ -74,8 +76,12 @@ class JupyterPlugin(Plugin):
if not self.kernel.initialized:
await self.kernel.initialize()
- output = await self.kernel.execute(action.code)
+ output = await self.kernel.execute(action.code, timeout=action.timeout)
return IPythonRunCellObservation(
content=output,
code=action.code,
)
+
+ async def run(self, action: Action) -> IPythonRunCellObservation:
+ obs = await self._run(action)
+ return obs
diff --git a/opendevin/runtime/plugins/jupyter/execute_server.py b/opendevin/runtime/plugins/jupyter/execute_server.py
index f43bc2e73a..3582425747 100755
--- a/opendevin/runtime/plugins/jupyter/execute_server.py
+++ b/opendevin/runtime/plugins/jupyter/execute_server.py
@@ -7,6 +7,7 @@ import re
from uuid import uuid4
import tornado
+from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
from tornado.escape import json_decode, json_encode, url_escape
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.ioloop import PeriodicCallback
@@ -134,13 +135,18 @@ class JupyterKernel:
)
self.heartbeat_callback.start()
+ @retry(
+ retry=retry_if_exception_type(ConnectionRefusedError),
+ stop=stop_after_attempt(3),
+ wait=wait_fixed(2),
+ )
async def execute(self, code, timeout=120):
if not self.ws:
await self._connect()
msg_id = uuid4().hex
assert self.ws is not None
- self.ws.write_message(
+ res = await self.ws.write_message(
json_encode(
{
'header': {
@@ -164,6 +170,7 @@ class JupyterKernel:
}
)
)
+ logging.info(f'Executed code in jupyter kernel:\n{res}')
outputs = []
diff --git a/opendevin/runtime/plugins/mixin.py b/opendevin/runtime/plugins/mixin.py
index 6e3de379a4..d193d3303d 100644
--- a/opendevin/runtime/plugins/mixin.py
+++ b/opendevin/runtime/plugins/mixin.py
@@ -62,7 +62,9 @@ class PluginMixin:
# Execute the bash script
abs_path_to_bash_script = os.path.join(
- requirement.sandbox_dest, requirement.bash_script_path
+ requirement.sandbox_dest,
+ requirement.name,
+ requirement.bash_script_path,
)
logger.info(
f'Initializing plugin [{requirement.name}] by executing [{abs_path_to_bash_script}] in the sandbox.'
diff --git a/opendevin/runtime/plugins/swe_agent_commands/__init__.py b/opendevin/runtime/plugins/swe_agent_commands/__init__.py
index 66316f36b2..d904190aa2 100644
--- a/opendevin/runtime/plugins/swe_agent_commands/__init__.py
+++ b/opendevin/runtime/plugins/swe_agent_commands/__init__.py
@@ -35,7 +35,7 @@ DEFAULT_DOCUMENTATION = ''.join(
class SWEAgentCommandsRequirement(PluginRequirement):
name: str = 'swe_agent_commands'
host_src: str = os.path.dirname(os.path.abspath(__file__))
- sandbox_dest: str = '/opendevin/plugins/swe_agent_commands'
+ sandbox_dest: str = '/opendevin/plugins/'
bash_script_path: str = 'setup_default.sh'
scripts_filepaths: list[str | None] = field(
diff --git a/opendevin/runtime/runtime.py b/opendevin/runtime/runtime.py
index c4bdf423eb..688227cb1b 100644
--- a/opendevin/runtime/runtime.py
+++ b/opendevin/runtime/runtime.py
@@ -4,11 +4,10 @@ import copy
import json
import os
from abc import abstractmethod
-from typing import Any, Optional
from opendevin.core.config import AppConfig, SandboxConfig
from opendevin.core.logger import opendevin_logger as logger
-from opendevin.events import EventStream, EventStreamSubscriber
+from opendevin.events import EventSource, EventStream, EventStreamSubscriber
from opendevin.events.action import (
Action,
ActionConfirmationStatus,
@@ -29,8 +28,6 @@ from opendevin.events.observation import (
)
from opendevin.events.serialization.action import ACTION_TYPE_TO_CLASS
from opendevin.runtime.plugins import JupyterRequirement, PluginRequirement
-from opendevin.runtime.tools import RuntimeTool
-from opendevin.storage import FileStore
def _default_env_vars(sandbox_config: SandboxConfig) -> dict[str, str]:
@@ -52,7 +49,7 @@ class Runtime:
"""
sid: str
- file_store: FileStore
+ config: AppConfig
DEFAULT_ENV_VARS: dict[str, str]
def __init__(
@@ -65,11 +62,12 @@ class Runtime:
self.sid = sid
self.event_stream = event_stream
self.event_stream.subscribe(EventStreamSubscriber.RUNTIME, self.on_event)
- self.plugins = plugins if plugins is not None else []
+ self.plugins = plugins if plugins is not None and len(plugins) > 0 else []
self.config = copy.deepcopy(config)
self.DEFAULT_ENV_VARS = _default_env_vars(config.sandbox)
atexit.register(self.close_sync)
+ logger.debug(f'Runtime `{sid}` config:\n{self.config}')
async def ainit(self, env_vars: dict[str, str] | None = None) -> None:
"""
@@ -100,19 +98,6 @@ class Runtime:
else:
loop.run_until_complete(self.close())
- # ====================================================================
- # Methods we plan to deprecate when we move to new EventStreamRuntime
- # ====================================================================
-
- def init_runtime_tools(
- self,
- runtime_tools: list[RuntimeTool],
- runtime_tools_config: Optional[dict[RuntimeTool, Any]] = None,
- is_async: bool = True,
- ) -> None:
- # TODO: deprecate this method when we move to the new EventStreamRuntime
- raise NotImplementedError('This method is not implemented in the base class.')
-
# ====================================================================
async def add_env_vars(self, env_vars: dict[str, str]) -> None:
@@ -143,9 +128,14 @@ class Runtime:
async def on_event(self, event: Event) -> None:
if isinstance(event, Action):
+ # set timeout to default if not set
+ if event.timeout is None:
+ event.timeout = self.config.sandbox.timeout
+ assert event.timeout is not None
observation = await self.run_action(event)
observation._cause = event.id # type: ignore[attr-defined]
- self.event_stream.add_event(observation, event.source) # type: ignore[arg-type]
+ source = event.source if event.source else EventSource.AGENT
+ self.event_stream.add_event(observation, source) # type: ignore[arg-type]
async def run_action(self, action: Action) -> Observation:
"""Run an action and return the resulting observation.
@@ -177,7 +167,7 @@ class Runtime:
return observation
# ====================================================================
- # Implement these methods in the subclass
+ # Action execution
# ====================================================================
@abstractmethod
@@ -203,3 +193,19 @@ class Runtime:
@abstractmethod
async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
pass
+
+ # ====================================================================
+ # File operations
+ # ====================================================================
+
+ @abstractmethod
+ async def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
+ raise NotImplementedError('This method is not implemented in the base class.')
+
+ @abstractmethod
+ async def list_files(self, path: str | None = None) -> list[str]:
+ """List files in the sandbox.
+
+ If path is None, list files in the sandbox's initial working directory (e.g., /workspace).
+ """
+ raise NotImplementedError('This method is not implemented in the base class.')
diff --git a/opendevin/runtime/server/runtime.py b/opendevin/runtime/server/runtime.py
deleted file mode 100644
index 9cfa6ad446..0000000000
--- a/opendevin/runtime/server/runtime.py
+++ /dev/null
@@ -1,235 +0,0 @@
-from typing import Any, Optional
-
-from opendevin.core.config import AppConfig
-from opendevin.core.exceptions import BrowserInitException
-from opendevin.core.logger import opendevin_logger as logger
-from opendevin.events.action import (
- BrowseInteractiveAction,
- BrowseURLAction,
- CmdRunAction,
- FileReadAction,
- FileWriteAction,
- IPythonRunCellAction,
-)
-from opendevin.events.observation import (
- CmdOutputObservation,
- ErrorObservation,
- IPythonRunCellObservation,
- Observation,
-)
-from opendevin.events.stream import EventStream
-from opendevin.runtime import (
- DockerSSHBox,
- E2BBox,
- LocalBox,
- Sandbox,
-)
-from opendevin.runtime.browser.browser_env import BrowserEnv
-from opendevin.runtime.plugins import JupyterRequirement, PluginRequirement
-from opendevin.runtime.runtime import Runtime
-from opendevin.runtime.tools import RuntimeTool
-from opendevin.storage.local import LocalFileStore
-
-from ..browser import browse
-from .files import read_file, write_file
-
-
-class ServerRuntime(Runtime):
- def __init__(
- self,
- config: AppConfig,
- event_stream: EventStream,
- sid: str = 'default',
- plugins: list[PluginRequirement] | None = None,
- sandbox: Sandbox | None = None,
- ):
- super().__init__(config, event_stream, sid, plugins)
- self.file_store = LocalFileStore(config.workspace_base)
- if sandbox is None:
- self.sandbox = self.create_sandbox(sid, config.sandbox.box_type)
- self._is_external_sandbox = False
- else:
- self.sandbox = sandbox
- self._is_external_sandbox = True
- self.browser: BrowserEnv | None = None
-
- def create_sandbox(self, sid: str = 'default', box_type: str = 'ssh') -> Sandbox:
- if box_type == 'local':
- return LocalBox(
- config=self.config.sandbox, workspace_base=self.config.workspace_base
- )
- elif box_type == 'ssh':
- return DockerSSHBox(
- config=self.config.sandbox,
- persist_sandbox=self.config.persist_sandbox,
- workspace_mount_path=self.config.workspace_mount_path,
- sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
- cache_dir=self.config.cache_dir,
- run_as_devin=self.config.run_as_devin,
- ssh_hostname=self.config.ssh_hostname,
- ssh_password=self.config.ssh_password,
- ssh_port=self.config.ssh_port,
- sid=sid,
- )
- elif box_type == 'e2b':
- return E2BBox(
- config=self.config.sandbox,
- e2b_api_key=self.config.e2b_api_key,
- )
- else:
- raise ValueError(f'Invalid sandbox type: {box_type}')
-
- async def ainit(self, env_vars: dict[str, str] | None = None):
- # init sandbox plugins
- self.sandbox.init_plugins(self.plugins)
-
- # MUST call super().ainit() to initialize both default env vars
- # AND the ones in env vars!
- await super().ainit(env_vars)
-
- if any(isinstance(plugin, JupyterRequirement) for plugin in self.plugins):
- obs = await self.run_ipython(
- IPythonRunCellAction(
- code=f'import os; os.chdir("{self.config.workspace_mount_path_in_sandbox}")'
- )
- )
- logger.info(
- f'Switch to working directory {self.config.workspace_mount_path_in_sandbox} in IPython. Output: {obs.content}'
- )
-
- async def close(self):
- if hasattr(self, '_is_external_sandbox') and not self._is_external_sandbox:
- self.sandbox.close()
- if hasattr(self, 'browser') and self.browser is not None:
- self.browser.close()
-
- def init_runtime_tools(
- self,
- runtime_tools: list[RuntimeTool],
- runtime_tools_config: Optional[dict[RuntimeTool, Any]] = None,
- is_async: bool = True,
- ) -> None:
- # if browser in runtime_tools, init it
- if RuntimeTool.BROWSER in runtime_tools:
- if runtime_tools_config is None:
- runtime_tools_config = {}
- browser_env_config = runtime_tools_config.get(RuntimeTool.BROWSER, {})
- try:
- self.browser = BrowserEnv(is_async=is_async, **browser_env_config)
- except BrowserInitException:
- logger.warn(
- 'Failed to start browser environment, web browsing functionality will not work'
- )
-
- async def run(self, action: CmdRunAction) -> Observation:
- return self._run_command(action.command)
-
- async def run_ipython(self, action: IPythonRunCellAction) -> Observation:
- self._run_command(
- f"cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n{action.code}\nEOL"
- )
-
- # run the code
- obs = self._run_command('cat /tmp/opendevin_jupyter_temp.py | execute_cli')
- output = obs.content
- if 'pip install' in action.code:
- print(output)
- package_names = action.code.split(' ', 2)[-1]
- is_single_package = ' ' not in package_names
-
- if 'Successfully installed' in output:
- restart_kernel = 'import IPython\nIPython.Application.instance().kernel.do_shutdown(True)'
- if (
- 'Note: you may need to restart the kernel to use updated packages.'
- in output
- ):
- self._run_command(
- (
- "cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n"
- f'{restart_kernel}\n'
- 'EOL'
- )
- )
- obs = self._run_command(
- 'cat /tmp/opendevin_jupyter_temp.py | execute_cli'
- )
- output = '[Package installed successfully]'
- if "{'status': 'ok', 'restart': True}" != obs.content.strip():
- print(obs.content)
- output += (
- '\n[But failed to restart the kernel to load the package]'
- )
- else:
- output += (
- '\n[Kernel restarted successfully to load the package]'
- )
-
- # re-init the kernel after restart
- if action.kernel_init_code:
- self._run_command(
- (
- f"cat > /tmp/opendevin_jupyter_init.py <<'EOL'\n"
- f'{action.kernel_init_code}\n'
- 'EOL'
- ),
- )
- obs = self._run_command(
- 'cat /tmp/opendevin_jupyter_init.py | execute_cli',
- )
- elif (
- is_single_package
- and f'Requirement already satisfied: {package_names}' in output
- ):
- output = '[Package already installed]'
- return IPythonRunCellObservation(content=output, code=action.code)
-
- async def read(self, action: FileReadAction) -> Observation:
- # TODO: use self.file_store
- working_dir = self.sandbox.get_working_directory()
- return await read_file(
- action.path,
- working_dir,
- self.config.workspace_base,
- self.config.workspace_mount_path_in_sandbox,
- action.start,
- action.end,
- )
-
- async def write(self, action: FileWriteAction) -> Observation:
- # TODO: use self.file_store
- working_dir = self.sandbox.get_working_directory()
- return await write_file(
- action.path,
- working_dir,
- self.config.workspace_base,
- self.config.workspace_mount_path_in_sandbox,
- action.content,
- action.start,
- action.end,
- )
-
- async def browse(self, action: BrowseURLAction) -> Observation:
- return await browse(action, self.browser)
-
- async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
- return await browse(action, self.browser)
-
- def _run_command(self, command: str) -> Observation:
- try:
- exit_code, output = self.sandbox.execute(command)
- if 'pip install' in command:
- package_names = command.split(' ', 2)[-1]
- is_single_package = ' ' not in package_names
- print(output)
- if 'Successfully installed' in output:
- output = '[Package installed successfully]'
- elif (
- is_single_package
- and f'Requirement already satisfied: {package_names}' in output
- ):
- output = '[Package already installed]'
- return CmdOutputObservation(
- command_id=-1, content=str(output), command=command, exit_code=exit_code
- )
- except UnicodeDecodeError:
- return ErrorObservation('Command output could not be decoded as utf-8')
diff --git a/opendevin/runtime/server/files.py b/opendevin/runtime/utils/files.py
similarity index 100%
rename from opendevin/runtime/server/files.py
rename to opendevin/runtime/utils/files.py
diff --git a/opendevin/runtime/utils/runtime_build.py b/opendevin/runtime/utils/runtime_build.py
index acb4ac6d02..b04e99d15e 100644
--- a/opendevin/runtime/utils/runtime_build.py
+++ b/opendevin/runtime/utils/runtime_build.py
@@ -6,11 +6,16 @@ import tempfile
import docker
import toml
+from dirhash import dirhash
from jinja2 import Environment, FileSystemLoader
import opendevin
from opendevin.core.logger import opendevin_logger as logger
+RUNTIME_IMAGE_REPO = os.getenv(
+ 'OD_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/opendevin/od_runtime'
+)
+
def _get_package_version():
"""Read the version from pyproject.toml as the other one may be outdated."""
@@ -47,7 +52,8 @@ def _create_project_source_dist():
return tarball_path
-def _put_source_code_to_dir(temp_dir: str) -> str:
+def _put_source_code_to_dir(temp_dir: str):
+ """Put the source code of OpenDevin to the temp_dir/code."""
tarball_path = _create_project_source_dist()
filename = os.path.basename(tarball_path)
filename = filename.removesuffix('.tar.gz')
@@ -59,11 +65,20 @@ def _put_source_code_to_dir(temp_dir: str) -> str:
logger.info(
f'Source distribution moved to {os.path.join(temp_dir, "project.tar.gz")}'
)
- return filename
+
+ # unzip the tarball
+ shutil.unpack_archive(os.path.join(temp_dir, 'project.tar.gz'), temp_dir)
+ # remove the tarball
+ os.remove(os.path.join(temp_dir, 'project.tar.gz'))
+ # rename the directory to the 'code'
+ os.rename(os.path.join(temp_dir, filename), os.path.join(temp_dir, 'code'))
+ logger.info(f'Unpacked source code directory: {os.path.join(temp_dir, "code")}')
def _generate_dockerfile(
- base_image: str, source_code_dirname: str, skip_init: bool = False
+ base_image: str,
+ skip_init: bool = False,
+ extra_deps: str | None = None,
) -> str:
"""Generate the Dockerfile content for the eventstream runtime image based on user-provided base image."""
env = Environment(
@@ -74,8 +89,8 @@ def _generate_dockerfile(
template = env.get_template('Dockerfile.j2')
dockerfile_content = template.render(
base_image=base_image,
- source_code_dirname=source_code_dirname,
skip_init=skip_init,
+ extra_deps=extra_deps if extra_deps is not None else '',
)
return dockerfile_content
@@ -84,11 +99,17 @@ def prep_docker_build_folder(
dir_path: str,
base_image: str,
skip_init: bool = False,
-):
- """Prepares the docker build folder by copying the source code and generating the Dockerfile."""
- source_code_dirname = _put_source_code_to_dir(dir_path)
+ extra_deps: str | None = None,
+) -> str:
+ """Prepares the docker build folder by copying the source code and generating the Dockerfile.
+
+ Return the MD5 hash of the directory.
+ """
+ _put_source_code_to_dir(dir_path)
dockerfile_content = _generate_dockerfile(
- base_image, source_code_dirname, skip_init=skip_init
+ base_image,
+ skip_init=skip_init,
+ extra_deps=extra_deps,
)
logger.info(
(
@@ -100,78 +121,110 @@ def prep_docker_build_folder(
with open(os.path.join(dir_path, 'Dockerfile'), 'w') as file:
file.write(dockerfile_content)
+ hash = dirhash(dir_path, 'md5')
+ logger.info(
+ f'Input base image: {base_image}\n'
+ f'Skip init: {skip_init}\n'
+ f'Extra deps: {extra_deps}\n'
+ f'Hash for docker build directory [{dir_path}] (contents: {os.listdir(dir_path)}): {hash}\n'
+ )
+ return hash
+
def _build_sandbox_image(
- base_image: str,
- target_image_name: str,
+ docker_folder: str,
docker_client: docker.DockerClient,
- skip_init: bool = False,
-):
+ target_image_repo: str,
+ target_image_hash_tag: str,
+ target_image_tag: str,
+) -> str:
+ """Build the sandbox image.
+
+ The image will be tagged as both:
+ - target_image_repo:target_image_hash_tag
+ - target_image_repo:target_image_tag
+
+ Args:
+ docker_folder: str: the path to the docker build folder
+ docker_client: docker.DockerClient: the docker client
+ target_image_repo: str: the repository name for the target image
+ target_image_hash_tag: str: the *hash* tag for the target image that is calculated based
+ on the contents of the docker build folder (source code and Dockerfile)
+ e.g., ubuntu:latest -> od_runtime:1234567890abcdef
+ target_image_tag: str: the tag for the target image that's generic and based on the base image name
+ e.g., ubuntu:latest -> od_runtime:ubuntu_tag_latest
+ """
+ # 1. Always directly build and tag using the dir_hash
+ target_image_hash_name = f'{target_image_repo}:{target_image_hash_tag}'
try:
- with tempfile.TemporaryDirectory() as temp_dir:
- if skip_init:
- logger.info(
- f'Reusing existing od_sandbox image [{target_image_name}] but will update the source code in it.'
- )
- else:
- logger.info(f'Building agnostic sandbox image: {target_image_name}')
- prep_docker_build_folder(temp_dir, base_image, skip_init=skip_init)
- api_client = docker_client.api
- build_logs = api_client.build(
- path=temp_dir,
- tag=target_image_name,
- rm=True,
- decode=True,
- # do not use cache when skip_init is True (i.e., when we want to update the source code in the existing image)
- nocache=skip_init,
- )
-
- if skip_init:
- logger.info(
- f'Rebuilding existing od_sandbox image [{target_image_name}] to update the source code.'
- )
- for log in build_logs:
- if 'stream' in log:
- print(log['stream'].strip())
- elif 'error' in log:
- logger.error(log['error'].strip())
- else:
- logger.info(str(log))
-
- # check if the image is built successfully
- image = docker_client.images.get(target_image_name)
- if image is None:
- raise RuntimeError(f'Build failed: Image {target_image_name} not found')
- logger.info(f'Image {target_image_name} built successfully')
+ build_logs = docker_client.api.build(
+ path=docker_folder,
+ tag=target_image_hash_name,
+ rm=True,
+ decode=True,
+ )
except docker.errors.BuildError as e:
logger.error(f'Sandbox image build failed: {e}')
raise e
+ for log in build_logs:
+ if 'stream' in log:
+ print(log['stream'].strip())
+ elif 'error' in log:
+ logger.error(log['error'].strip())
+ else:
+ logger.info(str(log))
-def get_new_image_name(base_image: str, dev_mode: bool = False) -> str:
- if dev_mode:
- if 'od_runtime' not in base_image:
- raise ValueError(
- f'Base image {base_image} must be a valid od_runtime image to be used for dev mode.'
- )
- # remove the 'od_runtime' prefix from the base_image
- return base_image.replace('od_runtime', 'od_runtime_dev')
- elif 'od_runtime' in base_image:
- # if the base image is a valid od_runtime image, we will use it as is
- logger.info(f'Using existing od_runtime image [{base_image}]')
- return base_image
+ # 2. Re-tag the image with a more generic tag (as somewhat of "latest" tag)
+ logger.info(f'Image [{target_image_hash_name}] build finished.')
+ image = docker_client.images.get(target_image_hash_name)
+ image.tag(target_image_repo, target_image_tag)
+ logger.info(
+ f'Re-tagged image [{target_image_hash_name}] with more generic tag [{target_image_tag}]'
+ )
+
+ # check if the image is built successfully
+ image = docker_client.images.get(target_image_hash_name)
+ if image is None:
+ raise RuntimeError(
+ f'Build failed: Image [{target_image_repo}:{target_image_hash_tag}] not found'
+ )
+ logger.info(
+ f'Image [{target_image_repo}:{target_image_hash_tag}] (hash: [{target_image_tag}]) built successfully'
+ )
+ return target_image_hash_name
+
+
+def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]:
+ if RUNTIME_IMAGE_REPO in base_image:
+ logger.info(
+ f'The provided image [{base_image}] is a already a valid od_runtime image.\n'
+ f'Will try to reuse it as is.'
+ )
+ if ':' not in base_image:
+ base_image = base_image + ':latest'
+ repo, tag = base_image.split(':')
+ return repo, tag
else:
- prefix = 'od_runtime'
if ':' not in base_image:
base_image = base_image + ':latest'
[repo, tag] = base_image.split(':')
repo = repo.replace('/', '___')
-
od_version = _get_package_version()
- return f'{prefix}:od_v{od_version}_image_{repo}_tag_{tag}'
+ return RUNTIME_IMAGE_REPO, f'od_v{od_version}_image_{repo}_tag_{tag}'
def _check_image_exists(image_name: str, docker_client: docker.DockerClient) -> bool:
+ """Check if the image exists in the registry (try to pull it first) AND in the local store.
+
+ image_name is f'{repo}:{tag}'
+ """
+ # Try to pull the new image from the registry
+ try:
+ docker_client.images.pull(image_name)
+ except Exception:
+ logger.info(f'Cannot pull image {image_name} directly')
+
images = docker_client.images.list()
if images:
for image in images:
@@ -183,91 +236,123 @@ def _check_image_exists(image_name: str, docker_client: docker.DockerClient) ->
def build_runtime_image(
base_image: str,
docker_client: docker.DockerClient,
- update_source_code: bool = False,
- save_to_local_store: bool = False, # New parameter to control saving to local store
+ extra_deps: str | None = None,
+ docker_build_folder: str | None = None,
+ dry_run: bool = False,
) -> str:
"""Build the runtime image for the OpenDevin runtime.
- This is only used for **eventstream runtime**.
+ See https://docs.all-hands.dev/modules/usage/runtime for more details.
"""
- new_image_name = get_new_image_name(base_image)
- if base_image == new_image_name:
+ runtime_image_repo, runtime_image_tag = get_runtime_image_repo_and_tag(base_image)
+
+ # Calculate the hash for the docker build folder (source code and Dockerfile)
+ with tempfile.TemporaryDirectory() as temp_dir:
+ from_scratch_hash = prep_docker_build_folder(
+ temp_dir,
+ base_image=base_image,
+ skip_init=False,
+ extra_deps=extra_deps,
+ )
+
+ # hash image name, if the hash matches, it means the image is already
+ # built from scratch with the *exact SAME source code* on the exact Dockerfile
+ hash_runtime_image_name = f'{runtime_image_repo}:{from_scratch_hash}'
+
+ # non-hash generic image name, it could contains *similar* dependencies
+ # but *might* not exactly match the state of the source code.
+ # It resembles the "latest" tag in the docker image naming convention for
+ # a particular {repo}:{tag} pair (e.g., ubuntu:latest -> od_runtime:ubuntu_tag_latest)
+ # we will build from IT to save time if the `from_scratch_hash` is not found
+ generic_runtime_image_name = f'{runtime_image_repo}:{runtime_image_tag}'
+
+ # 1. If the image exists with the same hash, we will reuse it as is
+ if _check_image_exists(hash_runtime_image_name, docker_client):
logger.info(
- f'Using existing od_runtime image [{base_image}]. Will NOT build a new image.'
+ f'Image [{hash_runtime_image_name}] exists with matched hash for Docker build folder.\n'
+ 'Will reuse it as is.'
)
- else:
- logger.info(f'New image name: {new_image_name}')
+ return hash_runtime_image_name
- # Ensure new_image_name contains a colon
- if ':' not in new_image_name:
- raise ValueError(
- f'Invalid image name: {new_image_name}. Expected format "repository:tag".'
+ # 2. If the exact hash is not found, we will FIRST try to re-build it
+ # by leveraging the non-hash `generic_runtime_image_name` to save some time
+ # from re-building the dependencies (e.g., poetry install, apt install)
+ elif _check_image_exists(generic_runtime_image_name, docker_client):
+ logger.info(
+ f'Cannot find matched hash for image [{hash_runtime_image_name}]\n'
+ f'Will try to re-build it from latest [{generic_runtime_image_name}] image to potentially save '
+ f'time for dependencies installation.\n'
)
- # Try to pull the new image from the registry
- try:
- docker_client.images.pull(new_image_name)
- except Exception:
- logger.info(f'Cannot pull image {new_image_name} directly')
+ cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp()
+ _skip_init_hash = prep_docker_build_folder(
+ cur_docker_build_folder,
+ # we want to use the existing generic image as base
+ # so that we can leverage existing dependencies already installed in the image
+ base_image=generic_runtime_image_name,
+ skip_init=True, # skip init since we are re-using the existing image
+ extra_deps=extra_deps,
+ )
+ assert (
+ _skip_init_hash != from_scratch_hash
+ ), f'The skip_init hash [{_skip_init_hash}] should not match the existing hash [{from_scratch_hash}]'
- # Detect if the sandbox image is built
- image_exists = _check_image_exists(new_image_name, docker_client)
- if image_exists:
- logger.info(f'Image {new_image_name} exists')
+ if not dry_run:
+ _build_sandbox_image(
+ docker_folder=cur_docker_build_folder,
+ docker_client=docker_client,
+ target_image_repo=runtime_image_repo,
+ # NOTE: WE ALWAYS use the "from_scratch_hash" tag for the target image
+ # otherwise, even if the source code is exactly the same, the image *might* be re-built
+ # because the same source code will generate different hash when skip_init=True/False
+ # since the Dockerfile is slightly different
+ target_image_hash_tag=from_scratch_hash,
+ target_image_tag=runtime_image_tag,
+ )
+ else:
+ logger.info(
+ f'Dry run: Skipping image build for [{generic_runtime_image_name}]'
+ )
+ if docker_build_folder is None:
+ shutil.rmtree(cur_docker_build_folder)
+
+ # 3. If the image is not found AND we cannot re-use the non-hash latest relavant image,
+ # we will build it completely from scratch
else:
- logger.info(f'Image {new_image_name} does not exist')
+ cur_docker_build_folder = docker_build_folder or tempfile.mkdtemp()
+ _new_from_scratch_hash = prep_docker_build_folder(
+ cur_docker_build_folder,
+ base_image,
+ skip_init=False,
+ extra_deps=extra_deps,
+ )
+ assert (
+ _new_from_scratch_hash == from_scratch_hash
+ ), f'The new from scratch hash [{_new_from_scratch_hash}] does not match the existing hash [{from_scratch_hash}]'
- skip_init = False
- if image_exists and not update_source_code:
- # If (1) Image exists & we are not updating the source code, we can reuse the existing production image
- logger.info('No image build done (not updating source code)')
- return new_image_name
- elif image_exists and update_source_code:
- # If (2) Image exists & we plan to update the source code (in dev mode), we need to rebuild the image
- # and give it a special name
- # e.g., od_runtime:ubuntu_tag_latest -> od_runtime_dev:ubuntu_tag_latest
- logger.info('Image exists, but updating source code requested')
- base_image = new_image_name
- new_image_name = get_new_image_name(base_image, dev_mode=True)
+ if not dry_run:
+ _build_sandbox_image(
+ docker_folder=cur_docker_build_folder,
+ docker_client=docker_client,
+ target_image_repo=runtime_image_repo,
+ # NOTE: WE ALWAYS use the "from_scratch_hash" tag for the target image
+ target_image_hash_tag=from_scratch_hash,
+ target_image_tag=runtime_image_tag,
+ )
+ else:
+ logger.info(
+ f'Dry run: Skipping image build for [{generic_runtime_image_name}]'
+ )
- skip_init = True # since we only need to update the source code
- else:
- # If (3) Image does not exist, we need to build it from scratch
- # e.g., ubuntu:latest -> od_runtime:ubuntu_tag_latest
- # This snippet would allow to load from archive:
- # tar_path = f'{new_image_name.replace(":", "_")}.tar'
- # if os.path.exists(tar_path):
- # logger.info(f'Loading image from {tar_path}')
- # load_command = ['docker', 'load', '-i', tar_path]
- # subprocess.run(load_command, check=True)
- # logger.info(f'Image {new_image_name} loaded from {tar_path}')
- # return new_image_name
- skip_init = False
+ if docker_build_folder is None:
+ shutil.rmtree(cur_docker_build_folder)
- if not skip_init:
- logger.info(f'Building image [{new_image_name}] from scratch')
-
- _build_sandbox_image(base_image, new_image_name, docker_client, skip_init=skip_init)
-
- # Only for development: allow to save image as archive:
- if not image_exists and save_to_local_store:
- tar_path = f'{new_image_name.replace(":", "_")}.tar'
- save_command = ['docker', 'save', '-o', tar_path, new_image_name]
- subprocess.run(save_command, check=True)
- logger.info(f'Image saved to {tar_path}')
-
- load_command = ['docker', 'load', '-i', tar_path]
- subprocess.run(load_command, check=True)
- logger.info(f'Image {new_image_name} loaded back into Docker from {tar_path}')
-
- return new_image_name
+ return f'{runtime_image_repo}:{from_scratch_hash}'
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--base_image', type=str, default='ubuntu:22.04')
- parser.add_argument('--update_source_code', action='store_true')
- parser.add_argument('--save_to_local_store', action='store_true')
parser.add_argument('--build_folder', type=str, default=None)
args = parser.parse_args()
@@ -279,29 +364,40 @@ if __name__ == '__main__':
logger.info(
f'Will prepare a build folder by copying the source code and generating the Dockerfile: {build_folder}'
)
- new_image_path = get_new_image_name(args.base_image)
- prep_docker_build_folder(
- build_folder, args.base_image, skip_init=args.update_source_code
+ runtime_image_repo, runtime_image_tag = get_runtime_image_repo_and_tag(
+ args.base_image
)
- new_image_name, new_image_tag = new_image_path.split(':')
+ with tempfile.TemporaryDirectory() as temp_dir:
+ runtime_image_hash_name = build_runtime_image(
+ args.base_image,
+ docker_client=docker.from_env(),
+ docker_build_folder=temp_dir,
+ dry_run=True,
+ )
+ _runtime_image_repo, runtime_image_hash_tag = runtime_image_hash_name.split(
+ ':'
+ )
+ # Move contents of temp_dir to build_folder
+ shutil.copytree(temp_dir, build_folder, dirs_exist_ok=True)
+ logger.info(
+ f'Build folder [{build_folder}] is ready: {os.listdir(build_folder)}'
+ )
+
with open(os.path.join(build_folder, 'config.sh'), 'a') as file:
file.write(
(
- f'DOCKER_IMAGE={new_image_name}\n'
- f'DOCKER_IMAGE_TAG={new_image_tag}\n'
+ f'\n'
+ f'DOCKER_IMAGE={runtime_image_repo}\n'
+ f'DOCKER_IMAGE_TAG={runtime_image_tag}\n'
+ f'DOCKER_IMAGE_HASH_TAG={runtime_image_hash_tag}\n'
)
)
logger.info(
- f'`config.sh` is updated with the new image name [{new_image_name}] and tag [{new_image_tag}]'
+ f'`config.sh` is updated with the new image name [{runtime_image_repo}] and tag [{runtime_image_tag}, {runtime_image_hash_tag}]'
)
logger.info(f'Dockerfile and source distribution are ready in {build_folder}')
else:
logger.info('Building image in a temporary folder')
client = docker.from_env()
- image_name = build_runtime_image(
- args.base_image,
- client,
- update_source_code=args.update_source_code,
- save_to_local_store=args.save_to_local_store,
- )
+ image_name = build_runtime_image(args.base_image, client)
print(f'\nBUILT Image: {image_name}\n')
diff --git a/opendevin/runtime/utils/runtime_templates/Dockerfile.j2 b/opendevin/runtime/utils/runtime_templates/Dockerfile.j2
index 9855dfdcce..6121e0e356 100644
--- a/opendevin/runtime/utils/runtime_templates/Dockerfile.j2
+++ b/opendevin/runtime/utils/runtime_templates/Dockerfile.j2
@@ -14,15 +14,21 @@ FROM {{ base_image }}
# Install necessary packages and clean up in one layer
RUN apt-get update && \
- apt-get install -y wget sudo apt-utils {{ LIBGL_MESA }} libasound2-plugins && \
- apt-get clean \
- && rm -rf /var/lib/apt/lists/*
+ apt-get install -y wget sudo apt-utils {{ LIBGL_MESA }} libasound2-plugins git && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Python if not already installed
+RUN if [ ! -e /usr/bin/python ]; then \
+ apt-get update && \
+ apt-get install -y python3 && \
+ ln -s /usr/bin/python3 /usr/bin/python; \
+fi
# Create necessary directories
RUN mkdir -p /opendevin && \
mkdir -p /opendevin/logs && \
- mkdir -p /opendevin/poetry && \
- chmod 777 -R /opendevin
+ mkdir -p /opendevin/poetry
ENV POETRY_VIRTUALENVS_PATH=/opendevin/poetry
@@ -35,8 +41,7 @@ RUN if [ ! -d /opendevin/miniforge3 ]; then \
fi
# Install Python and Poetry
-RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y
-RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y
+RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry python=3.11 -y
# ================================================================
# END: Build Runtime Image from Scratch
# ================================================================
@@ -45,10 +50,8 @@ RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y
# ================================================================
# START: Copy Project and Install/Update Dependencies
# ================================================================
-COPY project.tar.gz /opendevin
RUN if [ -d /opendevin/code ]; then rm -rf /opendevin/code; fi
-RUN cd /opendevin && tar -xzvf project.tar.gz && rm project.tar.gz
-RUN mv /opendevin/{{ source_code_dirname }} /opendevin/code
+COPY ./code /opendevin/code
# Install/Update Dependencies
# 1. Install pyproject.toml via poetry
@@ -60,7 +63,10 @@ RUN cd /opendevin/code && \
apt-get update && \
/opendevin/miniforge3/bin/mamba run -n base poetry run pip install playwright && \
/opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium && \
+ export OD_INTERPRETER_PATH=$(/opendevin/miniforge3/bin/mamba run -n base poetry run python -c "import sys; print(sys.executable)") && \
+ {{ extra_deps }} {% if extra_deps %} && {% endif %} \
/opendevin/miniforge3/bin/mamba run -n base poetry cache clear --all . && \
+ {% if not skip_init %}chmod -R g+rws /opendevin/poetry && {% endif %} \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/opendevin/miniforge3/bin/mamba clean --all
diff --git a/opendevin/server/README.md b/opendevin/server/README.md
index fa4d6249e6..991f0fe7c0 100644
--- a/opendevin/server/README.md
+++ b/opendevin/server/README.md
@@ -2,9 +2,23 @@
This is a WebSocket server that executes tasks using an agent.
+## Recommended Prerequisites
+
+- [Initialize the frontend code](../../frontend/README.md)
+- Install Python 3.12 (`brew install python` for those using homebrew)
+- Install pipx: (`brew install pipx` followed by `pipx ensurepath`)
+- Install poetry: (`pipx install poetry`)
+
## Install
-Follow the instructions in the base README.md to install dependencies and set up.
+First build a distribution of the frontend code (From the project root directory):
+```
+cd frontend
+npm install
+npm run build
+cd ..
+```
+Next run `poetry shell` (So you don't have to repeat `poetry run`)
## Start the Server
diff --git a/opendevin/server/listen.py b/opendevin/server/listen.py
index 12ff20ae6c..5f12bb9a4a 100644
--- a/opendevin/server/listen.py
+++ b/opendevin/server/listen.py
@@ -1,11 +1,10 @@
import os
import re
+import tempfile
import uuid
import warnings
import requests
-from pathspec import PathSpec
-from pathspec.patterns import GitWildMatchPattern
from opendevin.server.data_models.feedback import FeedbackDataModel, store_feedback
from opendevin.storage import get_file_store
@@ -33,13 +32,22 @@ from opendevin.controller.agent import Agent
from opendevin.core.config import LLMConfig, load_app_config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import AgentState # Add this import
-from opendevin.events.action import ChangeAgentStateAction, NullAction
+from opendevin.events.action import (
+ ChangeAgentStateAction,
+ FileReadAction,
+ FileWriteAction,
+ NullAction,
+)
from opendevin.events.observation import (
AgentStateChangedObservation,
+ ErrorObservation,
+ FileReadObservation,
+ FileWriteObservation,
NullObservation,
)
from opendevin.events.serialization import event_to_dict
from opendevin.llm import bedrock
+from opendevin.runtime.runtime import Runtime
from opendevin.server.auth import get_sid_from_token, sign_token
from opendevin.server.session import SessionManager
@@ -207,7 +215,7 @@ async def websocket_endpoint(websocket: WebSocket):
```
- Send a message:
```json
- {"action": "message", "args": {"content": "Hello, how are you?"}}
+ {"action": "message", "args": {"content": "Hello, how are you?", "images_urls": ["base64_url1", "base64_url2"]}}
```
- Write contents to a file:
```json
@@ -355,7 +363,7 @@ async def get_agents():
@app.get('/api/list-files')
-def list_files(request: Request, path: str = '/'):
+async def list_files(request: Request, path: str | None = None):
"""List files in the specified path.
This function retrieves a list of files from the agent's runtime file store,
@@ -368,7 +376,7 @@ def list_files(request: Request, path: str = '/'):
Args:
request (Request): The incoming request object.
- path (str, optional): The path to list files from. Defaults to '/'.
+ path (str, optional): The path to list files from. Defaults to None.
Returns:
list: A list of file names in the specified path.
@@ -381,90 +389,13 @@ def list_files(request: Request, path: str = '/'):
status_code=status.HTTP_404_NOT_FOUND,
content={'error': 'Runtime not yet initialized'},
)
-
- try:
- # Get the full path of the requested directory
- full_path = (
- request.state.session.agent_session.runtime.file_store.get_full_path(path)
- )
-
- # Check if the directory exists
- if not os.path.exists(full_path) or not os.path.isdir(full_path):
- return []
-
- # Check if .gitignore exists
- gitignore_path = os.path.join(full_path, '.gitignore')
- if os.path.exists(gitignore_path):
- # Use PathSpec to parse .gitignore
- with open(gitignore_path, 'r') as f:
- spec = PathSpec.from_lines(GitWildMatchPattern, f.readlines())
- else:
- # Fallback to default exclude list if .gitignore doesn't exist
- default_exclude = [
- '.git',
- '.DS_Store',
- '.svn',
- '.hg',
- '.idea',
- '.vscode',
- '.settings',
- '.pytest_cache',
- '__pycache__',
- 'node_modules',
- 'vendor',
- 'build',
- 'dist',
- 'bin',
- 'logs',
- 'log',
- 'tmp',
- 'temp',
- 'coverage',
- 'venv',
- 'env',
- ]
- spec = PathSpec.from_lines(GitWildMatchPattern, default_exclude)
-
- entries = request.state.session.agent_session.runtime.file_store.list(path)
-
- # Filter entries using PathSpec
- filtered_entries = [
- entry
- for entry in entries
- if not spec.match_file(os.path.relpath(entry, str(full_path)))
- ]
-
- # Separate directories and files
- directories = []
- files = []
- for entry in filtered_entries:
- # Remove leading slash and any parent directory components
- entry_relative = entry.lstrip('/').split('/')[-1]
-
- # Construct the full path by joining the base path with the relative entry path
- full_entry_path = os.path.join(full_path, entry_relative)
- if os.path.exists(full_entry_path):
- is_dir = os.path.isdir(full_entry_path)
- if is_dir:
- directories.append(entry)
- else:
- files.append(entry)
-
- # Sort directories and files separately
- directories.sort(key=lambda s: s.lower())
- files.sort(key=lambda s: s.lower())
-
- # Combine sorted directories and files
- sorted_entries = directories + files
- return sorted_entries
-
- except Exception as e:
- logger.error(f'Error listing files: {e}', exc_info=True)
- return []
+ runtime: Runtime = request.state.session.agent_session.runtime
+ file_list = await runtime.list_files(path)
+ return file_list
@app.get('/api/select-file')
-def select_file(file: str, request: Request):
+async def select_file(file: str, request: Request):
"""Retrieve the content of a specified file.
To select a file:
@@ -474,6 +405,7 @@ def select_file(file: str, request: Request):
Args:
file (str): The path of the file to be retrieved.
+ Expect path to be absolute inside the runtime.
request (Request): The incoming request object.
Returns:
@@ -482,16 +414,27 @@ def select_file(file: str, request: Request):
Raises:
HTTPException: If there's an error opening the file.
"""
- try:
- content = request.state.session.agent_session.runtime.file_store.read(file)
- except Exception as e:
- logger.error(f'Error opening file {file}: {e}', exc_info=False)
- error_msg = f'Error opening file: {e}'
+ runtime: Runtime = request.state.session.agent_session.runtime
+
+ # convert file to an absolute path inside the runtime
+ if not os.path.isabs(file):
+ return JSONResponse(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ content={'error': 'File path must be absolute'},
+ )
+
+ read_action = FileReadAction(file)
+ observation = await runtime.run_action(read_action)
+
+ if isinstance(observation, FileReadObservation):
+ content = observation.content
+ return {'code': content}
+ elif isinstance(observation, ErrorObservation):
+ logger.error(f'Error opening file {file}: {observation}', exc_info=False)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- content={'error': error_msg},
+ content={'error': f'Error opening file: {observation}'},
)
- return {'code': content}
@app.get('/api/files/{file_path:path}')
@@ -575,9 +518,17 @@ async def upload_file(request: Request, files: list[UploadFile]):
)
continue
- request.state.session.agent_session.runtime.file_store.write(
- safe_filename, file_contents
- )
+ # copy the file to the runtime
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ tmp_file_path = os.path.join(tmp_dir, safe_filename)
+ with open(tmp_file_path, 'wb') as tmp_file:
+ tmp_file.write(file_contents)
+ tmp_file.flush()
+
+ runtime: Runtime = request.state.session.agent_session.runtime
+ await runtime.copy_to(
+ tmp_file_path, runtime.config.workspace_mount_path_in_sandbox
+ )
uploaded_files.append(safe_filename)
response_content = {
@@ -732,13 +683,32 @@ async def save_file(request: Request):
if not file_path or content is None:
raise HTTPException(status_code=400, detail='Missing filePath or content')
- # Save the file to the agent's runtime file store
- request.state.session.agent_session.runtime.file_store.write(file_path, content)
+ # Make sure file_path is abs
+ if not os.path.isabs(file_path):
+ return JSONResponse(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ content={'error': 'File path must be absolute'},
+ )
- # Return a success response
- return JSONResponse(
- status_code=200, content={'message': 'File saved successfully'}
- )
+ # Save the file to the agent's runtime file store
+ runtime: Runtime = request.state.session.agent_session.runtime
+ write_action = FileWriteAction(file_path, content)
+ observation = await runtime.run_action(write_action)
+
+ if isinstance(observation, FileWriteObservation):
+ return JSONResponse(
+ status_code=200, content={'message': 'File saved successfully'}
+ )
+ elif isinstance(observation, ErrorObservation):
+ return JSONResponse(
+ status_code=500,
+ content={'error': f'Failed to save file: {observation}'},
+ )
+ else:
+ return JSONResponse(
+ status_code=500,
+ content={'error': f'Unexpected observation: {observation}'},
+ )
except Exception as e:
# Log the error and return a 500 response
logger.error(f'Error saving file: {e}', exc_info=True)
diff --git a/opendevin/server/session/agent.py b/opendevin/server/session/agent.py
index 104453a486..d77263734b 100644
--- a/opendevin/server/session/agent.py
+++ b/opendevin/server/session/agent.py
@@ -1,15 +1,13 @@
from typing import Optional
-from agenthub.codeact_agent.codeact_agent import CodeActAgent
from opendevin.controller import AgentController
from opendevin.controller.agent import Agent
from opendevin.controller.state.state import State
from opendevin.core.config import AppConfig, LLMConfig
from opendevin.core.logger import opendevin_logger as logger
from opendevin.events.stream import EventStream
-from opendevin.runtime import DockerSSHBox, get_runtime_cls
+from opendevin.runtime import get_runtime_cls
from opendevin.runtime.runtime import Runtime
-from opendevin.runtime.server.runtime import ServerRuntime
from opendevin.storage.files import FileStore
@@ -22,6 +20,7 @@ class AgentSession:
sid: str
event_stream: EventStream
+ file_store: FileStore
controller: Optional[AgentController] = None
runtime: Optional[Runtime] = None
_closed: bool = False
@@ -104,16 +103,6 @@ class AgentSession:
raise Exception('Runtime must be initialized before the agent controller')
logger.info(f'Creating agent {agent.name} using LLM {agent.llm.config.model}')
- if isinstance(agent, CodeActAgent):
- if not self.runtime or not (
- isinstance(self.runtime, ServerRuntime)
- and isinstance(self.runtime.sandbox, DockerSSHBox)
- ):
- logger.warning(
- 'CodeActAgent requires DockerSSHBox as sandbox! Using other sandbox that are not stateful'
- ' LocalBox will not work properly.'
- )
- self.runtime.init_runtime_tools(agent.runtime_tools)
self.controller = AgentController(
sid=self.sid,
diff --git a/opendevin/server/session/session.py b/opendevin/server/session/session.py
index 87326cb2e9..8cb1b8b596 100644
--- a/opendevin/server/session/session.py
+++ b/opendevin/server/session/session.py
@@ -10,7 +10,7 @@ from opendevin.core.logger import opendevin_logger as logger
from opendevin.core.schema import AgentState
from opendevin.core.schema.action import ActionType
from opendevin.core.schema.config import ConfigType
-from opendevin.events.action import ChangeAgentStateAction, NullAction
+from opendevin.events.action import ChangeAgentStateAction, MessageAction, NullAction
from opendevin.events.event import Event, EventSource
from opendevin.events.observation import (
AgentStateChangedObservation,
@@ -146,6 +146,14 @@ class Session:
await self._initialize_agent(data)
return
event = event_from_dict(data.copy())
+ # This checks if the model supports images
+ if isinstance(event, MessageAction) and event.images_urls:
+ controller = self.agent_session.controller
+ if controller and not controller.agent.llm.supports_vision():
+ await self.send_error(
+ 'Model does not support image upload, change to a different model or try without an image.'
+ )
+ return
self.agent_session.event_stream.add_event(event, EventSource.USER)
async def send(self, data: dict[str, object]) -> bool:
diff --git a/poetry.lock b/poetry.lock
index a33575d659..65a3890810 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -519,17 +519,17 @@ files = [
[[package]]
name = "boto3"
-version = "1.34.151"
+version = "1.34.157"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "boto3-1.34.151-py3-none-any.whl", hash = "sha256:35bc76faacf1667d3fbb66c1966acf2230ef26206557efc26d9d9d79337bef43"},
- {file = "boto3-1.34.151.tar.gz", hash = "sha256:30498a76b6f651ee2af7ae8edc1704379279ab8b91f1a8dd1f4ddf51259b0bc2"},
+ {file = "boto3-1.34.157-py3-none-any.whl", hash = "sha256:3cc357156df5482154a016f138d1953061a181b4c594f8b6302c9d6c024bd950"},
+ {file = "boto3-1.34.157.tar.gz", hash = "sha256:7ef19ed38cba9863b58430fb4a66a72a5c250304f234bd1c16b860f9bf25677b"},
]
[package.dependencies]
-botocore = ">=1.34.151,<1.35.0"
+botocore = ">=1.34.157,<1.35.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@@ -538,13 +538,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
-version = "1.34.151"
+version = "1.34.157"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
- {file = "botocore-1.34.151-py3-none-any.whl", hash = "sha256:9018680d7d4a8060c26d127ceec5ab5b270879f423ea39b863d8a46f3e34c404"},
- {file = "botocore-1.34.151.tar.gz", hash = "sha256:0d0968e427a94378f295b49d59170dad539938487ec948de3d030f06092ec6dc"},
+ {file = "botocore-1.34.157-py3-none-any.whl", hash = "sha256:c6cba6de8eb86ca4d2f934e009b37adbe1e7fdcfa52fbab74783f4c30676e07d"},
+ {file = "botocore-1.34.157.tar.gz", hash = "sha256:5628a36cec123cdc8c1158d05a7b06aa5e53649ad73796c50ef3fb51199785fb"},
]
[package.dependencies]
@@ -553,7 +553,7 @@ python-dateutil = ">=2.1,<3.0.0"
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
[package.extras]
-crt = ["awscrt (==0.20.11)"]
+crt = ["awscrt (==0.21.2)"]
[[package]]
name = "browsergym"
@@ -1378,6 +1378,20 @@ files = [
graph = ["objgraph (>=1.7.2)"]
profile = ["gprof2dot (>=2022.7.29)"]
+[[package]]
+name = "dirhash"
+version = "0.5.0"
+description = "Python module and CLI for hashing of file system directories."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "dirhash-0.5.0-py3-none-any.whl", hash = "sha256:523dfd6b058c64f45b31604376926c6e2bd2ea301d0df23095d4055674e38b09"},
+ {file = "dirhash-0.5.0.tar.gz", hash = "sha256:e60760f0ab2e935d8cb088923ea2c6492398dca42cec785df778985fd4cd5386"},
+]
+
+[package.dependencies]
+scantree = ">=0.0.4"
+
[[package]]
name = "dirtyjson"
version = "1.0.8"
@@ -1411,26 +1425,6 @@ files = [
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
]
-[[package]]
-name = "dnspython"
-version = "2.6.1"
-description = "DNS toolkit"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
- {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
-]
-
-[package.extras]
-dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
-dnssec = ["cryptography (>=41)"]
-doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
-doq = ["aioquic (>=0.9.25)"]
-idna = ["idna (>=3.6)"]
-trio = ["trio (>=0.23)"]
-wmi = ["wmi (>=1.5.1)"]
-
[[package]]
name = "docker"
version = "7.1.0"
@@ -1486,21 +1480,6 @@ typing-extensions = ">=4.8.0"
urllib3 = ">=1.25.3"
websockets = ">=11.0.3"
-[[package]]
-name = "email-validator"
-version = "2.2.0"
-description = "A robust email address syntax and deliverability validation library."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
- {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
-]
-
-[package.dependencies]
-dnspython = ">=2.0.0"
-idna = ">=2.0.0"
-
[[package]]
name = "english-words"
version = "2.0.1"
@@ -1587,45 +1566,23 @@ files = [
[[package]]
name = "fastapi"
-version = "0.111.1"
+version = "0.112.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
- {file = "fastapi-0.111.1-py3-none-any.whl", hash = "sha256:4f51cfa25d72f9fbc3280832e84b32494cf186f50158d364a8765aabf22587bf"},
- {file = "fastapi-0.111.1.tar.gz", hash = "sha256:ddd1ac34cb1f76c2e2d7f8545a4bcb5463bce4834e81abf0b189e0c359ab2413"},
+ {file = "fastapi-0.112.0-py3-none-any.whl", hash = "sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821"},
+ {file = "fastapi-0.112.0.tar.gz", hash = "sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05"},
]
[package.dependencies]
-email_validator = ">=2.0.0"
-fastapi-cli = ">=0.0.2"
-httpx = ">=0.23.0"
-jinja2 = ">=2.11.2"
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
-python-multipart = ">=0.0.7"
starlette = ">=0.37.2,<0.38.0"
typing-extensions = ">=4.8.0"
-uvicorn = {version = ">=0.12.0", extras = ["standard"]}
[package.extras]
-all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
-
-[[package]]
-name = "fastapi-cli"
-version = "0.0.4"
-description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "fastapi_cli-0.0.4-py3-none-any.whl", hash = "sha256:a2552f3a7ae64058cdbb530be6fa6dbfc975dc165e4fa66d224c3d396e25e809"},
- {file = "fastapi_cli-0.0.4.tar.gz", hash = "sha256:e2e9ffaffc1f7767f488d6da34b6f5a377751c996f397902eb6abb99a67bde32"},
-]
-
-[package.dependencies]
-typer = ">=0.12.3"
-
-[package.extras]
-standard = ["fastapi", "uvicorn[standard] (>=0.15.0)"]
+all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastcore"
@@ -1676,13 +1633,13 @@ typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "flake8"
-version = "7.1.0"
+version = "7.1.1"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.8.1"
files = [
- {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"},
- {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"},
+ {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"},
+ {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"},
]
[package.dependencies]
@@ -1926,6 +1883,36 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"]
tqdm = ["tqdm"]
+[[package]]
+name = "func-timeout"
+version = "4.3.5"
+description = "Python module which allows you to specify timeouts when calling any existing function. Also provides support for stoppable-threads"
+optional = false
+python-versions = "*"
+files = [
+ {file = "func_timeout-4.3.5.tar.gz", hash = "sha256:74cd3c428ec94f4edfba81f9b2f14904846d5ffccc27c92433b8b5939b5575dd"},
+]
+
+[[package]]
+name = "gdown"
+version = "5.2.0"
+description = "Google Drive Public File/Folder Downloader"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "gdown-5.2.0-py3-none-any.whl", hash = "sha256:33083832d82b1101bdd0e9df3edd0fbc0e1c5f14c9d8c38d2a35bf1683b526d6"},
+ {file = "gdown-5.2.0.tar.gz", hash = "sha256:2145165062d85520a3cd98b356c9ed522c5e7984d408535409fd46f94defc787"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+filelock = "*"
+requests = {version = "*", extras = ["socks"]}
+tqdm = "*"
+
+[package.extras]
+test = ["build", "mypy", "pytest", "pytest-xdist", "ruff", "twine", "types-requests", "types-setuptools"]
+
[[package]]
name = "gevent"
version = "24.2.1"
@@ -2140,13 +2127,13 @@ httplib2 = ">=0.19.0"
[[package]]
name = "google-cloud-aiplatform"
-version = "1.60.0"
+version = "1.61.0"
description = "Vertex AI API client library"
optional = false
python-versions = ">=3.8"
files = [
- {file = "google-cloud-aiplatform-1.60.0.tar.gz", hash = "sha256:782c7f1ec0e77a7c7daabef3b65bfd506ed2b4b1dc2186753c43cd6faf8dd04e"},
- {file = "google_cloud_aiplatform-1.60.0-py2.py3-none-any.whl", hash = "sha256:5f14159c9575f4b46335027e3ceb8fa57bd5eaa76a07f858105b8c6c034ec0d6"},
+ {file = "google-cloud-aiplatform-1.61.0.tar.gz", hash = "sha256:648e3cd7bb75be706d3c31d852a3d4d8a2e616ad4db4cf520ef4430615cf8ad9"},
+ {file = "google_cloud_aiplatform-1.61.0-py2.py3-none-any.whl", hash = "sha256:57b36d5fa085e68197e9fc576c43263a7cad320483aa3b166bcd1fdc7e8f49e7"},
]
[package.dependencies]
@@ -2158,7 +2145,7 @@ google-cloud-resource-manager = ">=1.3.3,<3.0.0dev"
google-cloud-storage = ">=1.32.0,<3.0.0dev"
packaging = ">=14.3"
proto-plus = ">=1.22.3,<2.0.0dev"
-protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev"
+protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
pydantic = "<3"
shapely = "<3.0.0dev"
@@ -2491,13 +2478,13 @@ test = ["objgraph", "psutil"]
[[package]]
name = "grep-ast"
-version = "0.3.2"
+version = "0.3.3"
description = "A tool to grep through the AST of a source file"
optional = false
python-versions = "*"
files = [
- {file = "grep_ast-0.3.2-py3-none-any.whl", hash = "sha256:b7ceb84743983c3f4f5bca82f3374534cd9dbd759792d0dedf5648fedbb6f3fc"},
- {file = "grep_ast-0.3.2.tar.gz", hash = "sha256:d53bc7d25dfefafe77643fec189ab38e3cbd839d546c070a950ebedad82ee164"},
+ {file = "grep_ast-0.3.3-py3-none-any.whl", hash = "sha256:515cb889bffefefa26c4ab1377b9a75b3fc678aa5fa02bf9aa4f8f20999a83ad"},
+ {file = "grep_ast-0.3.3.tar.gz", hash = "sha256:42b8887d57301dc55634368f8d549e9c49c913dafb4d19c9b54c3ddb604fccf4"},
]
[package.dependencies]
@@ -3003,6 +2990,76 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
+[[package]]
+name = "jiter"
+version = "0.5.0"
+description = "Fast iterable JSON parser."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f"},
+ {file = "jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5"},
+ {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28"},
+ {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e"},
+ {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a"},
+ {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749"},
+ {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc"},
+ {file = "jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d"},
+ {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87"},
+ {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e"},
+ {file = "jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf"},
+ {file = "jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e"},
+ {file = "jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553"},
+ {file = "jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3"},
+ {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6"},
+ {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4"},
+ {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9"},
+ {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614"},
+ {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e"},
+ {file = "jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06"},
+ {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403"},
+ {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646"},
+ {file = "jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb"},
+ {file = "jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae"},
+ {file = "jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a"},
+ {file = "jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df"},
+ {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248"},
+ {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544"},
+ {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba"},
+ {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f"},
+ {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e"},
+ {file = "jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a"},
+ {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e"},
+ {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338"},
+ {file = "jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4"},
+ {file = "jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5"},
+ {file = "jiter-0.5.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f04bc2fc50dc77be9d10f73fcc4e39346402ffe21726ff41028f36e179b587e6"},
+ {file = "jiter-0.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f433a4169ad22fcb550b11179bb2b4fd405de9b982601914ef448390b2954f3"},
+ {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad4a6398c85d3a20067e6c69890ca01f68659da94d74c800298581724e426c7e"},
+ {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6baa88334e7af3f4d7a5c66c3a63808e5efbc3698a1c57626541ddd22f8e4fbf"},
+ {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ece0a115c05efca597c6d938f88c9357c843f8c245dbbb53361a1c01afd7148"},
+ {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:335942557162ad372cc367ffaf93217117401bf930483b4b3ebdb1223dbddfa7"},
+ {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649b0ee97a6e6da174bffcb3c8c051a5935d7d4f2f52ea1583b5b3e7822fbf14"},
+ {file = "jiter-0.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4be354c5de82157886ca7f5925dbda369b77344b4b4adf2723079715f823989"},
+ {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5206144578831a6de278a38896864ded4ed96af66e1e63ec5dd7f4a1fce38a3a"},
+ {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8120c60f8121ac3d6f072b97ef0e71770cc72b3c23084c72c4189428b1b1d3b6"},
+ {file = "jiter-0.5.0-cp38-none-win32.whl", hash = "sha256:6f1223f88b6d76b519cb033a4d3687ca157c272ec5d6015c322fc5b3074d8a5e"},
+ {file = "jiter-0.5.0-cp38-none-win_amd64.whl", hash = "sha256:c59614b225d9f434ea8fc0d0bec51ef5fa8c83679afedc0433905994fb36d631"},
+ {file = "jiter-0.5.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0af3838cfb7e6afee3f00dc66fa24695199e20ba87df26e942820345b0afc566"},
+ {file = "jiter-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:550b11d669600dbc342364fd4adbe987f14d0bbedaf06feb1b983383dcc4b961"},
+ {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489875bf1a0ffb3cb38a727b01e6673f0f2e395b2aad3c9387f94187cb214bbf"},
+ {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b250ca2594f5599ca82ba7e68785a669b352156260c5362ea1b4e04a0f3e2389"},
+ {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ea18e01f785c6667ca15407cd6dabbe029d77474d53595a189bdc813347218e"},
+ {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462a52be85b53cd9bffd94e2d788a09984274fe6cebb893d6287e1c296d50653"},
+ {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92cc68b48d50fa472c79c93965e19bd48f40f207cb557a8346daa020d6ba973b"},
+ {file = "jiter-0.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c834133e59a8521bc87ebcad773608c6fa6ab5c7a022df24a45030826cf10bc"},
+ {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab3a71ff31cf2d45cb216dc37af522d335211f3a972d2fe14ea99073de6cb104"},
+ {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cccd3af9c48ac500c95e1bcbc498020c87e1781ff0345dd371462d67b76643eb"},
+ {file = "jiter-0.5.0-cp39-none-win32.whl", hash = "sha256:368084d8d5c4fc40ff7c3cc513c4f73e02c85f6009217922d0823a48ee7adf61"},
+ {file = "jiter-0.5.0-cp39-none-win_amd64.whl", hash = "sha256:ce03f7b4129eb72f1687fa11300fbf677b02990618428934662406d2a76742a1"},
+ {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"},
+]
+
[[package]]
name = "jmespath"
version = "1.0.1"
@@ -3027,13 +3084,13 @@ files = [
[[package]]
name = "json-repair"
-version = "0.25.3"
+version = "0.27.0"
description = "A package to repair broken json strings"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "json_repair-0.25.3-py3-none-any.whl", hash = "sha256:f00b510dd21b31ebe72581bdb07e66381df2883d6f640c89605e482882c12b17"},
- {file = "json_repair-0.25.3.tar.gz", hash = "sha256:4ee970581a05b0b258b749eb8bcac21de380edda97c3717a4edfafc519ec21a4"},
+ {file = "json_repair-0.27.0-py3-none-any.whl", hash = "sha256:20763c8cf1c3096e33ce7c09c2b8e6c471a4acdce468688c96052fc7cccbad7f"},
+ {file = "json_repair-0.27.0.tar.gz", hash = "sha256:f4e14c5ad2b3f17290a361c3c90915536b462c36f69989e915867e81663dd467"},
]
[[package]]
@@ -3509,13 +3566,13 @@ types-tqdm = "*"
[[package]]
name = "litellm"
-version = "1.42.5"
+version = "1.43.4"
description = "Library to easily interface with LLM API providers"
optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
files = [
- {file = "litellm-1.42.5-py3-none-any.whl", hash = "sha256:c8c2f9e40b5aa1c2dcfcac9adb854b8ac22ce2112825d742d8fce516d26e9a65"},
- {file = "litellm-1.42.5.tar.gz", hash = "sha256:64ea24040751009e70e816e9340c5c82717d9a309f4480e5ece9f3f67328e04e"},
+ {file = "litellm-1.43.4-py3-none-any.whl", hash = "sha256:619cfaab189f921f66ff50c2b7a0e965e562c2a95b17c2ee24649826ba35da11"},
+ {file = "litellm-1.43.4.tar.gz", hash = "sha256:949c51ad494b935d80da1cd18c3567e4ed181f8eb531ef4706e3be72afb0c43c"},
]
[package.dependencies]
@@ -3524,7 +3581,7 @@ click = "*"
importlib-metadata = ">=6.8.0"
jinja2 = ">=3.1.2,<4.0.0"
jsonschema = ">=4.22.0,<5.0.0"
-openai = ">=1.27.0"
+openai = ">=1.40.0"
pydantic = ">=2.0.0,<3.0.0"
python-dotenv = ">=0.2.0"
requests = ">=2.31.0,<3.0.0"
@@ -3677,13 +3734,13 @@ sentence-transformers = ">=2.6.1"
[[package]]
name = "llama-index-embeddings-ollama"
-version = "0.1.2"
+version = "0.1.3"
description = "llama-index embeddings ollama integration"
optional = false
-python-versions = ">=3.8.1,<4.0"
+python-versions = "<4.0,>=3.8.1"
files = [
- {file = "llama_index_embeddings_ollama-0.1.2-py3-none-any.whl", hash = "sha256:ac7afabfa1134059af351b021e05e256bf86dd15e5176ffa5ab0305bcf03b33f"},
- {file = "llama_index_embeddings_ollama-0.1.2.tar.gz", hash = "sha256:a9e0809bddd2e4ad888f249519edc7e3d339c74e4e03fc5a40c3060dc41d47a9"},
+ {file = "llama_index_embeddings_ollama-0.1.3-py3-none-any.whl", hash = "sha256:b960a8c744e2e56ce1fd75a34753614fed3ad81558570ae9958b90b9062afb6a"},
+ {file = "llama_index_embeddings_ollama-0.1.3.tar.gz", hash = "sha256:4bd1dd3230c9be04cfa45b28c3a8066e46c1654d4360fcbecdc1718ac9013eca"},
]
[package.dependencies]
@@ -5069,23 +5126,24 @@ sympy = "*"
[[package]]
name = "openai"
-version = "1.37.1"
+version = "1.40.2"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.7.1"
files = [
- {file = "openai-1.37.1-py3-none-any.whl", hash = "sha256:9a6adda0d6ae8fce02d235c5671c399cfa40d6a281b3628914c7ebf244888ee3"},
- {file = "openai-1.37.1.tar.gz", hash = "sha256:faf87206785a6b5d9e34555d6a3242482a6852bc802e453e2a891f68ee04ce55"},
+ {file = "openai-1.40.2-py3-none-any.whl", hash = "sha256:38068f858f310b4fd4b0ea8734c3efcfde3c15a2978311e1453bd84817231b96"},
+ {file = "openai-1.40.2.tar.gz", hash = "sha256:2180e9070bd36084328248b3ce668964e8ddd2e9019e1d426e31dc54cc117bb5"},
]
[package.dependencies]
anyio = ">=3.5.0,<5"
distro = ">=1.7.0,<2"
httpx = ">=0.23.0,<1"
+jiter = ">=0.4.0,<1"
pydantic = ">=1.9.0,<3"
sniffio = "*"
tqdm = ">4"
-typing-extensions = ">=4.7,<5"
+typing-extensions = ">=4.11,<5"
[package.extras]
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
@@ -5562,13 +5620,13 @@ xmp = ["defusedxml"]
[[package]]
name = "pip"
-version = "24.1.1"
+version = "24.2"
description = "The PyPA recommended tool for installing Python packages."
optional = false
python-versions = ">=3.8"
files = [
- {file = "pip-24.1.1-py3-none-any.whl", hash = "sha256:efca15145a95e95c00608afeab66311d40bfb73bb2266a855befd705e6bb15a0"},
- {file = "pip-24.1.1.tar.gz", hash = "sha256:5aa64f65e1952733ee0a9a9b1f52496ebdb3f3077cc46f80a16d983b58d1180a"},
+ {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"},
+ {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"},
]
[[package]]
@@ -6245,6 +6303,18 @@ files = [
{file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
]
+[[package]]
+name = "pysocks"
+version = "1.7.1"
+description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
+ {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
+ {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
+]
+
[[package]]
name = "pytest"
version = "8.3.2"
@@ -6386,18 +6456,19 @@ dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatc
[[package]]
name = "python-pptx"
-version = "0.6.23"
-description = "Generate and manipulate Open XML PowerPoint (.pptx) files"
+version = "1.0.2"
+description = "Create, read, and update PowerPoint 2007+ (.pptx) files."
optional = false
-python-versions = "*"
+python-versions = ">=3.8"
files = [
- {file = "python-pptx-0.6.23.tar.gz", hash = "sha256:587497ff28e779ab18dbb074f6d4052893c85dedc95ed75df319364f331fedee"},
- {file = "python_pptx-0.6.23-py3-none-any.whl", hash = "sha256:dd0527194627a2b7cc05f3ba23ecaa2d9a0d5ac9b6193a28ed1b7a716f4217d4"},
+ {file = "python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba"},
+ {file = "python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095"},
]
[package.dependencies]
lxml = ">=3.1.0"
Pillow = ">=3.3.2"
+typing-extensions = ">=4.9.0"
XlsxWriter = ">=0.5.7"
[[package]]
@@ -6747,6 +6818,7 @@ files = [
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
+PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""}
urllib3 = ">=1.21.1,<3"
[package.extras]
@@ -6953,29 +7025,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
-version = "0.5.5"
+version = "0.5.7"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"},
- {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"},
- {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"},
- {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
- {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"},
- {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"},
- {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"},
- {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"},
- {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"},
- {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"},
- {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"},
- {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
+ {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
+ {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
+ {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"},
+ {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"},
+ {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"},
+ {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"},
+ {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"},
+ {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"},
+ {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
]
[[package]]
@@ -7117,6 +7189,21 @@ tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"]
testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"]
torch = ["safetensors[numpy]", "torch (>=1.10)"]
+[[package]]
+name = "scantree"
+version = "0.0.4"
+description = "Flexible recursive directory iterator: scandir meets glob(\"**\", recursive=True)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "scantree-0.0.4-py3-none-any.whl", hash = "sha256:7616ab65aa6b7f16fcf8e6fa1d9afaa99a27ab72bba05c61b691853b96763174"},
+ {file = "scantree-0.0.4.tar.gz", hash = "sha256:15bd5cb24483b04db2c70653604e8ea3522e98087db7e38ab8482f053984c0ac"},
+]
+
+[package.dependencies]
+attrs = ">=18.0.0"
+pathspec = ">=0.10.1"
+
[[package]]
name = "scikit-learn"
version = "1.5.0"
@@ -7518,13 +7605,13 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7
[[package]]
name = "streamlit"
-version = "1.37.0"
+version = "1.37.1"
description = "A faster way to build and share data apps"
optional = false
python-versions = "!=3.9.7,>=3.8"
files = [
- {file = "streamlit-1.37.0-py2.py3-none-any.whl", hash = "sha256:d17e2d32b075a270a97f134ab5d22bbb98b4e474fa261ff49dc4a2b380386c84"},
- {file = "streamlit-1.37.0.tar.gz", hash = "sha256:463ef728ba21e74e05122e3704e8af644a7bdbb5822e281b8daf4a0a48761879"},
+ {file = "streamlit-1.37.1-py2.py3-none-any.whl", hash = "sha256:0651240fccc569900cc9450390b0a67473fda55be65f317e46285f99e2bddf04"},
+ {file = "streamlit-1.37.1.tar.gz", hash = "sha256:bc7e3813d94a39dda56f15678437eb37830973c601e8e574f2225a7bf188ea5a"},
]
[package.dependencies]
@@ -7564,7 +7651,7 @@ files = [
[[package]]
name = "swebench"
-version = "2.0.2"
+version = "2.0.12"
description = "The official SWE-bench package - a benchmark for evaluating LMs on software engineering"
optional = false
python-versions = ">=3.8"
@@ -7583,26 +7670,33 @@ python-dotenv = "*"
requests = "*"
rich = "*"
tqdm = "*"
+unidiff = "*"
+
+[package.extras]
+inference = ["anthropic", "flash_attn", "jedi", "openai", "peft", "protobuf", "sentencepiece", "tenacity", "tiktoken", "torch", "transformers", "triton"]
[package.source]
type = "git"
url = "https://github.com/OpenDevin/SWE-bench.git"
reference = "HEAD"
-resolved_reference = "4498af933679c518df6d1f8485864f72afc21500"
+resolved_reference = "c2b3cefd4a5af0b248966a773650a39046072975"
[[package]]
name = "sympy"
-version = "1.12.1"
+version = "1.13.1"
description = "Computer algebra system (CAS) in Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "sympy-1.12.1-py3-none-any.whl", hash = "sha256:9b2cbc7f1a640289430e13d2a56f02f867a1da0190f2f99d8968c2f74da0e515"},
- {file = "sympy-1.12.1.tar.gz", hash = "sha256:2877b03f998cd8c08f07cd0de5b767119cd3ef40d09f41c30d722f6686b0fb88"},
+ {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"},
+ {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"},
]
[package.dependencies]
-mpmath = ">=1.1.0,<1.4.0"
+mpmath = ">=1.1.0,<1.4"
+
+[package.extras]
+dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"]
[[package]]
name = "tenacity"
@@ -7892,36 +7986,36 @@ files = [
[[package]]
name = "torch"
-version = "2.2.0"
+version = "2.2.2"
description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration"
optional = false
python-versions = ">=3.8.0"
files = [
- {file = "torch-2.2.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:d366158d6503a3447e67f8c0ad1328d54e6c181d88572d688a625fac61b13a97"},
- {file = "torch-2.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:707f2f80402981e9f90d0038d7d481678586251e6642a7a6ef67fc93511cb446"},
- {file = "torch-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:15c8f0a105c66b28496092fca1520346082e734095f8eaf47b5786bac24b8a31"},
- {file = "torch-2.2.0-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:0ca4df4b728515ad009b79f5107b00bcb2c63dc202d991412b9eb3b6a4f24349"},
- {file = "torch-2.2.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:3d3eea2d5969b9a1c9401429ca79efc668120314d443d3463edc3289d7f003c7"},
- {file = "torch-2.2.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:0d1c580e379c0d48f0f0a08ea28d8e373295aa254de4f9ad0631f9ed8bc04c24"},
- {file = "torch-2.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9328e3c1ce628a281d2707526b4d1080eae7c4afab4f81cea75bde1f9441dc78"},
- {file = "torch-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:03c8e660907ac1b8ee07f6d929c4e15cd95be2fb764368799cca02c725a212b8"},
- {file = "torch-2.2.0-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:da0cefe7f84ece3e3b56c11c773b59d1cb2c0fd83ddf6b5f7f1fd1a987b15c3e"},
- {file = "torch-2.2.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:f81d23227034221a4a4ff8ef24cc6cec7901edd98d9e64e32822778ff01be85e"},
- {file = "torch-2.2.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:dcbfb2192ac41ca93c756ebe9e2af29df0a4c14ee0e7a0dd78f82c67a63d91d4"},
- {file = "torch-2.2.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:9eeb42971619e24392c9088b5b6d387d896e267889d41d267b1fec334f5227c5"},
- {file = "torch-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c718b2ca69a6cac28baa36d86d8c0ec708b102cebd1ceb1b6488e404cd9be1d1"},
- {file = "torch-2.2.0-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:f11d18fceb4f9ecb1ac680dde7c463c120ed29056225d75469c19637e9f98d12"},
- {file = "torch-2.2.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:ee1da852bfd4a7e674135a446d6074c2da7194c1b08549e31eae0b3138c6b4d2"},
- {file = "torch-2.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0d819399819d0862268ac531cf12a501c253007df4f9e6709ede8a0148f1a7b8"},
- {file = "torch-2.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:08f53ccc38c49d839bc703ea1b20769cc8a429e0c4b20b56921a9f64949bf325"},
- {file = "torch-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:93bffe3779965a71dab25fc29787538c37c5d54298fd2f2369e372b6fb137d41"},
- {file = "torch-2.2.0-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:c17ec323da778efe8dad49d8fb534381479ca37af1bfc58efdbb8607a9d263a3"},
- {file = "torch-2.2.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:c02685118008834e878f676f81eab3a952b7936fa31f474ef8a5ff4b5c78b36d"},
- {file = "torch-2.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d9f39d6f53cec240a0e3baa82cb697593340f9d4554cee6d3d6ca07925c2fac0"},
- {file = "torch-2.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:51770c065206250dc1222ea7c0eff3f88ab317d3e931cca2aee461b85fbc2472"},
- {file = "torch-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:008e4c6ad703de55af760c73bf937ecdd61a109f9b08f2bbb9c17e7c7017f194"},
- {file = "torch-2.2.0-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:de8680472dd14e316f42ceef2a18a301461a9058cd6e99a1f1b20f78f11412f1"},
- {file = "torch-2.2.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:99e1dcecb488e3fd25bcaac56e48cdb3539842904bdc8588b0b255fde03a254c"},
+ {file = "torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585"},
+ {file = "torch-2.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:15dffa4cc3261fa73d02f0ed25f5fa49ecc9e12bf1ae0a4c1e7a88bbfaad9030"},
+ {file = "torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5"},
+ {file = "torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e"},
+ {file = "torch-2.2.2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:877b3e6593b5e00b35bbe111b7057464e76a7dd186a287280d941b564b0563c2"},
+ {file = "torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb"},
+ {file = "torch-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:32827fa1fbe5da8851686256b4cd94cc7b11be962862c2293811c94eea9457bf"},
+ {file = "torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c"},
+ {file = "torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059"},
+ {file = "torch-2.2.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:49aa4126ede714c5aeef7ae92969b4b0bbe67f19665106463c39f22e0a1860d1"},
+ {file = "torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca"},
+ {file = "torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c"},
+ {file = "torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea"},
+ {file = "torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533"},
+ {file = "torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc"},
+ {file = "torch-2.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd2bf7697c9e95fb5d97cc1d525486d8cf11a084c6af1345c2c2c22a6b0029d0"},
+ {file = "torch-2.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b421448d194496e1114d87a8b8d6506bce949544e513742b097e2ab8f7efef32"},
+ {file = "torch-2.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:3dbcd563a9b792161640c0cffe17e3270d85e8f4243b1f1ed19cca43d28d235b"},
+ {file = "torch-2.2.2-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:31f4310210e7dda49f1fb52b0ec9e59382cfcb938693f6d5378f25b43d7c1d29"},
+ {file = "torch-2.2.2-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:c795feb7e8ce2e0ef63f75f8e1ab52e7fd5e1a4d7d0c31367ade1e3de35c9e95"},
+ {file = "torch-2.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a6e5770d68158d07456bfcb5318b173886f579fdfbf747543901ce718ea94782"},
+ {file = "torch-2.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:67dcd726edff108e2cd6c51ff0e416fd260c869904de95750e80051358680d24"},
+ {file = "torch-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:539d5ef6c4ce15bd3bd47a7b4a6e7c10d49d4d21c0baaa87c7d2ef8698632dfb"},
+ {file = "torch-2.2.2-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:dff696de90d6f6d1e8200e9892861fd4677306d0ef604cb18f2134186f719f82"},
+ {file = "torch-2.2.2-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:3a4dd910663fd7a124c056c878a52c2b0be4a5a424188058fe97109d4436ee42"},
]
[package.dependencies]
@@ -7941,7 +8035,7 @@ nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"
nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
sympy = "*"
-triton = {version = "2.2.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+triton = {version = "2.2.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\""}
typing-extensions = ">=4.8.0"
[package.extras]
@@ -8297,6 +8391,17 @@ files = [
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
+[[package]]
+name = "unidiff"
+version = "0.7.5"
+description = "Unified diff parsing/metadata extraction library."
+optional = false
+python-versions = "*"
+files = [
+ {file = "unidiff-0.7.5-py2.py3-none-any.whl", hash = "sha256:c93bf2265cc1ba2a520e415ab05da587370bc2a3ae9e0414329f54f0c2fc09e8"},
+ {file = "unidiff-0.7.5.tar.gz", hash = "sha256:2e5f0162052248946b9f0970a40e9e124236bf86c82b70821143a6fc1dea2574"},
+]
+
[[package]]
name = "uri-template"
version = "1.3.0"
@@ -8341,13 +8446,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
-version = "0.30.3"
+version = "0.30.5"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
- {file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
- {file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
+ {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"},
+ {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"},
]
[package.dependencies]
@@ -9066,47 +9171,45 @@ test = ["zope.testrunner"]
[[package]]
name = "zope-interface"
-version = "6.4.post2"
+version = "7.0.1"
description = "Interfaces for Python"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "zope.interface-6.4.post2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2eccd5bef45883802848f821d940367c1d0ad588de71e5cabe3813175444202c"},
- {file = "zope.interface-6.4.post2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:762e616199f6319bb98e7f4f27d254c84c5fb1c25c908c2a9d0f92b92fb27530"},
- {file = "zope.interface-6.4.post2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef8356f16b1a83609f7a992a6e33d792bb5eff2370712c9eaae0d02e1924341"},
- {file = "zope.interface-6.4.post2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b"},
- {file = "zope.interface-6.4.post2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d22fce0b0f5715cdac082e35a9e735a1752dc8585f005d045abb1a7c20e197f9"},
- {file = "zope.interface-6.4.post2-cp310-cp310-win_amd64.whl", hash = "sha256:97e615eab34bd8477c3f34197a17ce08c648d38467489359cb9eb7394f1083f7"},
- {file = "zope.interface-6.4.post2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:599f3b07bde2627e163ce484d5497a54a0a8437779362395c6b25e68c6590ede"},
- {file = "zope.interface-6.4.post2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:136cacdde1a2c5e5bc3d0b2a1beed733f97e2dad8c2ad3c2e17116f6590a3827"},
- {file = "zope.interface-6.4.post2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47937cf2e7ed4e0e37f7851c76edeb8543ec9b0eae149b36ecd26176ff1ca874"},
- {file = "zope.interface-6.4.post2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f0a6be264afb094975b5ef55c911379d6989caa87c4e558814ec4f5125cfa2e"},
- {file = "zope.interface-6.4.post2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47654177e675bafdf4e4738ce58cdc5c6d6ee2157ac0a78a3fa460942b9d64a8"},
- {file = "zope.interface-6.4.post2-cp311-cp311-win_amd64.whl", hash = "sha256:e2fb8e8158306567a3a9a41670c1ff99d0567d7fc96fa93b7abf8b519a46b250"},
- {file = "zope.interface-6.4.post2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b912750b13d76af8aac45ddf4679535def304b2a48a07989ec736508d0bbfbde"},
- {file = "zope.interface-6.4.post2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ac46298e0143d91e4644a27a769d1388d5d89e82ee0cf37bf2b0b001b9712a4"},
- {file = "zope.interface-6.4.post2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86a94af4a88110ed4bb8961f5ac72edf782958e665d5bfceaab6bf388420a78b"},
- {file = "zope.interface-6.4.post2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73f9752cf3596771c7726f7eea5b9e634ad47c6d863043589a1c3bb31325c7eb"},
- {file = "zope.interface-6.4.post2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854"},
- {file = "zope.interface-6.4.post2-cp312-cp312-win_amd64.whl", hash = "sha256:551db2fe892fcbefb38f6f81ffa62de11090c8119fd4e66a60f3adff70751ec7"},
- {file = "zope.interface-6.4.post2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96ac6b3169940a8cd57b4f2b8edcad8f5213b60efcd197d59fbe52f0accd66e"},
- {file = "zope.interface-6.4.post2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cebff2fe5dc82cb22122e4e1225e00a4a506b1a16fafa911142ee124febf2c9e"},
- {file = "zope.interface-6.4.post2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ee982237cffaf946db365c3a6ebaa37855d8e3ca5800f6f48890209c1cfefc"},
- {file = "zope.interface-6.4.post2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:fbf649bc77510ef2521cf797700b96167bb77838c40780da7ea3edd8b78044d1"},
- {file = "zope.interface-6.4.post2-cp37-cp37m-win_amd64.whl", hash = "sha256:4c0b208a5d6c81434bdfa0f06d9b667e5de15af84d8cae5723c3a33ba6611b82"},
- {file = "zope.interface-6.4.post2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3fe667935e9562407c2511570dca14604a654988a13d8725667e95161d92e9b"},
- {file = "zope.interface-6.4.post2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a96e6d4074db29b152222c34d7eec2e2db2f92638d2b2b2c704f9e8db3ae0edc"},
- {file = "zope.interface-6.4.post2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:866a0f583be79f0def667a5d2c60b7b4cc68f0c0a470f227e1122691b443c934"},
- {file = "zope.interface-6.4.post2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fe919027f29b12f7a2562ba0daf3e045cb388f844e022552a5674fcdf5d21f1"},
- {file = "zope.interface-6.4.post2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e0343a6e06d94f6b6ac52fbc75269b41dd3c57066541a6c76517f69fe67cb43"},
- {file = "zope.interface-6.4.post2-cp38-cp38-win_amd64.whl", hash = "sha256:dabb70a6e3d9c22df50e08dc55b14ca2a99da95a2d941954255ac76fd6982bc5"},
- {file = "zope.interface-6.4.post2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:706efc19f9679a1b425d6fa2b4bc770d976d0984335eaea0869bd32f627591d2"},
- {file = "zope.interface-6.4.post2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d136e5b8821073e1a09dde3eb076ea9988e7010c54ffe4d39701adf0c303438"},
- {file = "zope.interface-6.4.post2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1730c93a38b5a18d24549bc81613223962a19d457cfda9bdc66e542f475a36f4"},
- {file = "zope.interface-6.4.post2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc2676312cc3468a25aac001ec727168994ea3b69b48914944a44c6a0b251e79"},
- {file = "zope.interface-6.4.post2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a62fd6cd518693568e23e02f41816adedfca637f26716837681c90b36af3671"},
- {file = "zope.interface-6.4.post2-cp39-cp39-win_amd64.whl", hash = "sha256:d3f7e001328bd6466b3414215f66dde3c7c13d8025a9c160a75d7b2687090d15"},
- {file = "zope.interface-6.4.post2.tar.gz", hash = "sha256:1c207e6f6dfd5749a26f5a5fd966602d6b824ec00d2df84a7e9a924e8933654e"},
+ {file = "zope.interface-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec4e87e6fdc511a535254daa122c20e11959ce043b4e3425494b237692a34f1c"},
+ {file = "zope.interface-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51d5713e8e38f2d3ec26e0dfdca398ed0c20abda2eb49ffc15a15a23eb8e5f6d"},
+ {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8d51e5eb29e57d34744369cd08267637aa5a0fefc9b5d33775ab7ff2ebf2e3"},
+ {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55bbcc74dc0c7ab489c315c28b61d7a1d03cf938cc99cc58092eb065f120c3a5"},
+ {file = "zope.interface-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10ebac566dd0cec66f942dc759d46a994a2b3ba7179420f0e2130f88f8a5f400"},
+ {file = "zope.interface-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7039e624bcb820f77cc2ff3d1adcce531932990eee16121077eb51d9c76b6c14"},
+ {file = "zope.interface-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03bd5c0db82237bbc47833a8b25f1cc090646e212f86b601903d79d7e6b37031"},
+ {file = "zope.interface-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f52050c6a10d4a039ec6f2c58e5b3ade5cc570d16cf9d102711e6b8413c90e6"},
+ {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af0b33f04677b57843d529b9257a475d2865403300b48c67654c40abac2f9f24"},
+ {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696c2a381fc7876b3056711717dba5eddd07c2c9e5ccd50da54029a1293b6e43"},
+ {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f89a420cf5a6f2aa7849dd59e1ff0e477f562d97cf8d6a1ee03461e1eec39887"},
+ {file = "zope.interface-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b59deb0ddc7b431e41d720c00f99d68b52cb9bd1d5605a085dc18f502fe9c47f"},
+ {file = "zope.interface-7.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52f5253cca1b35eaeefa51abd366b87f48f8714097c99b131ba61f3fdbbb58e7"},
+ {file = "zope.interface-7.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88d108d004e0df25224de77ce349a7e73494ea2cb194031f7c9687e68a88ec9b"},
+ {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c203d82069ba31e1f3bc7ba530b2461ec86366cd4bfc9b95ec6ce58b1b559c34"},
+ {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3495462bc0438b76536a0e10d765b168ae636092082531b88340dc40dcd118"},
+ {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192b7a792e3145ed880ff6b1a206fdb783697cfdb4915083bfca7065ec845e60"},
+ {file = "zope.interface-7.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:400d06c9ec8dbcc96f56e79376297e7be07a315605c9a2208720da263d44d76f"},
+ {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c1dff87b30fd150c61367d0e2cdc49bb55f8b9fd2a303560bbc24b951573ae1"},
+ {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f749ca804648d00eda62fe1098f229b082dfca930d8bad8386e572a6eafa7525"},
+ {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec212037becf6d2f705b7ed4538d56980b1e7bba237df0d8995cbbed29961dc"},
+ {file = "zope.interface-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d33cb526efdc235a2531433fc1287fcb80d807d5b401f9b801b78bf22df560dd"},
+ {file = "zope.interface-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b419f2144e1762ab845f20316f1df36b15431f2622ebae8a6d5f7e8e712b413c"},
+ {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f1452d5d1f279184d5bdb663a3dc39902d9320eceb63276240791e849054b6"},
+ {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ba4b3638d014918b918aa90a9c8370bd74a03abf8fcf9deb353b3a461a59a84"},
+ {file = "zope.interface-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc0615351221926a36a0fbcb2520fb52e0b23e8c22a43754d9cb8f21358c33c0"},
+ {file = "zope.interface-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:ce6cbb852fb8f2f9bb7b9cdca44e2e37bce783b5f4c167ff82cb5f5128163c8f"},
+ {file = "zope.interface-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5566fd9271c89ad03d81b0831c37d46ae5e2ed211122c998637130159a120cf1"},
+ {file = "zope.interface-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da0cef4d7e3f19c3bd1d71658d6900321af0492fee36ec01b550a10924cffb9c"},
+ {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32ca483e6ade23c7caaee9d5ee5d550cf4146e9b68d2fb6c68bac183aa41c37"},
+ {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da21e7eec49252df34d426c2ee9cf0361c923026d37c24728b0fa4cc0599fd03"},
+ {file = "zope.interface-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a8195b99e650e6f329ce4e5eb22d448bdfef0406404080812bc96e2a05674cb"},
+ {file = "zope.interface-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:19c829d52e921b9fe0b2c0c6a8f9a2508c49678ee1be598f87d143335b6a35dc"},
+ {file = "zope.interface-7.0.1.tar.gz", hash = "sha256:f0f5fda7cbf890371a59ab1d06512da4f2c89a6ea194e595808123c863c38eff"},
]
[package.dependencies]
@@ -9120,4 +9223,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "27121ac9c66b1baa3953f2a41ea15e8585a64ce8d855b8bc92f984050fd92507"
+content-hash = "613a56e7dc5551be660388fb8603f6139dbc5d440ea39f3ba931870dc3234bb4"
diff --git a/pyproject.toml b/pyproject.toml
index c9c694c0a7..7e7d7276b6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "opendevin"
-version = "0.8.2"
+version = "0.8.3"
description = "OpenDevin: Code Less, Make More"
authors = ["OpenDevin"]
license = "MIT"
@@ -34,27 +34,29 @@ minio = "^7.2.7"
gevent = "^24.2.1"
pyarrow = "17.0.0" # transitive dependency, pinned here to avoid conflicts
tenacity = "^8.5.0"
-zope-interface = "6.4.post2"
+zope-interface = "7.0.1"
pathspec = "^0.12.1"
google-cloud-aiplatform = "*"
-grep-ast = "0.3.2"
+grep-ast = "0.3.3"
tree-sitter = "0.21.3"
bashlex = "^0.18"
pyjwt = "^2.9.0"
+dirhash = "*"
[tool.poetry.group.llama-index.dependencies]
llama-index = "*"
llama-index-vector-stores-chroma = "*"
chromadb = "*"
llama-index-embeddings-huggingface = "*"
-torch = "2.2.0"
+torch = "2.2.2"
llama-index-embeddings-azure-openai = "*"
llama-index-embeddings-ollama = "*"
[tool.poetry.group.dev.dependencies]
-ruff = "0.5.5"
+ruff = "0.5.7"
mypy = "1.11.1"
pre-commit = "3.8.0"
+build = "*"
[tool.poetry.group.test.dependencies]
pytest = "*"
@@ -116,3 +118,6 @@ whatthepatch = "*"
retry = "*"
evaluate = "*"
swebench = { git = "https://github.com/OpenDevin/SWE-bench.git" }
+func_timeout = "*"
+sympy = "*"
+gdown = "*"
diff --git a/tests/integration/README.md b/tests/integration/README.md
index 292153611a..390192e973 100644
--- a/tests/integration/README.md
+++ b/tests/integration/README.md
@@ -36,10 +36,11 @@ The folder is organised as follows:
├── README.md
├── conftest.py
├── mock
-│ ├── [AgentName]
-│ │ └── [TestName]
-│ │ ├── prompt_*.log
-│ │ ├── response_*.log
+ ├── [RuntimeType]
+│ | ├── [AgentName]
+│ │ └── [TestName]
+│ │ ├── prompt_*.log
+│ │ ├── response_*.log
└── [TestFiles].py
```
@@ -47,15 +48,19 @@ where `conftest.py` defines the infrastructure needed to load real-world LLM pro
and responses for mocking purpose. Prompts and responses generated during real runs
of agents with real LLMs are stored under `mock/AgentName/TestName` folders.
-**Note:** Set PERSIST_SANDBOX=false to use a clean sandbox for each test.
## Run Integration Tests
Take a look at `ghcr.yml` (in the `.github/workflow` folder) to learn
-how integration tests are launched in a CI environment. You can also simply run:
+how integration tests are launched in a CI environment.
+
+You can run:
```bash
-TEST_ONLY=true ./tests/integration/regenerate.sh
+# for server runtime
+TEST_RUNTIME=server TEST_ONLY=true ./tests/integration/regenerate.sh
+# for event stream
+TEST_RUNTIME=eventstream TEST_ONLY=true ./tests/integration/regenerate.sh
```
to run all integration tests until the first failure occurs.
@@ -75,7 +80,8 @@ When you make changes to an agent's prompt, the integration tests will fail. You
by running the following command from OpenDevin's project root directory:
```bash
-./tests/integration/regenerate.sh
+TEST_RUNTIME=server ./tests/integration/regenerate.sh
+TEST_RUNTIME=eventstream ./tests/integration/regenerate.sh
```
Please note that this will:
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 0d9d84a38b..659f1632c2 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -4,6 +4,7 @@ import re
import shutil
import subprocess
import tempfile
+import time
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
from threading import Thread
@@ -16,10 +17,18 @@ from opendevin.llm.llm import message_separator
script_dir = os.environ.get('SCRIPT_DIR')
project_root = os.environ.get('PROJECT_ROOT')
workspace_path = os.environ.get('WORKSPACE_BASE')
+test_runtime = os.environ.get('TEST_RUNTIME')
+MOCK_ROOT_DIR = os.path.join(
+ script_dir,
+ 'mock',
+ f'{test_runtime}_runtime',
+ os.environ.get('DEFAULT_AGENT'),
+)
assert script_dir is not None, 'SCRIPT_DIR environment variable is not set'
assert project_root is not None, 'PROJECT_ROOT environment variable is not set'
assert workspace_path is not None, 'WORKSPACE_BASE environment variable is not set'
+assert test_runtime is not None, 'TEST_RUNTIME environment variable is not set'
class SecretExit(Exception):
@@ -37,7 +46,19 @@ def pytest_exception_interact(node, call, report):
def filter_out_symbols(input):
+ # remove shell hostname patterns (e.g., will change between each run)
+ # opendevin@379c7fce40b4:/workspace $
+ input = re.sub(r'(opendevin|root)@.*(:/.*)', r'\1[DUMMY_HOSTNAME]\2', input)
+
+ # handle sha256 hashes
+ # sha256=4ecf8be428f55981e2a188f510ba5f9022bed88f5fb404d7d949f44382201e3d
+ input = re.sub(r'sha256=[a-z0-9]+', 'sha256=[DUMMY_HASH]', input)
+
+ # remove newlines and whitespace
input = re.sub(r'\\n|\\r\\n|\\r|\s+', '', input)
+
+ # remove all non-alphanumeric characters
+ input = re.sub(r'[^a-zA-Z0-9]', '', input)
return input
@@ -54,9 +75,7 @@ def apply_prompt_and_get_mock_response(test_name: str, messages: str, id: int) -
Note: this function blindly replaces existing prompt file with the given
input without checking the contents.
"""
- mock_dir = os.path.join(
- script_dir, 'mock', os.environ.get('DEFAULT_AGENT'), test_name
- )
+ mock_dir = os.path.join(MOCK_ROOT_DIR, test_name)
prompt_file_path = os.path.join(mock_dir, f'prompt_{"{0:03}".format(id)}.log')
resp_file_path = os.path.join(mock_dir, f'response_{"{0:03}".format(id)}.log')
try:
@@ -88,16 +107,14 @@ def get_mock_response(test_name: str, messages: str, id: int) -> str:
we start from the end of the file, but again, that is unnecessary and only
makes test code harder to understand.
"""
+ mock_dir = os.path.join(MOCK_ROOT_DIR, test_name)
prompt = filter_out_symbols(messages)
- mock_dir = os.path.join(
- script_dir, 'mock', os.environ.get('DEFAULT_AGENT'), test_name
- )
prompt_file_path = os.path.join(mock_dir, f'prompt_{"{0:03}".format(id)}.log')
resp_file_path = os.path.join(mock_dir, f'response_{"{0:03}".format(id)}.log')
# Open the prompt file and compare its contents
with open(prompt_file_path, 'r') as f:
file_content = filter_out_symbols(f.read())
- if file_content == prompt:
+ if file_content.strip() == prompt.strip():
# Read the response file and return its content
with open(resp_file_path, 'r') as resp_file:
return resp_file.read()
@@ -158,7 +175,9 @@ def mock_completion(*args, test_name, **kwargs):
messages = kwargs['messages']
message_str = ''
for message in messages:
- message_str += message_separator + message['content']
+ for m in message['content']:
+ if m['type'] == 'text':
+ message_str += message_separator + m['text']
# this assumes all response_(*).log filenames are in numerical order, starting from one
cur_id += 1
if os.environ.get('FORCE_APPLY_PROMPTS') == 'true':
@@ -211,7 +230,9 @@ def http_server():
thread = Thread(target=server.serve_forever)
thread.setDaemon(True)
thread.start()
+ time.sleep(1)
+ print('HTTP server started...')
yield server
# Stop the server
diff --git a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_001.log b/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_001.log
deleted file mode 100644
index 541e772d8b..0000000000
--- a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_001.log
+++ /dev/null
@@ -1,4 +0,0 @@
-Let's start by installing the `pymsgbox` package.
-
-
-%pip install pymsgbox==1.0.9
diff --git a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_002.log b/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_002.log
deleted file mode 100644
index 57d9d054a2..0000000000
--- a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_002.log
+++ /dev/null
@@ -1,15 +0,0 @@
-Next, let's import `pymsgbox` and print its version to `/workspace/test.txt`.
-
-
-import pymsgbox
-
-version = pymsgbox.__version__
-
-with open("/workspace/test.txt", "w") as file:
- file.write(f"pymsgbox version: {version}\n")
-
-# Verify the content of the file
-with open("/workspace/test.txt", "r") as file:
- content = file.read()
-
-content
diff --git a/tests/integration/mock/BrowsingAgent/test_browse_internet/prompt_001.log b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/prompt_001.log
similarity index 100%
rename from tests/integration/mock/BrowsingAgent/test_browse_internet/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/prompt_001.log
diff --git a/tests/integration/mock/BrowsingAgent/test_browse_internet/prompt_002.log b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/prompt_002.log
similarity index 100%
rename from tests/integration/mock/BrowsingAgent/test_browse_internet/prompt_002.log
rename to tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/prompt_002.log
diff --git a/tests/integration/mock/BrowsingAgent/test_browse_internet/prompt_003.log b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/prompt_003.log
similarity index 100%
rename from tests/integration/mock/BrowsingAgent/test_browse_internet/prompt_003.log
rename to tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/prompt_003.log
diff --git a/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_001.log b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_001.log
new file mode 100644
index 0000000000..8bdf5efd6c
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_001.log
@@ -0,0 +1,2 @@
+In order to accomplish my goal, I need to navigate to the localhost page.
+```goto('http://localhost:8000'
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/response_003.log b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/response_003.log
rename to tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_002.log
diff --git a/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_003.log b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_003.log
new file mode 100644
index 0000000000..2eeae38382
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/BrowsingAgent/test_browse_internet/response_003.log
@@ -0,0 +1,3 @@
+In order to accomplish my goal, I need to read the static text that reveals the answer to life, the universe, and everything.
+
+```send_msg_to_user('The answer is OpenDevin is all you need!'
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_001.log
new file mode 100644
index 0000000000..62ee7a52b6
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_001.log
@@ -0,0 +1,401 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
+
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_002.log
new file mode 100644
index 0000000000..292bb8cfdc
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_002.log
@@ -0,0 +1,126 @@
+
+
+----------
+
+# 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:
+Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life for you.. I should start with: Get the content on "http://localhost:8000"
+
+# Action Space
+
+16 different types of actions are available.
+
+noop(wait_ms: float = 1000)
+ Examples:
+ noop()
+
+ noop(500)
+
+send_msg_to_user(text: str)
+ Examples:
+ send_msg_to_user('Based on the results of my search, the city was built in 1751.')
+
+scroll(delta_x: float, delta_y: float)
+ Examples:
+ scroll(0, 200)
+
+ scroll(-50.2, -100.5)
+
+fill(bid: str, value: str)
+ Examples:
+ fill('237', 'example value')
+
+ fill('45', 'multi-line\nexample')
+
+ fill('a12', 'example with "quotes"')
+
+select_option(bid: str, options: str | list[str])
+ Examples:
+ select_option('48', 'blue')
+
+ select_option('48', ['red', 'green', 'blue'])
+
+click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ click('51')
+
+ click('b22', button='right')
+
+ click('48', button='middle', modifiers=['Shift'])
+
+dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ dblclick('12')
+
+ dblclick('ca42', button='right')
+
+ dblclick('178', button='middle', modifiers=['Shift'])
+
+hover(bid: str)
+ Examples:
+ hover('b8')
+
+press(bid: str, key_comb: str)
+ Examples:
+ press('88', 'Backspace')
+
+ press('a26', 'Control+a')
+
+ press('a61', 'Meta+Shift+t')
+
+focus(bid: str)
+ Examples:
+ focus('b455')
+
+clear(bid: str)
+ Examples:
+ clear('996')
+
+drag_and_drop(from_bid: str, to_bid: str)
+ Examples:
+ drag_and_drop('56', '498')
+
+upload_file(bid: str, file: str | list[str])
+ Examples:
+ upload_file('572', 'my_receipt.pdf')
+
+ upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
+
+go_back()
+ Examples:
+ go_back()
+
+go_forward()
+ Examples:
+ go_forward()
+
+goto(url: str)
+ Examples:
+ goto('http://www.example.com')
+
+Multiple actions can be provided at once. Example:
+fill('a12', 'example with "quotes"')
+click('51')
+click('48', button='middle', modifiers=['Shift'])
+Multiple actions are meant to be executed sequentially without any feedback from the page.
+Don't execute multiple actions at once if you need feedback from the page.
+
+
+
+----------
+
+# Current Accessibility Tree:
+
+
+# Previous 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")```
+"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_003.log
new file mode 100644
index 0000000000..991b967387
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_003.log
@@ -0,0 +1,130 @@
+
+
+----------
+
+# 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:
+Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life for you.. I should start with: Get the content on "http://localhost:8000"
+
+# Action Space
+
+16 different types of actions are available.
+
+noop(wait_ms: float = 1000)
+ Examples:
+ noop()
+
+ noop(500)
+
+send_msg_to_user(text: str)
+ Examples:
+ send_msg_to_user('Based on the results of my search, the city was built in 1751.')
+
+scroll(delta_x: float, delta_y: float)
+ Examples:
+ scroll(0, 200)
+
+ scroll(-50.2, -100.5)
+
+fill(bid: str, value: str)
+ Examples:
+ fill('237', 'example value')
+
+ fill('45', 'multi-line\nexample')
+
+ fill('a12', 'example with "quotes"')
+
+select_option(bid: str, options: str | list[str])
+ Examples:
+ select_option('48', 'blue')
+
+ select_option('48', ['red', 'green', 'blue'])
+
+click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ click('51')
+
+ click('b22', button='right')
+
+ click('48', button='middle', modifiers=['Shift'])
+
+dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ dblclick('12')
+
+ dblclick('ca42', button='right')
+
+ dblclick('178', button='middle', modifiers=['Shift'])
+
+hover(bid: str)
+ Examples:
+ hover('b8')
+
+press(bid: str, key_comb: str)
+ Examples:
+ press('88', 'Backspace')
+
+ press('a26', 'Control+a')
+
+ press('a61', 'Meta+Shift+t')
+
+focus(bid: str)
+ Examples:
+ focus('b455')
+
+clear(bid: str)
+ Examples:
+ clear('996')
+
+drag_and_drop(from_bid: str, to_bid: str)
+ Examples:
+ drag_and_drop('56', '498')
+
+upload_file(bid: str, file: str | list[str])
+ Examples:
+ upload_file('572', 'my_receipt.pdf')
+
+ upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
+
+go_back()
+ Examples:
+ go_back()
+
+go_forward()
+ Examples:
+ go_forward()
+
+goto(url: str)
+ Examples:
+ goto('http://www.example.com')
+
+Multiple actions can be provided at once. Example:
+fill('a12', 'example with "quotes"')
+click('51')
+click('48', button='middle', modifiers=['Shift'])
+Multiple actions are meant to be executed sequentially without any feedback from the page.
+Don't execute multiple actions at once if you need feedback from the page.
+
+
+
+----------
+
+# Current Accessibility Tree:
+RootWebArea 'The Ultimate Answer', focused
+ [8] heading 'The Ultimate Answer'
+ [9] paragraph ''
+ StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
+ [10] button 'Click me', clickable
+
+# Previous Actions
+goto("http://localhost:8000")
+
+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")```
+"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_004.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_004.log
new file mode 100644
index 0000000000..566df42a71
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_004.log
@@ -0,0 +1,132 @@
+
+
+----------
+
+# 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:
+Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life for you.. I should start with: Get the content on "http://localhost:8000"
+
+# Action Space
+
+16 different types of actions are available.
+
+noop(wait_ms: float = 1000)
+ Examples:
+ noop()
+
+ noop(500)
+
+send_msg_to_user(text: str)
+ Examples:
+ send_msg_to_user('Based on the results of my search, the city was built in 1751.')
+
+scroll(delta_x: float, delta_y: float)
+ Examples:
+ scroll(0, 200)
+
+ scroll(-50.2, -100.5)
+
+fill(bid: str, value: str)
+ Examples:
+ fill('237', 'example value')
+
+ fill('45', 'multi-line\nexample')
+
+ fill('a12', 'example with "quotes"')
+
+select_option(bid: str, options: str | list[str])
+ Examples:
+ select_option('48', 'blue')
+
+ select_option('48', ['red', 'green', 'blue'])
+
+click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ click('51')
+
+ click('b22', button='right')
+
+ click('48', button='middle', modifiers=['Shift'])
+
+dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ dblclick('12')
+
+ dblclick('ca42', button='right')
+
+ dblclick('178', button='middle', modifiers=['Shift'])
+
+hover(bid: str)
+ Examples:
+ hover('b8')
+
+press(bid: str, key_comb: str)
+ Examples:
+ press('88', 'Backspace')
+
+ press('a26', 'Control+a')
+
+ press('a61', 'Meta+Shift+t')
+
+focus(bid: str)
+ Examples:
+ focus('b455')
+
+clear(bid: str)
+ Examples:
+ clear('996')
+
+drag_and_drop(from_bid: str, to_bid: str)
+ Examples:
+ drag_and_drop('56', '498')
+
+upload_file(bid: str, file: str | list[str])
+ Examples:
+ upload_file('572', 'my_receipt.pdf')
+
+ upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
+
+go_back()
+ Examples:
+ go_back()
+
+go_forward()
+ Examples:
+ go_forward()
+
+goto(url: str)
+ Examples:
+ goto('http://www.example.com')
+
+Multiple actions can be provided at once. Example:
+fill('a12', 'example with "quotes"')
+click('51')
+click('48', button='middle', modifiers=['Shift'])
+Multiple actions are meant to be executed sequentially without any feedback from the page.
+Don't execute multiple actions at once if you need feedback from the page.
+
+
+
+----------
+
+# Current Accessibility Tree:
+RootWebArea 'The Ultimate Answer', focused
+ [8] heading 'The Ultimate Answer'
+ [9] paragraph ''
+ StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
+ [10] button 'Click me', clickable, focused
+ StaticText 'The answer is OpenDevin is all you need!'
+
+# Previous Actions
+goto("http://localhost:8000")
+click("10")
+
+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")```
+"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_005.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_005.log
new file mode 100644
index 0000000000..48ebcb32b8
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/prompt_005.log
@@ -0,0 +1,415 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
+
+----------
+
+
+
+Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life for you.. I should start with: Get the content on "http://localhost:8000"
+
+
+----------
+
+OBSERVATION:
+{'content': 'The ultimate answer to life, the universe, and everything is: OpenDevin is all you need!'}
+
+ENVIRONMENT REMINDER: You have 8 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_001.log
new file mode 100644
index 0000000000..25975d7d13
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_001.log
@@ -0,0 +1,4 @@
+Certainly! I'll browse localhost:8000 and retrieve the ultimate answer to life for you.
+
+
+Get the content on "http://localhost:8000"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_002.log
new file mode 100644
index 0000000000..05dffd72f4
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_002.log
@@ -0,0 +1,5 @@
+To accomplish our goal of retrieving the ultimate answer to life from localhost:8000, I need to start by accessing the content on that page. Here's my thought process and action:
+
+In order to accomplish my goal, I need to navigate to the specified URL using the goto action.
+
+```goto("http://localhost:8000"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_003.log
new file mode 100644
index 0000000000..66e87a3550
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_003.log
@@ -0,0 +1,6 @@
+To accomplish our goal of retrieving the ultimate answer to life, we need to click on the "Click me" button. The button has a bid of 10, so we should click on it.
+
+Here's the action I'll take:
+
+In order to accomplish my goal, I need to click on the button with bid 10
+```click("10"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_004.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_004.log
new file mode 100644
index 0000000000..ae88605a49
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_004.log
@@ -0,0 +1,6 @@
+Based on the current Accessibility Tree and the previous actions taken, it appears that we have already successfully navigated to the webpage and clicked the button to reveal the answer. The answer is now visible on the page.
+
+To accomplish our goal of retrieving the ultimate answer to life, I will now send this information to the user:
+
+```
+send_msg_to_user("The ultimate answer to life, the universe, and everything is: OpenDevin is all you need!"
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_005.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_005.log
new file mode 100644
index 0000000000..13e8a93af0
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_browse_internet/response_005.log
@@ -0,0 +1,11 @@
+Thank you for providing the content from localhost:8000. Based on the information retrieved, I can now answer your question about the ultimate answer to life.
+
+The ultimate answer to life, according to the content on localhost:8000, is:
+
+"OpenDevin is all you need!"
+
+This appears to be a playful reference to the popular science fiction series "The Hitchhiker's Guide to the Galaxy" by Douglas Adams, where the answer to the ultimate question of life, the universe, and everything is famously "42". In this case, it's been adapted to promote OpenDevin as the ultimate solution.
+
+Since I've successfully retrieved and reported the answer from the specified localhost, I believe I've completed the task you've given me.
+
+
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_001.log
new file mode 100644
index 0000000000..8f33d2e2f2
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_001.log
@@ -0,0 +1,401 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_002.log
new file mode 100644
index 0000000000..f33b77e595
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_002.log
@@ -0,0 +1,422 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! Let's start by opening the `bad.txt` file to identify the typos.
+
+open_file('bad.txt')
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total)]
+(this is the beginning of the file)
+1|This is a stupid typoo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_003.log
new file mode 100644
index 0000000000..14ed925795
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_003.log
@@ -0,0 +1,451 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! Let's start by opening the `bad.txt` file to identify the typos.
+
+open_file('bad.txt')
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total)]
+(this is the beginning of the file)
+1|This is a stupid typoo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+
+
+----------
+
+I will correct the typos in the `bad.txt` file.
+
+1. "stupid typoo" -> "stupid typo"
+2. "No mor typos!" -> "No more typos!"
+
+Let's make these changes.
+
+edit_file_by_replace(
+ 'bad.txt',
+ to_replace="This is a stupid typoo.",
+ new_content="This is a stupid typo."
+)
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total after edit)]
+(this is the beginning of the file)
+1|This is a stupid typo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_004.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_004.log
new file mode 100644
index 0000000000..80b43b7dc6
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/prompt_004.log
@@ -0,0 +1,479 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! Let's start by opening the `bad.txt` file to identify the typos.
+
+open_file('bad.txt')
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total)]
+(this is the beginning of the file)
+1|This is a stupid typoo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+
+
+----------
+
+I will correct the typos in the `bad.txt` file.
+
+1. "stupid typoo" -> "stupid typo"
+2. "No mor typos!" -> "No more typos!"
+
+Let's make these changes.
+
+edit_file_by_replace(
+ 'bad.txt',
+ to_replace="This is a stupid typoo.",
+ new_content="This is a stupid typo."
+)
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total after edit)]
+(this is the beginning of the file)
+1|This is a stupid typo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+
+----------
+
+Let's correct the remaining typo in the file.
+
+1. "No mor typos!" -> "No more typos!"
+
+I will make this change now.
+
+edit_file_by_replace(
+ 'bad.txt',
+ to_replace="No mor typos!",
+ new_content="No more typos!"
+)
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total after edit)]
+(this is the beginning of the file)
+1|This is a stupid typo.
+2|Really?
+3|No more typos!
+4|Enjoy!
+(this is the end of the file)
+[File updated (edited at line 3). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_001.log
new file mode 100644
index 0000000000..58fb6048ba
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_001.log
@@ -0,0 +1,4 @@
+Sure! Let's start by opening the `bad.txt` file to identify the typos.
+
+
+open_file('bad.txt')
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_002.log
new file mode 100644
index 0000000000..b83d070ecd
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_002.log
@@ -0,0 +1,13 @@
+I will correct the typos in the `bad.txt` file.
+
+1. "stupid typoo" -> "stupid typo"
+2. "No mor typos!" -> "No more typos!"
+
+Let's make these changes.
+
+
+edit_file_by_replace(
+ 'bad.txt',
+ to_replace="This is a stupid typoo.",
+ new_content="This is a stupid typo."
+)
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_003.log
new file mode 100644
index 0000000000..c5096a8785
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_003.log
@@ -0,0 +1,11 @@
+Let's correct the remaining typo in the file.
+
+1. "No mor typos!" -> "No more typos!"
+
+I will make this change now.
+
+edit_file_by_replace(
+ 'bad.txt',
+ to_replace="No mor typos!",
+ new_content="No more typos!"
+)
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_004.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_004.log
new file mode 100644
index 0000000000..4fee4faa5e
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_edits/response_004.log
@@ -0,0 +1,11 @@
+The typos in `bad.txt` have been corrected. Here is the final content of the file:
+
+```
+1|This is a stupid typo.
+2|Really?
+3|No more typos!
+4|Enjoy!
+```
+
+If you have any further requests, feel free to ask!
+
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_001.log
new file mode 100644
index 0000000000..c620c89184
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_001.log
@@ -0,0 +1,401 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
+
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_002.log
new file mode 100644
index 0000000000..013180481a
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_002.log
@@ -0,0 +1,416 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! I will create a text file named `test.txt` in the `/workspace` directory and write 'hello world' to it.
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write('hello world')
+
+
+----------
+
+OBSERVATION:
+[Code executed successfully with no output]
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/response_001.log
new file mode 100644
index 0000000000..ea1ad97051
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/response_001.log
@@ -0,0 +1,5 @@
+Sure! I will create a text file named `test.txt` in the `/workspace` directory and write 'hello world' to it.
+
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write('hello world')
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/response_002.log
new file mode 100644
index 0000000000..42790c2d4a
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/response_002.log
@@ -0,0 +1,3 @@
+The file `test.txt` has been successfully created in the `/workspace` directory with the content 'hello world'. If you have any further requests, please let me know!
+
+
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_001.log
new file mode 100644
index 0000000000..3ecc273f58
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_001.log
@@ -0,0 +1,401 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_002.log
new file mode 100644
index 0000000000..54f39c635d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_002.log
@@ -0,0 +1,431 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! Let's start by installing the `pymsgbox` package.
+
+%pip install pymsgbox==1.0.9
+
+
+----------
+
+OBSERVATION:
+Collecting pymsgbox==1.0.9
+ Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
+ Installing build dependencies ... [?25l- \ | / done
+[?25h Getting requirements to build wheel ... [?25l- done
+[?25h Preparing metadata (pyproject.toml) ... [?25l- done
+[?25hBuilding wheels for collected packages: pymsgbox
+ Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
+[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=7cc4ff18827e3c2e2bd0780a92ca70fd625dc7645b994b6f3191dfb0660e606e
+ Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
+Successfully built pymsgbox
+Installing collected packages: pymsgbox
+Successfully installed pymsgbox-1.0.9
+
+[notice] A new release of pip is available: 24.1 -> 24.2
+[notice] To update, run: pip install --upgrade pip
+Note: you may need to restart the kernel to use updated packages.
+
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_003.log
new file mode 100644
index 0000000000..2d2e459218
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_003.log
@@ -0,0 +1,448 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! Let's start by installing the `pymsgbox` package.
+
+%pip install pymsgbox==1.0.9
+
+
+----------
+
+OBSERVATION:
+Collecting pymsgbox==1.0.9
+ Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
+ Installing build dependencies ... [?25l- \ | / done
+[?25h Getting requirements to build wheel ... [?25l- done
+[?25h Preparing metadata (pyproject.toml) ... [?25l- done
+[?25hBuilding wheels for collected packages: pymsgbox
+ Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
+[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=7cc4ff18827e3c2e2bd0780a92ca70fd625dc7645b994b6f3191dfb0660e606e
+ Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
+Successfully built pymsgbox
+Installing collected packages: pymsgbox
+Successfully installed pymsgbox-1.0.9
+
+[notice] A new release of pip is available: 24.1 -> 24.2
+[notice] To update, run: pip install --upgrade pip
+Note: you may need to restart the kernel to use updated packages.
+
+
+----------
+
+Now, let's import `pymsgbox`, get its version, and write it to `/workspace/test.txt`.
+
+import pymsgbox
+
+version = pymsgbox.__version__
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write(f"pymsgbox version: {version}\n")
+
+
+----------
+
+OBSERVATION:
+[Code executed successfully with no output]
+
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_004.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_004.log
new file mode 100644
index 0000000000..043c096f61
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_004.log
@@ -0,0 +1,463 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! Let's start by installing the `pymsgbox` package.
+
+%pip install pymsgbox==1.0.9
+
+
+----------
+
+OBSERVATION:
+Collecting pymsgbox==1.0.9
+ Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
+ Installing build dependencies ... [?25l- \ | / done
+[?25h Getting requirements to build wheel ... [?25l- done
+[?25h Preparing metadata (pyproject.toml) ... [?25l- done
+[?25hBuilding wheels for collected packages: pymsgbox
+ Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
+[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=7cc4ff18827e3c2e2bd0780a92ca70fd625dc7645b994b6f3191dfb0660e606e
+ Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
+Successfully built pymsgbox
+Installing collected packages: pymsgbox
+Successfully installed pymsgbox-1.0.9
+
+[notice] A new release of pip is available: 24.1 -> 24.2
+[notice] To update, run: pip install --upgrade pip
+Note: you may need to restart the kernel to use updated packages.
+
+
+----------
+
+Now, let's import `pymsgbox`, get its version, and write it to `/workspace/test.txt`.
+
+import pymsgbox
+
+version = pymsgbox.__version__
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write(f"pymsgbox version: {version}\n")
+
+
+----------
+
+OBSERVATION:
+[Code executed successfully with no output]
+
+----------
+
+Let's verify the content of `/workspace/test.txt` to ensure the version information was written correctly.
+
+cat /workspace/test.txt
+
+
+----------
+
+OBSERVATION:
+pymsgbox version: 1.0.9
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_001.log
new file mode 100644
index 0000000000..3d96c1fdb2
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_001.log
@@ -0,0 +1,4 @@
+Sure! Let's start by installing the `pymsgbox` package.
+
+
+%pip install pymsgbox==1.0.9
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_002.log
new file mode 100644
index 0000000000..1886ba712d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_002.log
@@ -0,0 +1,9 @@
+Now, let's import `pymsgbox`, get its version, and write it to `/workspace/test.txt`.
+
+
+import pymsgbox
+
+version = pymsgbox.__version__
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write(f"pymsgbox version: {version}\n")
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_003.log
new file mode 100644
index 0000000000..26fa94b072
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_003.log
@@ -0,0 +1,4 @@
+Let's verify the content of `/workspace/test.txt` to ensure the version information was written correctly.
+
+
+cat /workspace/test.txt
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_004.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_004.log
new file mode 100644
index 0000000000..bbebebe77e
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/response_004.log
@@ -0,0 +1,3 @@
+The version of `pymsgbox` has been successfully written to `/workspace/test.txt`. If you have any further tasks or questions, feel free to ask!
+
+
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_001.log
new file mode 100644
index 0000000000..ddb157a1b0
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_001.log
@@ -0,0 +1,401 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_002.log
new file mode 100644
index 0000000000..a165da2063
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_002.log
@@ -0,0 +1,422 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! I will create a shell script named `hello.sh` that prints "hello".
+
+Let's start by creating the file and adding the necessary content.
+
+echo -e '#!/bin/bash\n\necho "hello"' > hello.sh
+chmod +x hello.sh
+
+
+----------
+
+OBSERVATION:
+
+opendevin@docker-desktop:/workspace $ chmod +x hello.sh
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_003.log
new file mode 100644
index 0000000000..cd5bec9cba
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/prompt_003.log
@@ -0,0 +1,437 @@
+
+
+----------
+
+A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
+The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with .
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+
+For example, you can list the files in the current directory by ls .
+Important, however: do not run interactive commands. You do not have access to stdin.
+Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
+For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: python3 app.py > server.log 2>&1 &
+Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
+The assistant can browse the Internet with and .
+For example, Tell me the usa's president using google search .
+Or Tell me what is in http://example.com .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+To interact with GitHub, use the $GITHUB_TOKEN environment variable.
+For example, to push a branch `my_branch` to the GitHub repo `owner/repo`:
+ git push https://$GITHUB_TOKEN@github.com/owner/repo.git my_branch
+If $GITHUB_TOKEN is not set, ask the user to set it.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
+
+Responses should be concise.
+The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
+Include ONLY ONE , , or per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
+If the assistant is finished with the task you MUST include in your response.
+IMPORTANT: Execute code using , , or whenever possible.
+When handling files, try to use full paths and pwd to avoid errors.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
+
+ASSISTANT:
+Sure! Let me create a Python file `app.py`:
+
+create_file('app.py')
+
+
+USER:
+OBSERVATION:
+[File: /workspace/app.py (1 lines total)]
+(this is the beginning of the file)
+1|
+(this is the end of the file)
+[File app.py created.]
+
+ASSISTANT:
+Now I will write the Python code for starting a web server and save it to the file `app.py`:
+
+EDITED_CODE="""from flask import Flask
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ numbers = list(range(1, 11))
+ return str(numbers)
+
+if __name__ == '__main__':
+ app.run(port=5000)"""
+
+insert_content_at_line(
+ 'app.py',
+ 1,
+ EDITED_CODE,
+)
+
+
+USER:
+OBSERVATION:
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+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:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 121[1]+ Exit 1 python3 app.py > server.log 2>&1
+
+ASSISTANT: Looks like the server is running with PID 121 then crashed. Let me check the server log:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+Traceback (most recent call last):
+ File "/workspace/app.py", line 2, in
+ from flask import Flask
+ModuleNotFoundError: No module named 'flask'
+
+ASSISTANT:
+It seems that Flask is not installed. Let me install Flask for you:
+
+pip install flask
+
+
+USER:
+OBSERVATION:
+Defaulting to user installation because normal site-packages is not writeable
+Collecting flask
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
+Collecting blinker>=1.6.2
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
+Collecting Werkzeug>=3.0.0
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
+Collecting click>=8.1.3
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
+Collecting itsdangerous>=2.1.2
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+Requirement already satisfied: Jinja2>=3.1.2 in /home/opendevin/.local/lib/python3.10/site-packages (from flask) (3.1.3)
+Requirement already satisfied: MarkupSafe>=2.0 in /home/opendevin/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
+Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
+Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
+
+ASSISTANT:
+Now that Flask is installed, let me run the Python file again:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+OBSERVATION:
+[1] 124
+
+ASSISTANT:
+Let me check the server log again:
+
+cat server.log
+
+
+USER:
+OBSERVATION:
+* Serving Flask app 'app'
+ * Debug mode: off
+WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
+ * Running on http://127.0.0.1:5000
+Press CTRL+C to quit
+
+ASSISTANT:
+The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
+
+USER: Now 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:
+
+Get the content on "http://127.0.0.1:5000"
+
+
+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:
+Sure! Let me stop the server first:
+
+kill 124
+
+
+USER:
+OBSERVATION:
+[1]+ Terminated python3 app.py > server.log 2>&1
+
+ASSISTANT:
+The server has been stopped. Let me open the Python file and modify it.
+
+open_file('app.py')
+
+
+USER:
+[File: /workspace/app.py (10 lines total)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return str(numbers)
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+
+ASSISTANT:
+I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
+
+edit_file_by_replace(
+ 'app.py',
+ to_replace=" return str(numbers)",
+ new_content=" return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'",
+)
+
+
+USER:
+Observation:
+[File: /workspace/app.py (10 lines total after edit)]
+(this is the beginning of the file)
+1|from flask import Flask
+2|app = Flask(__name__)
+3|
+4|@app.route('/')
+5|def index():
+6| numbers = list(range(1, 11))
+7| return '' + ''.join([f'| {i} |
' for i in numbers]) + '
'
+8|
+9|if __name__ == '__main__':
+10| app.run(port=5000)
+(this is the end of the file)
+[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+ASSISTANT:
+Running the updated file:
+
+python3 app.py > server.log 2>&1 &
+
+
+USER:
+Observation:
+[1] 126
+
+ASSISTANT:
+The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+----------
+
+Sure! I will create a shell script named `hello.sh` that prints "hello".
+
+Let's start by creating the file and adding the necessary content.
+
+echo -e '#!/bin/bash\n\necho "hello"' > hello.sh
+chmod +x hello.sh
+
+
+----------
+
+OBSERVATION:
+
+opendevin@docker-desktop:/workspace $ chmod +x hello.sh
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+----------
+
+The shell script `hello.sh` has been created and made executable. Let's run it to ensure it prints "hello".
+
+./hello.sh
+
+
+----------
+
+OBSERVATION:
+hello
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_001.log
new file mode 100644
index 0000000000..774f8cbb2a
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_001.log
@@ -0,0 +1,7 @@
+Sure! I will create a shell script named `hello.sh` that prints "hello".
+
+Let's start by creating the file and adding the necessary content.
+
+
+echo -e '#!/bin/bash\n\necho "hello"' > hello.sh
+chmod +x hello.sh
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_002.log
new file mode 100644
index 0000000000..a5de32570c
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_002.log
@@ -0,0 +1,4 @@
+The shell script `hello.sh` has been created and made executable. Let's run it to ensure it prints "hello".
+
+
+./hello.sh
diff --git a/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_003.log
new file mode 100644
index 0000000000..8804422f34
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActAgent/test_write_simple_script/response_003.log
@@ -0,0 +1,3 @@
+The shell script `hello.sh` successfully prints "hello". If you have any further tasks or questions, feel free to ask!
+
+
diff --git a/tests/integration/mock/CodeActSWEAgent/test_edits/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_001.log
similarity index 99%
rename from tests/integration/mock/CodeActSWEAgent/test_edits/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_001.log
index 0d198ce1f0..75f5cea76e 100644
--- a/tests/integration/mock/CodeActSWEAgent/test_edits/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_001.log
@@ -591,4 +591,4 @@ NOW, LET'S START!
Fix typos in bad.txt. Do not ask me for confirmation at any point.
-ENVIRONMENT REMINDER: You have 14 turns left to complete the task.
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActSWEAgent/test_edits/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_002.log
similarity index 99%
rename from tests/integration/mock/CodeActSWEAgent/test_edits/prompt_002.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_002.log
index 4f046938ce..8d973b5384 100644
--- a/tests/integration/mock/CodeActSWEAgent/test_edits/prompt_002.log
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_002.log
@@ -601,13 +601,13 @@ open_file("bad.txt")
----------
OBSERVATION:
-[File: /workspace/bad.txt (4 lines total)]
-(this is the beginning of the file)
-1|This is a stupid typoo.
-2|Really?
-3|No mor typos!
-4|Enjoy!
-(this is the end of the file)
+[File: /workspace/bad.txt (4 lines total)]
+(this is the beginning of the file)
+1|This is a stupid typoo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task.
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_003.log
new file mode 100644
index 0000000000..1387066cd3
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_003.log
@@ -0,0 +1,642 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Let's start by opening the `bad.txt` file to identify and fix the typos.
+
+open_file("bad.txt")
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total)]
+(this is the beginning of the file)
+1|This is a stupid typoo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+
+
+----------
+
+Let's correct the typos in the `bad.txt` file.
+
+1. "typoo" should be "typo".
+2. "mor" should be "more".
+
+I will make these corrections now.
+
+edit_file_by_replace(
+ "bad.txt",
+ to_replace="This is a stupid typoo.",
+ new_content="This is a stupid typo."
+)
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total after edit)]
+(this is the beginning of the file)
+1|This is a stupid typo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_004.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_004.log
new file mode 100644
index 0000000000..7a80c3b40d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/prompt_004.log
@@ -0,0 +1,666 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Let's start by opening the `bad.txt` file to identify and fix the typos.
+
+open_file("bad.txt")
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total)]
+(this is the beginning of the file)
+1|This is a stupid typoo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+
+
+----------
+
+Let's correct the typos in the `bad.txt` file.
+
+1. "typoo" should be "typo".
+2. "mor" should be "more".
+
+I will make these corrections now.
+
+edit_file_by_replace(
+ "bad.txt",
+ to_replace="This is a stupid typoo.",
+ new_content="This is a stupid typo."
+)
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total after edit)]
+(this is the beginning of the file)
+1|This is a stupid typo.
+2|Really?
+3|No mor typos!
+4|Enjoy!
+(this is the end of the file)
+[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+
+----------
+
+Let's proceed with correcting the second typo.
+
+edit_file_by_replace(
+ "bad.txt",
+ to_replace="No mor typos!",
+ new_content="No more typos!"
+)
+
+
+----------
+
+OBSERVATION:
+[File: /workspace/bad.txt (4 lines total after edit)]
+(this is the beginning of the file)
+1|This is a stupid typo.
+2|Really?
+3|No more typos!
+4|Enjoy!
+(this is the end of the file)
+[File updated (edited at line 3). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+
+
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActSWEAgent/test_edits/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_001.log
similarity index 100%
rename from tests/integration/mock/CodeActSWEAgent/test_edits/response_001.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_001.log
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_002.log
new file mode 100644
index 0000000000..2052a2319f
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_002.log
@@ -0,0 +1,13 @@
+Let's correct the typos in the `bad.txt` file.
+
+1. "typoo" should be "typo".
+2. "mor" should be "more".
+
+I will make these corrections now.
+
+
+edit_file_by_replace(
+ "bad.txt",
+ to_replace="This is a stupid typoo.",
+ new_content="This is a stupid typo."
+)
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_003.log
new file mode 100644
index 0000000000..9fa5f7a924
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_003.log
@@ -0,0 +1,8 @@
+Let's proceed with correcting the second typo.
+
+
+edit_file_by_replace(
+ "bad.txt",
+ to_replace="No mor typos!",
+ new_content="No more typos!"
+)
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_004.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_004.log
new file mode 100644
index 0000000000..74e10ef11e
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_edits/response_004.log
@@ -0,0 +1,10 @@
+The typos in `bad.txt` have been corrected successfully. The file now reads:
+
+```
+1|This is a stupid typo.
+2|Really?
+3|No more typos!
+4|Enjoy!
+```
+
+The task is complete.
diff --git a/tests/integration/mock/CodeActSWEAgent/test_ipython/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_001.log
similarity index 99%
rename from tests/integration/mock/CodeActSWEAgent/test_ipython/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_001.log
index 69a60d8dbb..04d267654d 100644
--- a/tests/integration/mock/CodeActSWEAgent/test_ipython/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_001.log
@@ -591,4 +591,4 @@ NOW, LET'S START!
Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
-ENVIRONMENT REMINDER: You have 14 turns left to complete the task.
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_002.log
new file mode 100644
index 0000000000..e0d98a986b
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_002.log
@@ -0,0 +1,607 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
+
+----------
+
+Understood. I will create a text file named `test.txt` in the `/workspace` directory and write "hello world" to it.
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write('hello world')
+
+
+----------
+
+OBSERVATION:
+[Code executed successfully with no output]
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/response_001.log
new file mode 100644
index 0000000000..1158c7999d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/response_001.log
@@ -0,0 +1,5 @@
+Understood. I will create a text file named `test.txt` in the `/workspace` directory and write "hello world" to it.
+
+
+with open('/workspace/test.txt', 'w') as file:
+ file.write('hello world')
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/response_002.log
new file mode 100644
index 0000000000..e82588acef
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/response_002.log
@@ -0,0 +1 @@
+The file `test.txt` has been successfully created in the `/workspace` directory with the content "hello world". If you need any further assistance, please let me know!
diff --git a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_001.log
similarity index 99%
rename from tests/integration/mock/CodeActSWEAgent/test_ipython_module/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_001.log
index 65bbb95fc3..a7073b5af5 100644
--- a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_001.log
@@ -591,4 +591,4 @@ NOW, LET'S START!
Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
-ENVIRONMENT REMINDER: You have 14 turns left to complete the task.
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_002.log
new file mode 100644
index 0000000000..16fc340733
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_002.log
@@ -0,0 +1,622 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Understood. Let's start by installing the `pymsgbox` package.
+
+%pip install pymsgbox==1.0.9
+
+
+----------
+
+OBSERVATION:
+Collecting pymsgbox==1.0.9
+ Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
+ Installing build dependencies ... [?25l- \ | / - \ | / done
+[?25h Getting requirements to build wheel ... [?25l- done
+[?25h Preparing metadata (pyproject.toml) ... [?25l- done
+[?25hBuilding wheels for collected packages: pymsgbox
+ Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
+[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e504d1b40c05b7da59bba1f908d23edcd98381d8e0ecc41a1162745ee4ee6fd2
+ Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
+Successfully built pymsgbox
+Installing collected packages: pymsgbox
+Successfully installed pymsgbox-1.0.9
+
+[notice] A new release of pip is available: 24.1 -> 24.2
+[notice] To update, run: pip install --upgrade pip
+Note: you may need to restart the kernel to use updated packages.
+
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_003.log
new file mode 100644
index 0000000000..9fc3c047cd
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_003.log
@@ -0,0 +1,641 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Understood. Let's start by installing the `pymsgbox` package.
+
+%pip install pymsgbox==1.0.9
+
+
+----------
+
+OBSERVATION:
+Collecting pymsgbox==1.0.9
+ Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
+ Installing build dependencies ... [?25l- \ | / - \ | / done
+[?25h Getting requirements to build wheel ... [?25l- done
+[?25h Preparing metadata (pyproject.toml) ... [?25l- done
+[?25hBuilding wheels for collected packages: pymsgbox
+ Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
+[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e504d1b40c05b7da59bba1f908d23edcd98381d8e0ecc41a1162745ee4ee6fd2
+ Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
+Successfully built pymsgbox
+Installing collected packages: pymsgbox
+Successfully installed pymsgbox-1.0.9
+
+[notice] A new release of pip is available: 24.1 -> 24.2
+[notice] To update, run: pip install --upgrade pip
+Note: you may need to restart the kernel to use updated packages.
+
+
+----------
+
+Now, let's import `pymsgbox`, print its version, and write the output to `/workspace/test.txt`.
+
+import pymsgbox
+
+version_info = pymsgbox.__version__
+
+with open("/workspace/test.txt", "w") as file:
+ file.write(f"pymsgbox version: {version_info}\n")
+
+version_info
+
+
+----------
+
+OBSERVATION:
+'1.0.9'
+
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_004.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_004.log
new file mode 100644
index 0000000000..815ffa8585
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_004.log
@@ -0,0 +1,656 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
+
+----------
+
+Understood. Let's start by installing the `pymsgbox` package.
+
+%pip install pymsgbox==1.0.9
+
+
+----------
+
+OBSERVATION:
+Collecting pymsgbox==1.0.9
+ Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
+ Installing build dependencies ... [?25l- \ | / - \ | / done
+[?25h Getting requirements to build wheel ... [?25l- done
+[?25h Preparing metadata (pyproject.toml) ... [?25l- done
+[?25hBuilding wheels for collected packages: pymsgbox
+ Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
+[?25h Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e504d1b40c05b7da59bba1f908d23edcd98381d8e0ecc41a1162745ee4ee6fd2
+ Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
+Successfully built pymsgbox
+Installing collected packages: pymsgbox
+Successfully installed pymsgbox-1.0.9
+
+[notice] A new release of pip is available: 24.1 -> 24.2
+[notice] To update, run: pip install --upgrade pip
+Note: you may need to restart the kernel to use updated packages.
+
+
+----------
+
+Now, let's import `pymsgbox`, print its version, and write the output to `/workspace/test.txt`.
+
+import pymsgbox
+
+version_info = pymsgbox.__version__
+
+with open("/workspace/test.txt", "w") as file:
+ file.write(f"pymsgbox version: {version_info}\n")
+
+version_info
+
+
+----------
+
+OBSERVATION:
+'1.0.9'
+
+----------
+
+The version of `pymsgbox` has been successfully written to `/workspace/test.txt`. Let's verify the content of the file to ensure everything is correct.
+
+cat /workspace/test.txt
+
+
+----------
+
+OBSERVATION:
+pymsgbox version: 1.0.9
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_001.log
new file mode 100644
index 0000000000..f0dcb7ab6d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_001.log
@@ -0,0 +1,4 @@
+Understood. Let's start by installing the `pymsgbox` package.
+
+
+%pip install pymsgbox==1.0.9
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_002.log
new file mode 100644
index 0000000000..ae6944507f
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_002.log
@@ -0,0 +1,11 @@
+Now, let's import `pymsgbox`, print its version, and write the output to `/workspace/test.txt`.
+
+
+import pymsgbox
+
+version_info = pymsgbox.__version__
+
+with open("/workspace/test.txt", "w") as file:
+ file.write(f"pymsgbox version: {version_info}\n")
+
+version_info
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_003.log
new file mode 100644
index 0000000000..530fccf29d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_003.log
@@ -0,0 +1,4 @@
+The version of `pymsgbox` has been successfully written to `/workspace/test.txt`. Let's verify the content of the file to ensure everything is correct.
+
+
+cat /workspace/test.txt
diff --git a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_004.log
similarity index 59%
rename from tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_003.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_004.log
index 62ad1b8be0..205a098d64 100644
--- a/tests/integration/mock/CodeActSWEAgent/test_ipython_module/response_003.log
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/response_004.log
@@ -1 +1 @@
-The version of `pymsgbox` has been successfully written to `/workspace/test.txt`. The task is now complete.
+The version of `pymsgbox` has been successfully written to `/workspace/test.txt`. The task is complete.
diff --git a/tests/integration/mock/CodeActSWEAgent/test_write_simple_script/prompt_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_001.log
similarity index 99%
rename from tests/integration/mock/CodeActSWEAgent/test_write_simple_script/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_001.log
index 1fcc8aae38..82481572e1 100644
--- a/tests/integration/mock/CodeActSWEAgent/test_write_simple_script/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_001.log
@@ -591,4 +591,4 @@ NOW, LET'S START!
Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
-ENVIRONMENT REMINDER: You have 14 turns left to complete the task.
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_002.log
new file mode 100644
index 0000000000..6fec6ca795
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_002.log
@@ -0,0 +1,608 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+----------
+
+Sure, I'll create the `hello.sh` script for you.
+
+echo 'echo "hello"' > hello.sh
+
+
+----------
+
+OBSERVATION:
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_003.log
new file mode 100644
index 0000000000..0bc7c580c0
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_003.log
@@ -0,0 +1,625 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+----------
+
+Sure, I'll create the `hello.sh` script for you.
+
+echo 'echo "hello"' > hello.sh
+
+
+----------
+
+OBSERVATION:
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+----------
+
+The `hello.sh` script has been created. Let's verify its content and make it executable.
+
+First, let's check the content:
+
+cat hello.sh
+
+
+----------
+
+OBSERVATION:
+echo "hello"
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_004.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_004.log
new file mode 100644
index 0000000000..c7be5e5981
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/prompt_004.log
@@ -0,0 +1,643 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+----------
+
+Sure, I'll create the `hello.sh` script for you.
+
+echo 'echo "hello"' > hello.sh
+
+
+----------
+
+OBSERVATION:
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+----------
+
+The `hello.sh` script has been created. Let's verify its content and make it executable.
+
+First, let's check the content:
+
+cat hello.sh
+
+
+----------
+
+OBSERVATION:
+echo "hello"
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+----------
+
+The content of `hello.sh` is correct. Now, let's make the script executable and run it to ensure it works as expected.
+
+chmod +x hello.sh
+./hello.sh
+
+
+----------
+
+OBSERVATION:
+
+opendevin@docker-desktop:/workspace $ ./hello.sh
+hello
+
+opendevin@docker-desktop:/workspace $
+[Command -1 finished with exit code 0]
+
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_001.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_001.log
new file mode 100644
index 0000000000..752c145b85
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_001.log
@@ -0,0 +1,4 @@
+Sure, I'll create the `hello.sh` script for you.
+
+
+echo 'echo "hello"' > hello.sh
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_002.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_002.log
new file mode 100644
index 0000000000..dcc2eabc6a
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_002.log
@@ -0,0 +1,5 @@
+The `hello.sh` script has been created. Let's verify its content and make it executable.
+
+First, let's check the content:
+
+cat hello.sh
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_003.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_003.log
new file mode 100644
index 0000000000..6da7ce2855
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_003.log
@@ -0,0 +1,5 @@
+The content of `hello.sh` is correct. Now, let's make the script executable and run it to ensure it works as expected.
+
+
+chmod +x hello.sh
+./hello.sh
diff --git a/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_004.log b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_004.log
new file mode 100644
index 0000000000..89d9d9ab87
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_write_simple_script/response_004.log
@@ -0,0 +1 @@
+The `hello.sh` script executed successfully and printed "hello". The task is complete.
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/prompt_001.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_001.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_001.log
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/prompt_003.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_002.log
similarity index 86%
rename from tests/integration/mock/DelegatorAgent/test_edits/prompt_003.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_002.log
index 5019fbdf7f..8b8ef0b4a3 100644
--- a/tests/integration/mock/DelegatorAgent/test_edits/prompt_003.log
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_002.log
@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
-[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}, {"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "bad.txt"}}]
+[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
## Format
Your response MUST be in JSON format. It must be an object, and it must contain two fields:
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_003.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_003.log
new file mode 100644
index 0000000000..982b3d2b64
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_003.log
@@ -0,0 +1,84 @@
+
+
+----------
+
+# Task
+You are a software architect. Your team has inherited an existing codebase, and
+need to finish a project:
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+As an architect, you need to study the codebase to find all the information that
+might be helpful for your software engineering team.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+You must ONLY `run` commands that have no side-effects, like `ls` and `grep`. You
+MUST NOT modify or write to any file.
+
+Do NOT finish until you have a complete understanding of which parts of the
+codebase are relevant to the project, including particular files, functions, and classes.
+When you're done, put your summary in `outputs.summary` in the `finish` action.
+Remember, your task is to explore and study the current repository, not actually
+implement the solution. If the codebase is empty, you should call the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}, {"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "/workspace/bad.txt"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
+
+
+## Examples
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
+
+ASSISTANT:
+{
+ "action": "run",
+ "args": {
+ "command": "ls"
+ }
+}
+
+USER:
+OBSERVATION:
+[]
+
+ASSISTANT:
+{
+ "action": "finish",
+ "args": {
+ "outputs": {
+ "summary": "The codebase appears to be empty. Engineers should start everything from scratch."
+ }
+ }
+}
+
+--- END OF EXAMPLE ---
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_004.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_004.log
new file mode 100644
index 0000000000..946bb4950c
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_004.log
@@ -0,0 +1,58 @@
+
+
+----------
+
+# Task
+You are a software engineer. You've inherited an existing codebase, which you
+need to modify to complete this task:
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+
+Here's a summary of the codebase, as it relates to this task:
+
+The codebase consists of a single file named 'bad.txt' which contains several typos. The content of 'bad.txt' is:
+
+This is a stupid typoo.
+Really?
+No mor typos!
+Enjoy!
+
+The engineering team needs to correct the typos in this file.
+
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+Do NOT finish until you have completed the tasks.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_005.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_005.log
new file mode 100644
index 0000000000..f1518f188d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_005.log
@@ -0,0 +1,58 @@
+
+
+----------
+
+# Task
+You are a software engineer. You've inherited an existing codebase, which you
+need to modify to complete this task:
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+
+Here's a summary of the codebase, as it relates to this task:
+
+The codebase consists of a single file named 'bad.txt' which contains several typos. The content of 'bad.txt' is:
+
+This is a stupid typoo.
+Really?
+No mor typos!
+Enjoy!
+
+The engineering team needs to correct the typos in this file.
+
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+Do NOT finish until you have completed the tasks.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "/workspace/bad.txt"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_006.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_006.log
new file mode 100644
index 0000000000..6e6649226d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_006.log
@@ -0,0 +1,58 @@
+
+
+----------
+
+# Task
+You are a software engineer. You've inherited an existing codebase, which you
+need to modify to complete this task:
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+
+Here's a summary of the codebase, as it relates to this task:
+
+The codebase consists of a single file named 'bad.txt' which contains several typos. The content of 'bad.txt' is:
+
+This is a stupid typoo.
+Really?
+No mor typos!
+Enjoy!
+
+The engineering team needs to correct the typos in this file.
+
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+Do NOT finish until you have completed the tasks.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "/workspace/bad.txt"}}, {"source": "agent", "action": "write", "args": {"path": "bad.txt", "content": "This is a stupid typo.\nReally?\nNo more typos!\nEnjoy!\n", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "/workspace/bad.txt"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/prompt_007.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_007.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/prompt_007.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_007.log
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_008.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_008.log
new file mode 100644
index 0000000000..b5683d733d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/prompt_008.log
@@ -0,0 +1,49 @@
+
+
+----------
+
+# Task
+You are a quality assurance engineer. Another engineer has made changes to the
+codebase which are supposed to solve this task:
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+Note the changes might have already been applied in-line. You should focus on
+validating if the task is solved, nothing else.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+You must ONLY `run` commands that have no side-effects, like `ls`, `grep`, and test scripts.
+
+Do NOT finish until you know whether the task is complete and correct.
+When you're done, add a `completed` boolean to the `outputs` of the `finish` action.
+If `completed` is `false`, you MUST also provide a `summary` in the `outputs` of the `finish` action
+explaining what the problem is.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typo.\nReally?\nNo more typos!\nEnjoy!\n\n", "extras": {"path": "/workspace/bad.txt"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_001.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_001.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_001.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_001.log
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_002.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_002.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_002.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_002.log
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_003.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_003.log
new file mode 100644
index 0000000000..10e978d770
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_003.log
@@ -0,0 +1,8 @@
+{
+ "action": "finish",
+ "args": {
+ "outputs": {
+ "summary": "The codebase consists of a single file named 'bad.txt' which contains several typos. The content of 'bad.txt' is:\n\nThis is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n\nThe engineering team needs to correct the typos in this file."
+ }
+ }
+}
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_004.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_004.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_004.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_004.log
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_005.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_005.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_005.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_005.log
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_006.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_006.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_006.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_006.log
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_007.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_007.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_007.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_007.log
diff --git a/tests/integration/mock/DelegatorAgent/test_edits/response_008.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_008.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_edits/response_008.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_edits/response_008.log
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_001.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_001.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_001.log
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_002.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_002.log
new file mode 100644
index 0000000000..d998ca89f5
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_002.log
@@ -0,0 +1,84 @@
+
+
+----------
+
+# Task
+You are a software architect. Your team has inherited an existing codebase, and
+need to finish a project:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+As an architect, you need to study the codebase to find all the information that
+might be helpful for your software engineering team.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+You must ONLY `run` commands that have no side-effects, like `ls` and `grep`. You
+MUST NOT modify or write to any file.
+
+Do NOT finish until you have a complete understanding of which parts of the
+codebase are relevant to the project, including particular files, functions, and classes.
+When you're done, put your summary in `outputs.summary` in the `finish` action.
+Remember, your task is to explore and study the current repository, not actually
+implement the solution. If the codebase is empty, you should call the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
+
+
+## Examples
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- START OF EXAMPLE ---
+
+USER: Can you create a list of numbers from 1 to 10, and create a web page to display them at port 5000?
+
+ASSISTANT:
+{
+ "action": "run",
+ "args": {
+ "command": "ls"
+ }
+}
+
+USER:
+OBSERVATION:
+[]
+
+ASSISTANT:
+{
+ "action": "finish",
+ "args": {
+ "outputs": {
+ "summary": "The codebase appears to be empty. Engineers should start everything from scratch."
+ }
+ }
+}
+
+--- END OF EXAMPLE ---
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_003.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_003.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_003.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_003.log
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_004.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_004.log
new file mode 100644
index 0000000000..36bf347a65
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_004.log
@@ -0,0 +1,51 @@
+
+
+----------
+
+# Task
+You are a software engineer. You've inherited an existing codebase, which you
+need to modify to complete this task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+
+Here's a summary of the codebase, as it relates to this task:
+
+The codebase appears to be empty. Engineers should start everything from scratch.
+
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+Do NOT finish until you have completed the tasks.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "/workspace/hello.sh"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_005.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_005.log
new file mode 100644
index 0000000000..896c201f5b
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_005.log
@@ -0,0 +1,51 @@
+
+
+----------
+
+# Task
+You are a software engineer. You've inherited an existing codebase, which you
+need to modify to complete this task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+
+Here's a summary of the codebase, as it relates to this task:
+
+The codebase appears to be empty. Engineers should start everything from scratch.
+
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+Do NOT finish until you have completed the tasks.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "/workspace/hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_006.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_006.log
new file mode 100644
index 0000000000..7648030083
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_006.log
@@ -0,0 +1,51 @@
+
+
+----------
+
+# Task
+You are a software engineer. You've inherited an existing codebase, which you
+need to modify to complete this task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+
+Here's a summary of the codebase, as it relates to this task:
+
+The codebase appears to be empty. Engineers should start everything from scratch.
+
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+Do NOT finish until you have completed the tasks.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "/workspace/hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}, {"source": "agent", "action": "run", "args": {"command": "./hello.sh", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_007.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_007.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_007.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_007.log
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_008.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_008.log
new file mode 100644
index 0000000000..5ddd4dd4eb
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_008.log
@@ -0,0 +1,49 @@
+
+
+----------
+
+# Task
+You are a quality assurance engineer. Another engineer has made changes to the
+codebase which are supposed to solve this task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+Note the changes might have already been applied in-line. You should focus on
+validating if the task is solved, nothing else.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+You must ONLY `run` commands that have no side-effects, like `ls`, `grep`, and test scripts.
+
+Do NOT finish until you know whether the task is complete and correct.
+When you're done, add a `completed` boolean to the `outputs` of the `finish` action.
+If `completed` is `false`, you MUST also provide a `summary` in the `outputs` of the `finish` action
+explaining what the problem is.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\necho 'hello'\n", "extras": {"path": "/workspace/hello.sh"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_009.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_009.log
new file mode 100644
index 0000000000..51bc8477cb
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/prompt_009.log
@@ -0,0 +1,49 @@
+
+
+----------
+
+# Task
+You are a quality assurance engineer. Another engineer has made changes to the
+codebase which are supposed to solve this task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+Note the changes might have already been applied in-line. You should focus on
+validating if the task is solved, nothing else.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+You must ONLY `run` commands that have no side-effects, like `ls`, `grep`, and test scripts.
+
+Do NOT finish until you know whether the task is complete and correct.
+When you're done, add a `completed` boolean to the `outputs` of the `finish` action.
+If `completed` is `false`, you MUST also provide a `summary` in the `outputs` of the `finish` action
+explaining what the problem is.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\necho 'hello'\n", "extras": {"path": "/workspace/hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_001.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_001.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_001.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_001.log
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_002.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_002.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_002.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_002.log
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_003.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_003.log
new file mode 100644
index 0000000000..c146bbc016
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_003.log
@@ -0,0 +1,9 @@
+```json
+{
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'"
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_004.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_004.log
new file mode 100644
index 0000000000..a64dfbf481
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_004.log
@@ -0,0 +1,8 @@
+```json
+{
+ "action": "run",
+ "args": {
+ "command": "chmod +x hello.sh"
+ }
+}
+```
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_005.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_005.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_005.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_005.log
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_006.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_006.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_006.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_006.log
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_007.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_007.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_007.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_007.log
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_008.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_008.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_008.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_008.log
diff --git a/tests/integration/mock/DelegatorAgent/test_write_simple_script/response_009.log b/tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_009.log
similarity index 100%
rename from tests/integration/mock/DelegatorAgent/test_write_simple_script/response_009.log
rename to tests/integration/mock/eventstream_runtime/DelegatorAgent/test_write_simple_script/response_009.log
diff --git a/tests/integration/mock/ManagerAgent/test_edits/prompt_001.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_001.log
similarity index 98%
rename from tests/integration/mock/ManagerAgent/test_edits/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_001.log
index 2839256104..9839525439 100644
--- a/tests/integration/mock/ManagerAgent/test_edits/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_001.log
@@ -70,7 +70,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
-[{"source": "user", "action": "message", "args": {"content": "Fix typos in bad.txt. Do not ask me for confirmation at any point.", "wait_for_response": false}}]
+[{"source": "user", "action": "message", "args": {"content": "Fix typos in bad.txt. Do not ask me for confirmation at any point.", "images_urls": null, "wait_for_response": false}}]
If the last item in the history is an error, you should try to fix it. If you
cannot fix it, call the `reject` action.
diff --git a/tests/integration/mock/ManagerAgent/test_edits/prompt_002.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_002.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_edits/prompt_002.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_002.log
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_003.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_003.log
new file mode 100644
index 0000000000..c219bff987
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_003.log
@@ -0,0 +1,79 @@
+
+
+----------
+
+# Task
+You are a proofreader tasked with fixing typos in the files in your current working directory.
+
+
+Specifically, your task is:
+Fix typos in bad.txt
+
+
+To achieve this goal, you should:
+
+1. Scan the files for typos
+2. Overwrite the files with the typos fixed
+3. Provide a summary of the typos fixed
+
+## Available Actions
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+To complete this task:
+1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
+2. Use the `message` action to analyze the contents and identify typos.
+3. Use the `write` action to create new versions of the files with the typos fixed.
+ - Overwrite the original files with the corrected content. Make sure to provide the file path in the format `'./file_name.ext'`.
+4. Use the `message` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
+5. Use the `finish` action to return the summary in the `outputs.summary` field.
+
+Do NOT finish until you have fixed all the typos and generated a summary.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "./bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "/workspace/bad.txt"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
+
+
+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."
+ }
+}
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_004.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_004.log
new file mode 100644
index 0000000000..bb7dbbc98d
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_004.log
@@ -0,0 +1,79 @@
+
+
+----------
+
+# Task
+You are a proofreader tasked with fixing typos in the files in your current working directory.
+
+
+Specifically, your task is:
+Fix typos in bad.txt
+
+
+To achieve this goal, you should:
+
+1. Scan the files for typos
+2. Overwrite the files with the typos fixed
+3. Provide a summary of the typos fixed
+
+## Available Actions
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+To complete this task:
+1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
+2. Use the `message` action to analyze the contents and identify typos.
+3. Use the `write` action to create new versions of the files with the typos fixed.
+ - Overwrite the original files with the corrected content. Make sure to provide the file path in the format `'./file_name.ext'`.
+4. Use the `message` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
+5. Use the `finish` action to return the summary in the `outputs.summary` field.
+
+Do NOT finish until you have fixed all the typos and generated a summary.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "./bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "/workspace/bad.txt"}}, {"source": "agent", "action": "message", "args": {"content": "The content of 'bad.txt' has been read. The following typos have been identified:\n1. 'typoo' should be 'typo'\n2. 'mor' should be 'more'\nI will now proceed to fix these typos and overwrite the original file with the corrected content.", "images_urls": null, "wait_for_response": false}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
+
+
+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."
+ }
+}
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_005.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_005.log
new file mode 100644
index 0000000000..84df94faca
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_005.log
@@ -0,0 +1,79 @@
+
+
+----------
+
+# Task
+You are a proofreader tasked with fixing typos in the files in your current working directory.
+
+
+Specifically, your task is:
+Fix typos in bad.txt
+
+
+To achieve this goal, you should:
+
+1. Scan the files for typos
+2. Overwrite the files with the typos fixed
+3. Provide a summary of the typos fixed
+
+## Available Actions
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `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
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+To complete this task:
+1. Use the `read` action to read the contents of the files in your current working directory. Make sure to provide the file path in the format `'./file_name.ext'`.
+2. Use the `message` action to analyze the contents and identify typos.
+3. Use the `write` action to create new versions of the files with the typos fixed.
+ - Overwrite the original files with the corrected content. Make sure to provide the file path in the format `'./file_name.ext'`.
+4. Use the `message` action to generate a summary of the typos fixed, including the original and fixed versions of each typo, and the file(s) they were found in.
+5. Use the `finish` action to return the summary in the `outputs.summary` field.
+
+Do NOT finish until you have fixed all the typos and generated a summary.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "read", "args": {"path": "./bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "/workspace/bad.txt"}}, {"source": "agent", "action": "message", "args": {"content": "The content of 'bad.txt' has been read. The following typos have been identified:\n1. 'typoo' should be 'typo'\n2. 'mor' should be 'more'\nI will now proceed to fix these typos and overwrite the original file with the corrected content.", "images_urls": null, "wait_for_response": false}}, {"source": "agent", "action": "write", "args": {"path": "./bad.txt", "content": "This is a stupid typo.\nReally?\nNo more typos!\nEnjoy!\n", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "/workspace/bad.txt"}}]
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
+
+
+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."
+ }
+}
diff --git a/tests/integration/mock/ManagerAgent/test_edits/prompt_006.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_006.log
similarity index 86%
rename from tests/integration/mock/ManagerAgent/test_edits/prompt_006.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_006.log
index 9fb0e100db..1ee30eefbf 100644
--- a/tests/integration/mock/ManagerAgent/test_edits/prompt_006.log
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/prompt_006.log
@@ -70,7 +70,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
-[{"source": "user", "action": "message", "args": {"content": "Fix typos in bad.txt. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"source": "agent", "action": "delegate", "args": {"agent": "TypoFixerAgent", "inputs": {"task": "Fix typos in bad.txt"}, "thought": ""}}, {"source": "agent", "observation": "delegate", "content": "TypoFixerAgent finishes task with summary: {'file': './bad.txt', 'typos_fixed': [{'original': 'typoo', 'corrected': 'typo'}, {'original': 'mor', 'corrected': 'more'}]}", "extras": {"outputs": {"summary": {"file": "./bad.txt", "typos_fixed": [{"original": "typoo", "corrected": "typo"}, {"original": "mor", "corrected": "more"}]}}}}]
+[{"source": "user", "action": "message", "args": {"content": "Fix typos in bad.txt. Do not ask me for confirmation at any point.", "images_urls": null, "wait_for_response": false}}, {"source": "agent", "action": "delegate", "args": {"agent": "TypoFixerAgent", "inputs": {"task": "Fix typos in bad.txt"}, "thought": ""}}, {"source": "agent", "observation": "delegate", "content": "TypoFixerAgent finishes task with summary: {'bad.txt': {'original': ['typoo', 'mor'], 'fixed': ['typo', 'more']}}", "extras": {"outputs": {"summary": {"bad.txt": {"original": ["typoo", "mor"], "fixed": ["typo", "more"]}}}}}]
If the last item in the history is an error, you should try to fix it. If you
cannot fix it, call the `reject` action.
diff --git a/tests/integration/mock/ManagerAgent/test_edits/response_001.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_001.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_edits/response_001.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_001.log
diff --git a/tests/integration/mock/ManagerAgent/test_edits/response_002.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_002.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_edits/response_002.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_002.log
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_003.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_003.log
new file mode 100644
index 0000000000..10a33cbb7a
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_003.log
@@ -0,0 +1,6 @@
+{
+ "action": "message",
+ "args": {
+ "content": "The content of 'bad.txt' has been read. The following typos have been identified:\n1. 'typoo' should be 'typo'\n2. 'mor' should be 'more'\nI will now proceed to fix these typos and overwrite the original file with the corrected content."
+ }
+}
diff --git a/tests/integration/mock/ManagerAgent/test_edits/response_004.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_004.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_edits/response_004.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_004.log
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_005.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_005.log
new file mode 100644
index 0000000000..501d4cbc22
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_005.log
@@ -0,0 +1,19 @@
+{
+ "action": "finish",
+ "args": {
+ "outputs": {
+ "summary": {
+ "bad.txt": {
+ "original": [
+ "typoo",
+ "mor"
+ ],
+ "fixed": [
+ "typo",
+ "more"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_006.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_006.log
new file mode 100644
index 0000000000..5900f83a70
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_edits/response_006.log
@@ -0,0 +1,15 @@
+```json
+{
+ "action": "finish",
+ "args": {
+ "outputs": {
+ "summary": {
+ "bad.txt": {
+ "original": ["typoo", "mor"],
+ "fixed": ["typo", "more"]
+ }
+ }
+ }
+ }
+}
+```
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_001.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_001.log
similarity index 97%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_001.log
index 8ad7e8de7c..71235b4455 100644
--- a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_001.log
@@ -70,7 +70,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
-[{"source": "user", "action": "message", "args": {"content": "Write a git commit message for the current staging area. Do not ask me for confirmation at any point.", "wait_for_response": false}}]
+[{"source": "user", "action": "message", "args": {"content": "Write a git commit message for the current staging area. Do not ask me for confirmation at any point.", "images_urls": null, "wait_for_response": false}}]
If the last item in the history is an error, you should try to fix it. If you
cannot fix it, call the `reject` action.
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_002.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_002.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_002.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_002.log
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_004.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_003.log
similarity index 81%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_004.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_003.log
index 29f1dc8392..4ffd673c10 100644
--- a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_004.log
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_003.log
@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
-[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}]
If the last item in the history is an error, you should try to fix it.
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_005.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_004.log
similarity index 76%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_005.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_004.log
index 07713e2d3b..7ba2092a68 100644
--- a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_005.log
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_004.log
@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.
-[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
If the last item in the history is an error, you should try to fix it.
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_005.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_005.log
new file mode 100644
index 0000000000..f26a33bc5f
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_005.log
@@ -0,0 +1,51 @@
+
+
+----------
+
+# Task
+You are a responsible software engineer and always write good commit messages.
+
+Please analyze the diff in the staging area, understand the context and content
+of the updates from the diff only. Identify key elements like:
+- Which files are affected?
+- What types of changes were made (e.g., new features, bug fixes, refactoring, documentation, testing)?
+
+Then you should generate a commit message that succinctly summarizes the staged
+changes. The commit message should include:
+- A summary line that clearly states the purpose of the changes.
+- Optionally, a detailed description if the changes are complex or need further explanation.
+
+You should first use `git status` to check whether it's a valid git repo and there
+is diff in the staging area. If not, please call the `reject` action.
+
+If it is a valid git repo and there is diff in the staging area, you should find
+the diff using `git diff --cached`, compile a commit message, and call the `finish`
+action with `outputs.answer` set to the answer.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+
+If the last item in the history is an error, you should try to fix it.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `reject` - reject the task. Arguments:
+ * `outputs` - a dictionary with only a `reason` attribute
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_006.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_006.log
new file mode 100644
index 0000000000..948fd8c57e
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_006.log
@@ -0,0 +1,51 @@
+
+
+----------
+
+# Task
+You are a responsible software engineer and always write good commit messages.
+
+Please analyze the diff in the staging area, understand the context and content
+of the updates from the diff only. Identify key elements like:
+- Which files are affected?
+- What types of changes were made (e.g., new features, bug fixes, refactoring, documentation, testing)?
+
+Then you should generate a commit message that succinctly summarizes the staged
+changes. The commit message should include:
+- A summary line that clearly states the purpose of the changes.
+- Optionally, a detailed description if the changes are complex or need further explanation.
+
+You should first use `git status` to check whether it's a valid git repo and there
+is diff in the staging area. If not, please call the `reject` action.
+
+If it is a valid git repo and there is diff in the staging area, you should find
+the diff using `git diff --cached`, compile a commit message, and call the `finish`
+action with `outputs.answer` set to the answer.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+
+If the last item in the history is an error, you should try to fix it.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `reject` - reject the task. Arguments:
+ * `outputs` - a dictionary with only a `reason` attribute
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_007.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_007.log
new file mode 100644
index 0000000000..79f3fc54cf
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_007.log
@@ -0,0 +1,51 @@
+
+
+----------
+
+# Task
+You are a responsible software engineer and always write good commit messages.
+
+Please analyze the diff in the staging area, understand the context and content
+of the updates from the diff only. Identify key elements like:
+- Which files are affected?
+- What types of changes were made (e.g., new features, bug fixes, refactoring, documentation, testing)?
+
+Then you should generate a commit message that succinctly summarizes the staged
+changes. The commit message should include:
+- A summary line that clearly states the purpose of the changes.
+- Optionally, a detailed description if the changes are complex or need further explanation.
+
+You should first use `git status` to check whether it's a valid git repo and there
+is diff in the staging area. If not, please call the `reject` action.
+
+If it is a valid git repo and there is diff in the staging area, you should find
+the diff using `git diff --cached`, compile a commit message, and call the `finish`
+action with `outputs.answer` set to the answer.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "keep_prompt": true, "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\r\n\r\nopendevin@docker-desktop:/workspace $ ", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+
+If the last item in the history is an error, you should try to fix it.
+
+## Available Actions
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+
+* `reject` - reject the task. Arguments:
+ * `outputs` - a dictionary with only a `reason` attribute
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_008.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_008.log
new file mode 100644
index 0000000000..c869abe630
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/prompt_008.log
@@ -0,0 +1,95 @@
+
+
+----------
+
+# Task
+You are in charge of accomplishing the following task:
+Write a git commit message for the current staging area. Do not ask me for confirmation at any point.
+
+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
+select one of the delegates below to move towards accomplishing the task, and you MUST
+provide the correct inputs for the delegate you select.
+
+Note: the delegated agent either returns "finish" or "reject".
+- If the action is "finish", but the full task is not done yet, you should
+continue to delegate to one of the agents below to until the full task is finished.
+- If the action is "reject", it means the delegated agent is not capable of the
+task you send to. You should revisit the input you send to the delegate, and consider
+whether any other delegate would be able to solve the task. If you cannot find
+a proper delegate agent, or the delegate attempts keep failing, call the `reject`
+action. In `reason` attribute, make sure you include your attempts (e.g. what agent
+you have delegated to, and why they failed).
+
+## Agents
+
+### CoderAgent
+Given a particular task, and a detailed description of the codebase, accomplishes the task
+#### Inputs
+{"task": "string", "summary": "string"}
+
+### CommitWriterAgent
+Write a git commit message for files in the git staging area
+#### Inputs
+{}
+
+### MathAgent
+Solves simple and complex math problems using python
+#### Inputs
+{"task": "string"}
+
+### PostgresAgent
+Writes and maintains PostgreSQL migrations
+#### Inputs
+{"task": "string"}
+
+### RepoExplorerAgent
+Generates a detailed summary of an existing codebase
+#### Inputs
+{}
+
+### StudyRepoForTaskAgent
+Given a particular task, finds and describes all relevant parts of the codebase
+#### Inputs
+{"task": "string"}
+
+### TypoFixerAgent
+Fixes typos in files in the current working directory
+#### Inputs
+{"task": "string"}
+
+### VerifierAgent
+Given a particular task, verifies that the task has been completed
+#### Inputs
+{"task": "string"}
+
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+actions and observations--more may have happened before that.
+They are time-ordered, with your most recent action at the bottom.
+
+[{"source": "user", "action": "message", "args": {"content": "Write a git commit message for the current staging area. Do not ask me for confirmation at any point.", "images_urls": null, "wait_for_response": false}}, {"source": "agent", "action": "delegate", "args": {"agent": "CommitWriterAgent", "inputs": {}, "thought": ""}}, {"source": "agent", "observation": "delegate", "content": "CommitWriterAgent finishes task with reason: Not a valid git repository.", "extras": {"outputs": {"reason": "Not a valid git repository."}}}]
+
+If the last item in the history is an error, you should try to fix it. If you
+cannot fix it, call the `reject` action.
+
+## Available Actions
+* `delegate` - send a task to another agent from the list provided. Arguments:
+ * `agent` - the agent to which the task is delegated. MUST match a name in the list of agents provided.
+ * `inputs` - a dictionary of input parameters to the agent, as specified in the list
+
+* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
+ * `outputs` - a dictionary representing the outputs of your task, if any
+
+* `reject` - reject the task. Arguments:
+ * `outputs` - a dictionary with only a `reason` attribute
+
+
+## Format
+Your response MUST be in JSON format. It must be an object, and it must contain two fields:
+* `action`, which is one of the actions specified here
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+You MUST NOT include any other text besides the JSON response
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/response_001.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_001.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/response_001.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_001.log
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/response_002.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_002.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/response_002.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_002.log
diff --git a/tests/integration/mock/ManagerAgent/test_simple_task_rejection/response_003.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_003.log
similarity index 100%
rename from tests/integration/mock/ManagerAgent/test_simple_task_rejection/response_003.log
rename to tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_003.log
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_004.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_004.log
new file mode 100644
index 0000000000..284ec6eaf9
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_004.log
@@ -0,0 +1,8 @@
+```json
+{
+ "action": "reject",
+ "args": {
+ "reason": "Not a valid git repository."
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_005.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_005.log
new file mode 100644
index 0000000000..284ec6eaf9
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_005.log
@@ -0,0 +1,8 @@
+```json
+{
+ "action": "reject",
+ "args": {
+ "reason": "Not a valid git repository."
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_006.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_006.log
new file mode 100644
index 0000000000..284ec6eaf9
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_006.log
@@ -0,0 +1,8 @@
+```json
+{
+ "action": "reject",
+ "args": {
+ "reason": "Not a valid git repository."
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_007.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_007.log
new file mode 100644
index 0000000000..eea0660fde
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_007.log
@@ -0,0 +1,8 @@
+{
+ "action": "reject",
+ "args": {
+ "outputs": {
+ "reason": "Not a valid git repository."
+ }
+ }
+}
diff --git a/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_008.log b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_008.log
new file mode 100644
index 0000000000..71122a7f88
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/ManagerAgent/test_simple_task_rejection/response_008.log
@@ -0,0 +1,10 @@
+```json
+{
+ "action": "reject",
+ "args": {
+ "outputs": {
+ "reason": "CommitWriterAgent failed because the repository is not valid. No other agents can resolve this issue."
+ }
+ }
+}
+```
diff --git a/tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_001.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_001.log
similarity index 99%
rename from tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_001.log
rename to tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_001.log
index cab7b2af58..00389e2e9f 100644
--- a/tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_001.log
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_001.log
@@ -57,6 +57,7 @@ ten actions--more happened before that.
"action": "message",
"args": {
"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
"wait_for_response": false
}
}
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_002.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_002.log
new file mode 100644
index 0000000000..f9e7cf5b63
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_002.log
@@ -0,0 +1,149 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "open",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "open",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "open",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "open",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_003.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_003.log
new file mode 100644
index 0000000000..c1aa30ab70
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_003.log
@@ -0,0 +1,160 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "in_progress",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "open",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Create the shell script file 'hello.sh'..
+If it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_004.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_004.log
new file mode 100644
index 0000000000..c03ff9801b
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_004.log
@@ -0,0 +1,179 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "in_progress",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "open",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Create the shell script file 'hello.sh'..
+If it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You just changed a file. You should think about how it affects your plan.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_005.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_005.log
new file mode 100644
index 0000000000..fe13547223
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_005.log
@@ -0,0 +1,187 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "open",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Write a shell script 'hello.sh' that prints 'hello'..
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_006.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_006.log
new file mode 100644
index 0000000000..7d762e8029
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_006.log
@@ -0,0 +1,197 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "in_progress",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Write the content to print 'hello' in 'hello.sh'..
+If it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_007.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_007.log
new file mode 100644
index 0000000000..dd92e38b6c
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_007.log
@@ -0,0 +1,216 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "in_progress",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Write the content to print 'hello' in 'hello.sh'..
+If it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You just changed a file. You should think about how it affects your plan.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_008.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_008.log
new file mode 100644
index 0000000000..237f8f8046
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_008.log
@@ -0,0 +1,224 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "open",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Write a shell script 'hello.sh' that prints 'hello'..
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "completed",
+ "thought": "The content to print 'hello' has been written to 'hello.sh'."
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_009.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_009.log
new file mode 100644
index 0000000000..152b99d118
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_009.log
@@ -0,0 +1,234 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Run the shell script to verify it prints 'hello'..
+If it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "completed",
+ "thought": "The content to print 'hello' has been written to 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.2",
+ "state": "in_progress",
+ "thought": "The next step is to run the shell script to verify it prints 'hello'."
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_010.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_010.log
new file mode 100644
index 0000000000..16fd2fd0f8
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_010.log
@@ -0,0 +1,254 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Run the shell script to verify it prints 'hello'..
+If it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW.
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "completed",
+ "thought": "The content to print 'hello' has been written to 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.2",
+ "state": "in_progress",
+ "thought": "The next step is to run the shell script to verify it prints 'hello'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "run",
+ "args": {
+ "command": "bash hello.sh",
+ "thought": "",
+ "keep_prompt": true,
+ "is_confirmed": "confirmed"
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "run",
+ "content": "hello\r\n\r\nopendevin@docker-desktop:/workspace $ ",
+ "extras": {
+ "command_id": -1,
+ "command": "bash hello.sh",
+ "exit_code": 0
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the command you just ran, what output it gave, and how that affects your plan.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_011.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_011.log
new file mode 100644
index 0000000000..6012255eb8
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/prompt_011.log
@@ -0,0 +1,262 @@
+
+
+----------
+
+
+# Task
+You're a diligent software engineer AI. You can't see, draw, or interact with a
+browser, but you can read and write files, and you can run commands, and you can think.
+
+You've been given the following task:
+
+Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
+
+## Plan
+As you complete this task, you're building a plan and keeping
+track of your progress. Here's a JSON representation of your plan:
+
+{
+ "id": "",
+ "goal": "",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "state": "in_progress",
+ "subtasks": [
+ {
+ "id": "0.0",
+ "goal": "Create the shell script file 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.1",
+ "goal": "Write the content to print 'hello' in 'hello.sh'.",
+ "state": "completed",
+ "subtasks": []
+ },
+ {
+ "id": "0.2",
+ "goal": "Run the shell script to verify it prints 'hello'.",
+ "state": "completed",
+ "subtasks": []
+ }
+ ]
+ }
+ ]
+}
+
+
+You're currently working on this task:
+Write a shell script 'hello.sh' that prints 'hello'..
+
+You're responsible for managing this plan and the status of tasks in
+it, by using the `add_task` and `modify_task` actions described below.
+
+If the History below contradicts the state of any of these tasks, you
+MUST modify the task using the `modify_task` action described below.
+
+Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
+a task that's already represented. Every task must be represented only once.
+
+Tasks that are sequential MUST be siblings. They must be added in order
+to their parent task.
+
+If you mark a task as 'completed', 'verified', or 'abandoned',
+all non-abandoned subtasks will be marked the same way.
+So before closing a task this way, you MUST not only be sure that it has
+been completed successfully--you must ALSO be sure that all its subtasks
+are ready to be marked the same way.
+
+If, and only if, ALL tasks have already been marked verified,
+you MUST respond with the `finish` action.
+
+## History
+Here is a recent history of actions you've taken in service of this plan,
+as well as observations you've made. This only includes the MOST RECENT
+ten actions--more happened before that.
+
+[
+ {
+ "source": "user",
+ "action": "message",
+ "args": {
+ "content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.",
+ "images_urls": null,
+ "wait_for_response": false
+ }
+ },
+ {
+ "source": "agent",
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ],
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "in_progress",
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'",
+ "start": 0,
+ "end": -1,
+ "thought": ""
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "write",
+ "content": "",
+ "extras": {
+ "path": "/workspace/hello.sh"
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "completed",
+ "thought": "The content to print 'hello' has been written to 'hello.sh'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.2",
+ "state": "in_progress",
+ "thought": "The next step is to run the shell script to verify it prints 'hello'."
+ }
+ },
+ {
+ "source": "agent",
+ "action": "run",
+ "args": {
+ "command": "bash hello.sh",
+ "thought": "",
+ "keep_prompt": true,
+ "is_confirmed": "confirmed"
+ }
+ },
+ {
+ "source": "agent",
+ "observation": "run",
+ "content": "hello\r\n\r\nopendevin@docker-desktop:/workspace $ ",
+ "extras": {
+ "command_id": -1,
+ "command": "bash hello.sh",
+ "exit_code": 0
+ }
+ },
+ {
+ "source": "agent",
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.2",
+ "state": "completed",
+ "thought": "The shell script 'hello.sh' ran successfully and printed 'hello'."
+ }
+ }
+]
+
+
+Your most recent action is at the bottom of that history.
+
+## Action
+What is your next thought or action? Your response must be in JSON format.
+
+It must be an object, and it must contain two fields:
+* `action`, which is one of the actions below
+* `args`, which is a map of key-value pairs, specifying the arguments for that action
+
+* `read` - reads the content of a file. Arguments:
+ * `path` - the path of the file to read
+* `write` - writes the content to a file. Arguments:
+ * `path` - the path of the file to write
+ * `content` - the content to write to the file
+* `run` - runs a command on the command line in a Linux shell. Arguments:
+ * `command` - the command to run
+* `browse` - opens a web page. Arguments:
+ * `url` - the URL to open
+* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
+ * `content` - the message to record
+ * `wait_for_response` - set to `true` to wait for the user to respond before proceeding
+* `add_task` - add a task to your plan. Arguments:
+ * `parent` - the ID of the parent task (leave empty if it should go at the top level)
+ * `goal` - the goal of the task
+ * `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
+* `modify_task` - close a task. Arguments:
+ * `task_id` - the ID of the task to close
+ * `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
+* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
+
+You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
+You should never act twice in a row without thinking. But if your last several
+actions are all `message` actions, you should consider taking a different action.
+
+What is your next thought or action? Again, you must reply with JSON, and only with JSON.
+
+You should think about the next action to take.
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_001.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_001.log
new file mode 100644
index 0000000000..6c92e2144c
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_001.log
@@ -0,0 +1,20 @@
+```json
+{
+ "action": "add_task",
+ "args": {
+ "parent": "",
+ "goal": "Write a shell script 'hello.sh' that prints 'hello'.",
+ "subtasks": [
+ {
+ "goal": "Create the shell script file 'hello.sh'."
+ },
+ {
+ "goal": "Write the content to print 'hello' in 'hello.sh'."
+ },
+ {
+ "goal": "Run the shell script to verify it prints 'hello'."
+ }
+ ]
+ }
+}
+```
diff --git a/tests/integration/mock/PlannerAgent/test_write_simple_script/response_002.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_002.log
similarity index 100%
rename from tests/integration/mock/PlannerAgent/test_write_simple_script/response_002.log
rename to tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_002.log
diff --git a/tests/integration/mock/PlannerAgent/test_write_simple_script/response_003.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_003.log
similarity index 100%
rename from tests/integration/mock/PlannerAgent/test_write_simple_script/response_003.log
rename to tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_003.log
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_004.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_004.log
new file mode 100644
index 0000000000..c617e4d165
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_004.log
@@ -0,0 +1,10 @@
+```json
+{
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.0",
+ "state": "completed",
+ "thought": "The shell script file 'hello.sh' has been created successfully."
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_005.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_005.log
new file mode 100644
index 0000000000..d8a2c6e3f8
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_005.log
@@ -0,0 +1,10 @@
+```json
+{
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "in_progress",
+ "thought": "The next step is to write the content to print 'hello' in 'hello.sh'."
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_006.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_006.log
new file mode 100644
index 0000000000..c146bbc016
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_006.log
@@ -0,0 +1,9 @@
+```json
+{
+ "action": "write",
+ "args": {
+ "path": "hello.sh",
+ "content": "#!/bin/bash\n\necho 'hello'"
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_007.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_007.log
new file mode 100644
index 0000000000..c548df329a
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_007.log
@@ -0,0 +1,10 @@
+```json
+{
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.1",
+ "state": "completed",
+ "thought": "The content to print 'hello' has been written to 'hello.sh'."
+ }
+}
+```
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_008.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_008.log
new file mode 100644
index 0000000000..0b767dfd2b
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_008.log
@@ -0,0 +1,10 @@
+```json
+{
+ "action": "modify_task",
+ "args": {
+ "task_id": "0.2",
+ "state": "in_progress",
+ "thought": "The next step is to run the shell script to verify it prints 'hello'."
+ }
+}
+```
diff --git a/tests/integration/mock/PlannerAgent/test_write_simple_script/response_009.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_009.log
similarity index 100%
rename from tests/integration/mock/PlannerAgent/test_write_simple_script/response_009.log
rename to tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_009.log
diff --git a/tests/integration/mock/PlannerAgent/test_write_simple_script/response_010.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_010.log
similarity index 100%
rename from tests/integration/mock/PlannerAgent/test_write_simple_script/response_010.log
rename to tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_010.log
diff --git a/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_011.log b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_011.log
new file mode 100644
index 0000000000..4aecac12eb
--- /dev/null
+++ b/tests/integration/mock/eventstream_runtime/PlannerAgent/test_write_simple_script/response_011.log
@@ -0,0 +1,10 @@
+```json
+{
+ "action": "modify_task",
+ "args": {
+ "task_id": "0",
+ "state": "verified",
+ "thought": "All subtasks for writing the shell script 'hello.sh' that prints 'hello' have been completed successfully. The script has been verified to work as expected."
+ }
+}
+```
diff --git a/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_001.log b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_001.log
new file mode 100644
index 0000000000..dcde11e4e3
--- /dev/null
+++ b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_001.log
@@ -0,0 +1,126 @@
+
+
+----------
+
+# 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:
+Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
+
+# Action Space
+
+16 different types of actions are available.
+
+noop(wait_ms: float = 1000)
+ Examples:
+ noop()
+
+ noop(500)
+
+send_msg_to_user(text: str)
+ Examples:
+ send_msg_to_user('Based on the results of my search, the city was built in 1751.')
+
+scroll(delta_x: float, delta_y: float)
+ Examples:
+ scroll(0, 200)
+
+ scroll(-50.2, -100.5)
+
+fill(bid: str, value: str)
+ Examples:
+ fill('237', 'example value')
+
+ fill('45', 'multi-line\nexample')
+
+ fill('a12', 'example with "quotes"')
+
+select_option(bid: str, options: str | list[str])
+ Examples:
+ select_option('48', 'blue')
+
+ select_option('48', ['red', 'green', 'blue'])
+
+click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ click('51')
+
+ click('b22', button='right')
+
+ click('48', button='middle', modifiers=['Shift'])
+
+dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ dblclick('12')
+
+ dblclick('ca42', button='right')
+
+ dblclick('178', button='middle', modifiers=['Shift'])
+
+hover(bid: str)
+ Examples:
+ hover('b8')
+
+press(bid: str, key_comb: str)
+ Examples:
+ press('88', 'Backspace')
+
+ press('a26', 'Control+a')
+
+ press('a61', 'Meta+Shift+t')
+
+focus(bid: str)
+ Examples:
+ focus('b455')
+
+clear(bid: str)
+ Examples:
+ clear('996')
+
+drag_and_drop(from_bid: str, to_bid: str)
+ Examples:
+ drag_and_drop('56', '498')
+
+upload_file(bid: str, file: str | list[str])
+ Examples:
+ upload_file('572', 'my_receipt.pdf')
+
+ upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
+
+go_back()
+ Examples:
+ go_back()
+
+go_forward()
+ Examples:
+ go_forward()
+
+goto(url: str)
+ Examples:
+ goto('http://www.example.com')
+
+Multiple actions can be provided at once. Example:
+fill('a12', 'example with "quotes"')
+click('51')
+click('48', button='middle', modifiers=['Shift'])
+Multiple actions are meant to be executed sequentially without any feedback from the page.
+Don't execute multiple actions at once if you need feedback from the page.
+
+
+
+----------
+
+# Current Accessibility Tree:
+
+
+# Previous 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")```
+"
diff --git a/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_002.log b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_002.log
new file mode 100644
index 0000000000..7bb7b33095
--- /dev/null
+++ b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_002.log
@@ -0,0 +1,130 @@
+
+
+----------
+
+# 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:
+Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
+
+# Action Space
+
+16 different types of actions are available.
+
+noop(wait_ms: float = 1000)
+ Examples:
+ noop()
+
+ noop(500)
+
+send_msg_to_user(text: str)
+ Examples:
+ send_msg_to_user('Based on the results of my search, the city was built in 1751.')
+
+scroll(delta_x: float, delta_y: float)
+ Examples:
+ scroll(0, 200)
+
+ scroll(-50.2, -100.5)
+
+fill(bid: str, value: str)
+ Examples:
+ fill('237', 'example value')
+
+ fill('45', 'multi-line\nexample')
+
+ fill('a12', 'example with "quotes"')
+
+select_option(bid: str, options: str | list[str])
+ Examples:
+ select_option('48', 'blue')
+
+ select_option('48', ['red', 'green', 'blue'])
+
+click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ click('51')
+
+ click('b22', button='right')
+
+ click('48', button='middle', modifiers=['Shift'])
+
+dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ dblclick('12')
+
+ dblclick('ca42', button='right')
+
+ dblclick('178', button='middle', modifiers=['Shift'])
+
+hover(bid: str)
+ Examples:
+ hover('b8')
+
+press(bid: str, key_comb: str)
+ Examples:
+ press('88', 'Backspace')
+
+ press('a26', 'Control+a')
+
+ press('a61', 'Meta+Shift+t')
+
+focus(bid: str)
+ Examples:
+ focus('b455')
+
+clear(bid: str)
+ Examples:
+ clear('996')
+
+drag_and_drop(from_bid: str, to_bid: str)
+ Examples:
+ drag_and_drop('56', '498')
+
+upload_file(bid: str, file: str | list[str])
+ Examples:
+ upload_file('572', 'my_receipt.pdf')
+
+ upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
+
+go_back()
+ Examples:
+ go_back()
+
+go_forward()
+ Examples:
+ go_forward()
+
+goto(url: str)
+ Examples:
+ goto('http://www.example.com')
+
+Multiple actions can be provided at once. Example:
+fill('a12', 'example with "quotes"')
+click('51')
+click('48', button='middle', modifiers=['Shift'])
+Multiple actions are meant to be executed sequentially without any feedback from the page.
+Don't execute multiple actions at once if you need feedback from the page.
+
+
+
+----------
+
+# Current Accessibility Tree:
+RootWebArea 'The Ultimate Answer', focused
+ [8] heading 'The Ultimate Answer'
+ [9] paragraph ''
+ StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
+ [10] button 'Click me', clickable
+
+# Previous Actions
+goto('http://localhost:8000')
+
+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")```
+"
diff --git a/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_003.log b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_003.log
new file mode 100644
index 0000000000..f183ccbb59
--- /dev/null
+++ b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/prompt_003.log
@@ -0,0 +1,132 @@
+
+
+----------
+
+# 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:
+Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
+
+# Action Space
+
+16 different types of actions are available.
+
+noop(wait_ms: float = 1000)
+ Examples:
+ noop()
+
+ noop(500)
+
+send_msg_to_user(text: str)
+ Examples:
+ send_msg_to_user('Based on the results of my search, the city was built in 1751.')
+
+scroll(delta_x: float, delta_y: float)
+ Examples:
+ scroll(0, 200)
+
+ scroll(-50.2, -100.5)
+
+fill(bid: str, value: str)
+ Examples:
+ fill('237', 'example value')
+
+ fill('45', 'multi-line\nexample')
+
+ fill('a12', 'example with "quotes"')
+
+select_option(bid: str, options: str | list[str])
+ Examples:
+ select_option('48', 'blue')
+
+ select_option('48', ['red', 'green', 'blue'])
+
+click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ click('51')
+
+ click('b22', button='right')
+
+ click('48', button='middle', modifiers=['Shift'])
+
+dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'Meta', 'Shift']] = [])
+ Examples:
+ dblclick('12')
+
+ dblclick('ca42', button='right')
+
+ dblclick('178', button='middle', modifiers=['Shift'])
+
+hover(bid: str)
+ Examples:
+ hover('b8')
+
+press(bid: str, key_comb: str)
+ Examples:
+ press('88', 'Backspace')
+
+ press('a26', 'Control+a')
+
+ press('a61', 'Meta+Shift+t')
+
+focus(bid: str)
+ Examples:
+ focus('b455')
+
+clear(bid: str)
+ Examples:
+ clear('996')
+
+drag_and_drop(from_bid: str, to_bid: str)
+ Examples:
+ drag_and_drop('56', '498')
+
+upload_file(bid: str, file: str | list[str])
+ Examples:
+ upload_file('572', 'my_receipt.pdf')
+
+ upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
+
+go_back()
+ Examples:
+ go_back()
+
+go_forward()
+ Examples:
+ go_forward()
+
+goto(url: str)
+ Examples:
+ goto('http://www.example.com')
+
+Multiple actions can be provided at once. Example:
+fill('a12', 'example with "quotes"')
+click('51')
+click('48', button='middle', modifiers=['Shift'])
+Multiple actions are meant to be executed sequentially without any feedback from the page.
+Don't execute multiple actions at once if you need feedback from the page.
+
+
+
+----------
+
+# Current Accessibility Tree:
+RootWebArea 'The Ultimate Answer', focused
+ [8] heading 'The Ultimate Answer'
+ [9] paragraph ''
+ StaticText 'Click the button to reveal the answer to life, the universe, and everything.'
+ [10] button 'Click me', clickable, focused
+ StaticText 'The answer is OpenDevin is all you need!'
+
+# Previous Actions
+goto('http://localhost:8000')
+click("10")
+
+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")```
+"
diff --git a/tests/integration/mock/BrowsingAgent/test_browse_internet/response_001.log b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/response_001.log
similarity index 100%
rename from tests/integration/mock/BrowsingAgent/test_browse_internet/response_001.log
rename to tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/response_001.log
diff --git a/tests/integration/mock/BrowsingAgent/test_browse_internet/response_002.log b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/response_002.log
similarity index 100%
rename from tests/integration/mock/BrowsingAgent/test_browse_internet/response_002.log
rename to tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/response_002.log
diff --git a/tests/integration/mock/BrowsingAgent/test_browse_internet/response_003.log b/tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/response_003.log
similarity index 100%
rename from tests/integration/mock/BrowsingAgent/test_browse_internet/response_003.log
rename to tests/integration/mock/server_runtime/BrowsingAgent/test_browse_internet/response_003.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/prompt_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/prompt_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/prompt_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/prompt_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_002.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/prompt_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_003.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/prompt_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_003.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/prompt_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_004.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/prompt_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_004.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/prompt_005.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_005.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/prompt_005.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_005.log
index 645ac5d1ef..72cc98e468 100644
--- a/tests/integration/mock/CodeActAgent/test_browse_internet/prompt_005.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/prompt_005.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Browse localhost:8000, and tell me the ultimate answer to life. Do not ask me for confirmation at any point.
----------
@@ -412,4 +410,4 @@ Sure! Let me browse the server's homepage at http://localhost:8000 to find the u
OBSERVATION:
{'content': 'The answer to life, the universe, and everything is: OpenDevin is all you need!'}
-ENVIRONMENT REMINDER: You have 8 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 8 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/response_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/response_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/response_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/response_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_002.log
diff --git a/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_003.log
new file mode 100644
index 0000000000..cfc29b9169
--- /dev/null
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_003.log
@@ -0,0 +1,2 @@
+In order to accomplish my goal, I need to click on the button with bid 10 to reveal the answer to life, the universe, and everything.
+```click("10"
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/response_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_004.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/response_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_004.log
diff --git a/tests/integration/mock/CodeActAgent/test_browse_internet/response_005.log b/tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_005.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_browse_internet/response_005.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_browse_internet/response_005.log
diff --git a/tests/integration/mock/CodeActAgent/test_edits/prompt_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_edits/prompt_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_edits/prompt_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_002.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_edits/prompt_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_002.log
index 14ca3b1ee8..b86926a956 100644
--- a/tests/integration/mock/CodeActAgent/test_edits/prompt_002.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_002.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
@@ -415,4 +413,4 @@ OBSERVATION:
[End of matches for "bad.txt" in ./]
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_edits/prompt_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_003.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_edits/prompt_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_003.log
index c154368638..e5d519666d 100644
--- a/tests/integration/mock/CodeActAgent/test_edits/prompt_003.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_003.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
@@ -434,4 +432,4 @@ OBSERVATION:
(this is the end of the file)
-ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_edits/prompt_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_004.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_edits/prompt_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_004.log
index 5234b65b2a..b6aa38ba38 100644
--- a/tests/integration/mock/CodeActAgent/test_edits/prompt_004.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_004.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
@@ -463,4 +461,4 @@ OBSERVATION:
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
-ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_edits/prompt_005.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_005.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_edits/prompt_005.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_005.log
index 9d3c125ec7..110fb844dc 100644
--- a/tests/integration/mock/CodeActAgent/test_edits/prompt_005.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/prompt_005.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Fix typos in bad.txt. Do not ask me for confirmation at any point.
----------
@@ -491,4 +489,4 @@ OBSERVATION:
[File updated (edited at line 3). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
-ENVIRONMENT REMINDER: You have 10 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 10 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_edits/response_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_edits/response_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_edits/response_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_edits/response_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_002.log
diff --git a/tests/integration/mock/CodeActAgent/test_edits/response_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_003.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_edits/response_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_003.log
diff --git a/tests/integration/mock/CodeActAgent/test_edits/response_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_004.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_edits/response_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_004.log
diff --git a/tests/integration/mock/CodeActAgent/test_edits/response_005.log b/tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_005.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_edits/response_005.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_edits/response_005.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython/prompt_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython/prompt_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython/prompt_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_002.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_ipython/prompt_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_002.log
index 544403208a..3d257a10fa 100644
--- a/tests/integration/mock/CodeActAgent/test_ipython/prompt_002.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_002.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
----------
@@ -415,4 +413,4 @@ OBSERVATION:
File created successfully.
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_ipython/prompt_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_003.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_ipython/prompt_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_003.log
index 1eb6cc6274..0990c25bf1 100644
--- a/tests/integration/mock/CodeActAgent/test_ipython/prompt_003.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/prompt_003.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
----------
@@ -430,4 +428,4 @@ OBSERVATION:
Content of /workspace/test.txt: hello world
-ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_ipython/response_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/response_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython/response_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython/response_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython/response_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/response_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython/response_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython/response_002.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython/response_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython/response_003.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython/response_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython/response_003.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/prompt_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_002.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/prompt_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_002.log
index 5b67d25233..61e76b991e 100644
--- a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_002.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_002.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
----------
@@ -413,4 +411,4 @@ OBSERVATION:
[Package installed successfully]
[Kernel restarted successfully to load the package]
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_003.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/prompt_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_003.log
index a5e6549bf3..9d966960bc 100644
--- a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_003.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_003.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
----------
@@ -429,4 +427,4 @@ OBSERVATION:
Version written to /workspace/test.txt
-ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_004.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/prompt_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_004.log
index c4c3175080..9fd341238d 100644
--- a/tests/integration/mock/CodeActAgent/test_ipython_module/prompt_004.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/prompt_004.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
----------
@@ -442,4 +440,4 @@ OBSERVATION:
pymsgbox version: 1.0.9
[Command -1 finished with exit code 0]
-ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/response_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/response_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/response_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/response_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_002.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/response_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_003.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/response_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_003.log
diff --git a/tests/integration/mock/CodeActAgent/test_ipython_module/response_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_004.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_ipython_module/response_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_ipython_module/response_004.log
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_002.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_002.log
index ca4f9fe02e..fe5ece61fc 100644
--- a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_002.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_002.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
----------
@@ -419,4 +417,4 @@ OBSERVATION:
[File hello.sh created.]
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_003.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_003.log
index 08061310e5..3223fc5c80 100644
--- a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_003.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_003.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
----------
@@ -437,4 +435,4 @@ OBSERVATION:
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
-ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_004.log
similarity index 99%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_004.log
index af83dd9d13..12fdb252d7 100644
--- a/tests/integration/mock/CodeActAgent/test_write_simple_script/prompt_004.log
+++ b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/prompt_004.log
@@ -396,8 +396,6 @@ The server is running on port 5000 with PID 126. You can access the list of numb
NOW, LET'S START!
-----------
-
Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.
----------
@@ -451,4 +449,4 @@ OBSERVATION:
hello
[Command -1 finished with exit code 0]
-ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/response_001.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_001.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/response_001.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_001.log
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/response_002.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_002.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/response_002.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_002.log
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/response_003.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_003.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/response_003.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_003.log
diff --git a/tests/integration/mock/CodeActAgent/test_write_simple_script/response_004.log b/tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_004.log
similarity index 100%
rename from tests/integration/mock/CodeActAgent/test_write_simple_script/response_004.log
rename to tests/integration/mock/server_runtime/CodeActAgent/test_write_simple_script/response_004.log
diff --git a/tests/integration/mock/server_runtime/CodeActSWEAgent/test_edits/prompt_001.log b/tests/integration/mock/server_runtime/CodeActSWEAgent/test_edits/prompt_001.log
new file mode 100644
index 0000000000..75f5cea76e
--- /dev/null
+++ b/tests/integration/mock/server_runtime/CodeActSWEAgent/test_edits/prompt_001.log
@@ -0,0 +1,594 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or 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 or instead of providing it.
+
+
+----------
+
+Here is an example of how you can interact with the environment for task solving:
+
+--- 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.
+
+create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+ls -F
+
+
+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.
+
+find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
+
+
+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.
+
+open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
+
+
+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.
+
+edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
+
+
+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.
+
+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)
+
+
+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.
+
+python reproduce.py
+
+
+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.
+
+rm reproduce.py
+
+
+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.
+
+exit
+
+
+--- END OF EXAMPLE ---
+
+
+NOW, LET'S START!
+
+----------
+
+Fix typos in bad.txt. Do not ask me for confirmation at any point.
+
+ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with .
diff --git a/tests/integration/mock/server_runtime/CodeActSWEAgent/test_edits/prompt_002.log b/tests/integration/mock/server_runtime/CodeActSWEAgent/test_edits/prompt_002.log
new file mode 100644
index 0000000000..069b0382b1
--- /dev/null
+++ b/tests/integration/mock/server_runtime/CodeActSWEAgent/test_edits/prompt_002.log
@@ -0,0 +1,613 @@
+
+
+----------
+
+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 "" tag, for example:
+
+print("Hello World!")
+
+The assistant can execute bash commands on behalf of the user by wrapping them with and .
+For example, you can list the files in the current directory by ls .
+The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: %pip install [package needed] and should always import packages and define variables before starting to use them.
+
+
+Apart from the standard Python library, the assistant can also use the following functions (already imported) in environment:
+open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None:
+ Opens the file at the given path in the editor. If line_number is provided, the window will be moved to include that line.
+ It only shows the first 100 lines by default! Max `context_lines` supported is 2000, use `scroll up/down`
+ to view the file if you want to see more.
+ Args:
+ path: str: The path to the file to open, preferred absolute path.
+ line_number: int | None = 1: The line number to move to. Defaults to 1.
+ context_lines: int | None = 100: Only shows this number of lines in the context window (usually from line 1), with line_number as the center (if possible). Defaults to 100.
+
+goto_line(line_number: int) -> None:
+ Moves the window to show the specified line number.
+ Args:
+ line_number: int: The line number to move to.
+
+scroll_down() -> None:
+ Moves the window down by 100 lines.
+ Args:
+ None
+
+scroll_up() -> None:
+ Moves the window up by 100 lines.
+ Args:
+ None
+
+create_file(filename: str) -> None:
+ Creates and opens a new file with the given name.
+ Args:
+ filename: str: The name of the file to create.
+
+edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
+ Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
+ Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc.
+ Include enough lines to make code in `to_replace` unique. `to_replace` should NOT be empty.
+ For example, given a file "/workspace/example.txt" with the following content:
+ ```
+ line 1
+ line 2
+ line 2
+ line 3
+ ```
+ EDITING: If you want to replace the second occurrence of "line 2", you can make `to_replace` unique:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='new line
+ line 3',
+ )
+ This will replace only the second "line 2" with "new line". The first "line 2" will remain unchanged.
+ The resulting file will be:
+ ```
+ line 1
+ line 2
+ new line
+ line 3
+ ```
+ REMOVAL: If you want to remove "line 2" and "line 3", you can set `new_content` to an empty string:
+ edit_file_by_replace(
+ '/workspace/example.txt',
+ to_replace='line 2
+ line 3',
+ new_content='',
+ )
+ Args:
+ file_name: str: The name of the file to edit.
+ to_replace: str: The content to search for and replace.
+ new_content: str: The new content to replace the old content with.
+
+insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
+ Insert content at the given line number in a file.
+ This will NOT modify the content of the lines before OR after the given line number.
+ For example, if the file has the following content:
+ ```
+ line 1
+ line 2
+ line 3
+ ```
+ and you call `insert_content_at_line('file.txt', 2, 'new line')`, the file will be updated to:
+ ```
+ line 1
+ new line
+ line 2
+ line 3
+ ```
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+append_file(file_name: str, content: str) -> None:
+ Append content to the given file.
+ It appends text `content` to the end of the specified file.
+ Args:
+ file_name: str: The name of the file to edit.
+ line_number: int: The line number (starting from 1) to insert the content after.
+ content: str: The content to insert.
+
+search_dir(search_term: str, dir_path: str = './') -> None:
+ Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
+ Args:
+ search_term: str: The term to search for.
+ dir_path: Optional[str]: The path to the directory to search.
+
+search_file(search_term: str, file_path: Optional[str] = None) -> None:
+ Searches for search_term in file. If file is not provided, searches in the current open file.
+ Args:
+ search_term: str: The term to search for.
+ file_path: Optional[str]: The path to the file to search.
+
+find_file(file_name: str, dir_path: str = './') -> None:
+ Finds all files with the given name in the specified directory.
+ Args:
+ file_name: str: The name of the file to find.
+ dir_path: Optional[str]: The path to the directory to search.
+
+parse_pdf(file_path: str) -> None:
+ Parses the content of a PDF file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_docx(file_path: str) -> None:
+ Parses the content of a DOCX file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_latex(file_path: str) -> None:
+ Parses the content of a LaTex file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+parse_pptx(file_path: str) -> None:
+ Parses the content of a pptx file and prints it.
+ Args:
+ file_path: str: The path to the file to open.
+
+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.
+
+The assistant's response should be concise.
+The assistant should include ONLY ONE or