Compare commits

..

60 Commits

Author SHA1 Message Date
Xingyao Wang 8d6eda3623 fix eval_infer.sh to correctly copy SWE-Bench logs (#4111) 2024-09-29 18:39:18 -05:00
Cole Murray d5f965b474 Update LiteLLLM to 1.48.5 (#4110) 2024-09-29 06:42:59 +00:00
tobitege c3bbe604eb (fix) Fix logging in shared eval file to prevent key disclosure (#4108) 2024-09-28 19:33:16 +00:00
Ana Noemi c7fe39998c Update README to decrease unsuccessful drivebys (#4091) 2024-09-28 18:52:01 +00:00
Xingyao Wang ec6e07647f fix hash equivalance verification ci for fork (#4107) 2024-09-29 02:19:59 +08:00
Graham Neubig e744eadb8b Robustify openhands resolver workflow (#4105) 2024-09-28 11:35:56 -04:00
Engel Nyst e582806004 Vision and prompt caching fixes (#4014) 2024-09-28 14:37:29 +02:00
OpenHands f427f9d8d4 Fix issue #4103: Improve description of how to do frontend setup and testing in .openhands_instructions (#4104)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-09-28 06:41:34 +00:00
Graham Neubig d669c7b60d Add github issue resolution workflow (#4102) 2024-09-28 04:52:52 +00:00
dependabot[bot] 42be4ee5bc chore(deps-dev): bump openai from 1.48.0 to 1.50.2 (#4101)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-28 05:04:03 +02:00
Engel Nyst f994277d0f Make agents follow configured temperature (#4099) 2024-09-28 01:15:46 +00:00
tofarr 5ccee7c8a7 Fix Bash commands now do not block and actually respect the timeout (#4058) 2024-09-28 08:40:00 +08:00
tobitege 575a829d94 (enh) add test_python_version to test_bash.py runtime tests (#4098) 2024-09-28 08:21:14 +08:00
Xingyao Wang 2bed3a424c chore: pass logger DEBUG mode to client side (#4096) 2024-09-28 08:21:04 +08:00
Xingyao Wang a4cc010110 chore: parser fix for deepseek (#4097) 2024-09-28 08:20:51 +08:00
tobitege 9651368e6a revert #3871 dockerfile template: don't write to .bashrc file (#4095) 2024-09-27 21:49:51 +00:00
tofarr c5025fb66e Fix Reducing the amount being downloaded every time the hash changes. (#4078) 2024-09-27 15:48:33 -06:00
Robert Brennan 3f9111c615 add idle time to client server (#4084) 2024-09-27 19:41:16 +00:00
dependabot[bot] 89e95f2671 chore(deps): bump boto3 from 1.35.27 to 1.35.28 (#4090)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 16:48:34 +00:00
dependabot[bot] 5bfa0c2f8d chore(deps): bump browsergym from 0.7.0 to 0.7.1 (#4089)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 16:21:58 +00:00
dependabot[bot] 84141f656d chore(deps-dev): bump chromadb from 0.5.9 to 0.5.11 (#4088)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 16:19:17 +00:00
dependabot[bot] 6ff7506581 chore(deps-dev): bump reportlab from 4.2.2 to 4.2.4 (#4086)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 16:01:32 +00:00
dependabot[bot] 41dc7f0256 chore(deps-dev): bump llama-index from 0.11.13 to 0.11.14 (#4085)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 15:50:17 +00:00
Xingyao Wang 34f3b61536 [runtime hash] fix runtime hash mismatch between inside app image and in "development mode" (#4039) 2024-09-27 15:26:26 +00:00
dependabot[bot] 4533c47595 chore(deps-dev): bump @types/node from 22.7.2 to 22.7.3 in /frontend (#4081)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 15:20:11 +00:00
Xingyao Wang 47774e60b0 chore: remove deprecated dockerfile (#4079) 2024-09-27 15:03:23 +00:00
Robert Brennan b78f646b65 Release 0.9.6 (#4076) 2024-09-26 21:27:17 +00:00
Amir 3e5c01dfc8 Remove param from docstring that does not exist in the append_file (#4060) 2024-09-26 22:25:11 +02:00
tobitege 29c34e0b6a (fix) actions.ts: restored handleAssistantMessage handling order (#4074) 2024-09-26 19:56:12 +00:00
tofarr c919086e25 Fix for regression (#4075)
Regression fixed
2024-09-26 12:58:00 -06:00
Engel Nyst 0a03c802f5 Refactor llm.py (#4057) 2024-09-26 17:44:18 +00:00
Xingyao Wang 081ebdbdd8 [runtime] do not keep rebuilding from generic image (#4072) 2024-09-26 17:19:46 +00:00
dependabot[bot] 572c7b726d chore(deps-dev): bump ruff from 0.6.7 to 0.6.8 (#4067)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 17:19:16 +00:00
Xingyao Wang cfc5bb70c1 Update README.md for CodeAct (#4070) 2024-09-26 16:55:08 +00:00
dependabot[bot] 008b866a38 chore(deps-dev): bump jsdom from 25.0.0 to 25.0.1 in /frontend (#3992)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 16:34:29 +00:00
dependabot[bot] 676ad3e140 chore(deps-dev): bump chromadb from 0.5.7 to 0.5.9 (#4069)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 16:08:17 +00:00
dependabot[bot] 19278de5d0 chore(deps): bump json-repair from 0.29.4 to 0.29.5 (#4068)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 15:48:39 +00:00
dependabot[bot] 891e4a8d34 chore(deps): bump datasets from 3.0.0 to 3.0.1 (#4065)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 15:35:36 +00:00
dependabot[bot] 85be8607e0 chore(deps): bump litellm from 1.48.1 to 1.48.2 (#4066)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 23:33:24 +08:00
dependabot[bot] 49b244610c chore(deps-dev): bump openai from 1.47.1 to 1.48.0 (#4063)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 17:16:59 +02:00
dependabot[bot] b347b1d06f chore(deps): bump boto3 from 1.35.26 to 1.35.27 (#4064)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 17:15:11 +02:00
dependabot[bot] 0c86a60b35 chore(deps-dev): bump @types/node from 22.7.0 to 22.7.2 in /frontend (#4062)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 19:11:06 +04:00
tofarr 01317138e2 Fix: uvicorn reloading when python files in workspace change, & started section for debugging instructions for developers (#4041)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2024-09-26 08:57:37 -06:00
Xingyao Wang e03855cd7f Make sure we print the observation in the same way as the LLM sees it 2024-09-26 14:01:48 +00:00
jaki300 757c9593f1 Create gke-example.md (#3795)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-09-26 09:11:33 -04:00
mamoodi 266e8ff951 Release 0.9.5 (#4061) 2024-09-26 08:36:31 -04:00
dependabot[bot] 3e79cd12a6 chore(deps-dev): bump @types/react from 18.3.8 to 18.3.9 in /frontend (#4029)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 14:41:21 +04:00
tobitege 2cc1c3ef42 (enh) Docker runtime builder with BuildKit support, enh. caching (#4009) 2024-09-26 08:50:53 +02:00
dependabot[bot] ef0b08a46e chore(deps-dev): bump tailwindcss from 3.4.12 to 3.4.13 in /frontend (#4030)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 05:28:03 +00:00
dependabot[bot] f1d5202884 chore(deps): bump vite from 5.4.7 to 5.4.8 in /frontend (#4046) 2024-09-26 12:56:35 +08:00
dependabot[bot] 11cedfb854 chore(deps): bump google-cloud-aiplatform from 1.67.1 to 1.68.0 (#4051) 2024-09-26 12:56:16 +08:00
dependabot[bot] 6d103a0db2 chore(deps-dev): bump @types/node from 22.6.1 to 22.7.0 in /frontend (#4047) 2024-09-26 12:56:00 +08:00
Engel Nyst 798aaeaef6 remove Exception in the agent (#4054) 2024-09-26 06:39:17 +02:00
tofarr 0df4b97e5b Fix startup statuses (#4053) 2024-09-25 14:38:32 -06:00
Xingyao Wang 81b3cd71b3 [eval] log evaluating warnings directly to console (#4026) 2024-09-26 03:42:32 +08:00
Robert Brennan 9241ae2148 Fix persistence of "advanced settings" (#4038) 2024-09-25 12:57:08 -04:00
dependabot[bot] d3f86e052a chore(deps-dev): bump llama-index from 0.11.12 to 0.11.13 (#4044)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 18:24:09 +02:00
dependabot[bot] e0c65f8f9c chore(deps): bump google-generativeai from 0.8.1 to 0.8.2 (#4050)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 18:22:01 +02:00
dependabot[bot] 394ab360a8 chore(deps): bump boto3 from 1.35.25 to 1.35.26 (#4048)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 18:21:27 +02:00
dependabot[bot] 8a146d5ced chore(deps): bump litellm from 1.48.0 to 1.48.1 (#4049)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 18:20:47 +02:00
128 changed files with 2563 additions and 1752 deletions
+25 -7
View File
@@ -14,20 +14,38 @@ jobs:
runs-on: ubuntu-latest
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,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: 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'
- name: Set up environment
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install --without evaluation,llama-index
poetry run playwright install --with-deps chromium
wget https://huggingface.co/BAAI/bge-small-en-v1.5/raw/main/1_Pooling/config.json -P /tmp/llama_index/models--BAAI--bge-small-en-v1.5/snapshots/5c38ec7c405ec4b44b94cc5a9bb96e735b38267a/1_Pooling/
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: |
set -e
poetry run python openhands/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
poetry run python3 openhands/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
- name: Check exit code
run: |
if [ $? -ne 0 ]; then
@@ -25,7 +25,71 @@ on:
required: true
default: ''
env:
BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.11-nodejs22
jobs:
# Builds the OpenHands Docker images
ghcr_build_app:
name: Build App Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
hash_from_app_image: ${{ steps.get_hash_in_app_image.outputs.hash_from_app_image }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Build and push app image
if: "!github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ github.repository_owner }} --push
- name: Build app image
if: "github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ github.repository_owner }} --load
- name: Get hash in App Image
id: get_hash_in_app_image
run: |
# Lowercase the repository owner
export REPO_OWNER=${{ github.repository_owner }}
REPO_OWNER=$(echo $REPO_OWNER | tr '[:upper:]' '[:lower:]')
# Run the build script in the app image
docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${REPO_OWNER}/openhands:${{ github.sha }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt
# Get the hash from the build script
hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT
echo "Hash from app image: $hash_from_app_image"
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Image
@@ -56,7 +120,9 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v3.0.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
uses: docker/login-action@v3
with:
@@ -88,7 +154,7 @@ jobs:
- name: Build and push runtime image ${{ matrix.base_image.image }}
if: github.event.pull_request.head.repo.fork != true
run: |
./containers/build.sh runtime ${{ github.repository_owner }} --push ${{ matrix.base_image.tag }}
./containers/build.sh -i runtime -o ${{ github.repository_owner }} --push -t ${{ matrix.base_image.tag }}
# Forked repos can't push to GHCR, so we need to upload the image as an artifact
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
@@ -104,6 +170,56 @@ jobs:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp/runtime-${{ matrix.base_image.tag }}.tar
verify_hash_equivalence_in_runtime_and_app:
name: Verify Hash Equivalence in Runtime and Docker images
runs-on: ubuntu-latest
needs: [ghcr_build_runtime, ghcr_build_app]
strategy:
fail-fast: false
matrix:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Get hash in App Image
run: |
echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
echo "hash_from_app_image=${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" >> $GITHUB_ENV
- name: Get hash using code (development mode)
run: |
mkdir -p containers/runtime
poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild > output.txt 2>&1
hash_from_code=$(cat output.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_code=$hash_from_code" >> $GITHUB_ENV
- name: Compare hashes
run: |
echo "Hash from App Image: ${{ env.hash_from_app_image }}"
echo "Hash from Code: ${{ env.hash_from_code }}"
if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then
echo "Hashes match!"
else
echo "Hashes do not match!"
exit 1
fi
# Run unit tests with the EventStream runtime Docker images as root
test_runtime_root:
name: RT Unit Tests (Root)
@@ -115,6 +231,23 @@ jobs:
base_image: ['nikolaik']
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,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
if: github.event.pull_request.head.repo.fork
@@ -176,6 +309,23 @@ jobs:
base_image: ['nikolaik']
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,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
if: github.event.pull_request.head.repo.fork
@@ -238,6 +388,23 @@ jobs:
base_image: ['nikolaik']
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,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
if: github.event.pull_request.head.repo.fork
@@ -290,7 +457,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux]
needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: All tests passed
run: echo "All runtime tests have passed successfully!"
@@ -299,7 +466,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux]
needs: [test_runtime_root, test_runtime_oh, runtime_integration_tests_on_linux, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: Some tests failed
run: |
-65
View File
@@ -1,65 +0,0 @@
# Workflow that builds, tests and then pushes the app docker images to the ghcr.io repository
name: Build and Publish App Image
# Always run on "main"
# Always run on tags
# Always run on PRs
# Can also be triggered manually
on:
push:
branches:
- main
tags:
- '*'
pull_request:
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: true
default: ''
jobs:
# Builds the OpenHands Docker images
ghcr_build:
name: Build App Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Build and push app image
if: "!github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh openhands ${{ github.repository_owner }} --push
- name: Build app image
if: "github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh openhands image ${{ github.repository_owner }}
+126
View File
@@ -0,0 +1,126 @@
name: Auto-Fix Tagged Issues with OpenHands
on:
issues:
types: [labeled]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
auto-fix:
if: github.event.label.name == 'fix-me'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Comment on issue with start message
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `OpenHands started fixing the issue! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
});
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install openhands-resolver
- name: Attempt to resolve issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
run: |
python -m openhands_resolver.resolve_issues \
--repo ${{ github.repository }} \
--issue-numbers ${{ github.event.issue.number }}
- name: Check resolution result
id: check_result
run: |
if grep -q '"success":true' output/output.jsonl; then
echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT
else
echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT
fi
- name: Create draft PR or push branch
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
run: |
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
python -m openhands_resolver.send_pull_request \
--issue-number ${{ github.event.issue.number }} \
--pr-type draft | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
python -m openhands_resolver.send_pull_request \
--issue-number ${{ github.event.issue.number }} \
--pr-type branch \
--send-on-failure | tee branch_result.txt && \
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
fi
- name: Comment on issue
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
const issueNumber = context.issue.number;
const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }};
let prNumber = '';
let branchName = '';
try {
if (success) {
prNumber = fs.readFileSync('pr_number.txt', 'utf8').trim();
} else {
branchName = fs.readFileSync('branch_name.txt', 'utf8').trim();
}
} catch (error) {
console.error('Error reading file:', error);
}
if (success && prNumber) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.`
});
} else if (!success && branchName) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`
});
} else {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Please check the workflow logs for more information.`
});
}
+6
View File
@@ -89,6 +89,9 @@ jobs:
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: Build Environment
run: make build
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Run Tests
run: poetry run pytest --forked --cov=agenthub --cov=openhands --cov-report=xml ./tests/unit
- name: Upload coverage to Codecov
@@ -107,6 +110,9 @@ jobs:
python-version: ['3.11']
steps:
- uses: actions/checkout@v4
- 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
@@ -29,6 +29,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Set up Python
uses: actions/setup-python@v5
with:
+3
View File
@@ -15,6 +15,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Set up Python
uses: actions/setup-python@v5
with:
-113
View File
@@ -1,113 +0,0 @@
# Workflow that uses OpenHands to resolve a GitHub issue. Issue must be labeled 'solve-this'
name: Use OpenHands to Resolve GitHub Issue
on:
issues:
types: [labeled]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
dogfood:
if: github.event.label.name == 'solve-this'
runs-on: ubuntu-latest
container:
image: ghcr.io/all-hands-ai/openhands
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 }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
echo "TITLE:" > task.txt
echo "${ISSUE_TITLE}" >> task.txt
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 OpenHands
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 }}
run: |
# Append path to launch poetry
export PATH="/github/home/.local/bin:$PATH"
# Append path to correctly import package, note: must set pwd at first
export PYTHONPATH=$(pwd):$PYTHONPATH
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE poetry run python ./openhands/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
git config --global --add safe.directory $PWD
git config --global user.name 'OpenHands'
git config --global user.email 'OpenHands@users.noreply.github.com'
# Create a unique branch name with a timestamp
BRANCH_NAME="fix/${{ github.event.issue.number }}-$(date +%Y%m%d%H%M%S)"
# Checkout new branch
git checkout -b $BRANCH_NAME
# Add all changes to staging, except task.txt
git add --all -- ':!task.txt'
# Commit the changes, if any
git commit -m "OpenHands: Resolve Issue #${{ github.event.issue.number }}"
if [ $? -ne 0 ]; then
echo "No changes to commit."
exit 0
fi
# Push changes
git push --set-upstream origin $BRANCH_NAME
- name: Fetch Default Branch
env:
GH_TOKEN: ${{ github.token }}
run: |
# Fetch the default branch using gh cli
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 }}
run: |
# Create PR and capture URL
PR_URL=$(gh pr create \
--title "OpenHands: Resolve Issue #2" \
--body "This PR was generated by OpenHands to resolve issue #2" \
--repo "foragerr/OpenHands" \
--head "${{ github.head_ref }}" \
--base "${{ env.DEFAULT_BRANCH }}" \
| grep -o 'https://github.com/[^ ]*')
# Extract PR number from URL
PR_NUMBER=$(echo "$PR_URL" | grep -o '[0-9]\+$')
# Set environment vars
echo "PR_URL=$PR_URL" >> $GITHUB_ENV
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
- name: Post Comment
env:
GH_TOKEN: ${{ github.token }}
run: |
gh issue comment ${{ github.event.issue.number }} \
-b "OpenHands raised [PR #${{ env.PR_NUMBER }}](${{ env.PR_URL }}) to resolve this issue."
-2
View File
@@ -217,8 +217,6 @@ config.toml
config.toml_
config.toml.bak
containers/agnostic_sandbox
# swe-bench-eval
image_build_logs
run_instance_logs
+27
View File
@@ -0,0 +1,27 @@
OpenHands is an automated AI software engineer. It is a repo with a Python backend
(in the `openhands` directory) and TypeScript frontend (in the `frontend` directory).
General Setup:
- To set up the entire repo, including frontend and backend, run `make build`
Backend:
- Located in the `openhands` directory
- Testing:
- All tests are in `tests/unit/test_*.py`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
Frontend:
- Located in the `frontend` directory
- Prerequisites: A recent version of NodeJS / NPM
- Setup: Run `npm install` in the frontend directory
- Testing:
- Run tests: `npm run test`
- To run specific tests: `npm run test -- -t "TestName"`
- Building:
- Build for production: `npm run build`
- Environment Variables:
- Set in `frontend/.env` or as environment variables
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
- Internationalization:
- Generate i18n declaration file: `npm run make-i18n`
+1 -1
View File
@@ -190,7 +190,7 @@ build-frontend:
# Start backend
start-backend:
@echo "$(YELLOW)Starting backend...$(RESET)"
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "workspace/*"
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "$(shell pwd)/workspace"
# Start frontend
start-frontend:
+6
View File
@@ -42,6 +42,8 @@ system requirements and more information.
```bash
export WORKSPACE_BASE=$(pwd)/workspace
docker pull ghcr.io/all-hands-ai/runtime:0.9-nikolaik
docker run -it --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
@@ -56,6 +58,10 @@ docker run -it --pull=always \
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
You'll need a model provider and API key. One option that works well: [Claude 3.5 Sonnet](https://www.anthropic.com/api), but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
---
You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
or as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode).
@@ -218,7 +218,6 @@ class BrowsingAgent(Agent):
response = self.llm.completion(
messages=self.llm.format_messages_for_llm(messages),
temperature=0.0,
stop=[')```', ')\n```'],
)
return self.response_parser.parse(response)
-17
View File
@@ -10,20 +10,3 @@ The conceptual idea is illustrated below. At each turn, the agent can:
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
![image](https://github.com/All-Hands-AI/OpenHands/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3)
## Plugin System
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenHands's plugin system:
- [Jupyter plugin](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/runtime/plugins/jupyter): for IPython execution via bash command
- [Agent Skills plugin](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/runtime/plugins/agent_skills): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
## Demo
https://github.com/All-Hands-AI/OpenHands/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
## Work-in-progress & Next step
[] Support web-browsing
[] Complete the workflow for CodeAct agent to submit Github PRs
+5 -31
View File
@@ -6,7 +6,6 @@ from openhands.events.action import (
AgentDelegateAction,
AgentFinishAction,
CmdRunAction,
FileEditAction,
IPythonRunCellAction,
MessageAction,
)
@@ -17,7 +16,6 @@ class CodeActResponseParser(ResponseParser):
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
- FileEditAction(diff_block) - Search/Replace block to edit.
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
@@ -30,7 +28,6 @@ class CodeActResponseParser(ResponseParser):
CodeActActionParserCmdRun(),
CodeActActionParserIPythonRunCell(),
CodeActActionParserAgentDelegate(),
CodeActActionParserFileEdit(),
]
self.default_parser = CodeActActionParserMessage()
@@ -42,7 +39,11 @@ class CodeActResponseParser(ResponseParser):
action = response.choices[0].message.content
if action is None:
return ''
for lang in ['bash', 'ipython', 'edit', 'browse']:
for lang in ['bash', 'ipython', 'browse']:
# special handling for DeepSeek: it has stop-word bug and returns </execute_ipython instead of </execute_ipython>
if f'</execute_{lang}' in action and f'</execute_{lang}>' not in action:
action = action.replace(f'</execute_{lang}', f'</execute_{lang}>')
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
action += f'</execute_{lang}>'
return action
@@ -161,33 +162,6 @@ class CodeActActionParserAgentDelegate(ActionParser):
return AgentDelegateAction(agent='BrowsingAgent', inputs={'task': task})
class CodeActActionParserFileEdit(ActionParser):
"""Parser action:
- FileEditAction(diff_block) - Search/Replace block to edit.
"""
def __init__(
self,
):
self.diff_block = None
def check_condition(self, action_str: str) -> bool:
self.diff_block = re.search(
r'<execute_edit>(.*)</execute_edit>', action_str, re.DOTALL
)
return self.diff_block is not None
def parse(self, action_str: str) -> Action:
assert (
self.diff_block is not None
), 'self.diff_block should not be None when parse is called'
thought = action_str.replace(self.diff_block.group(0), '').strip()
return FileEditAction(
diff_block=self.diff_block.group(1).strip(),
thought=thought,
)
class CodeActActionParserMessage(ActionParser):
"""Parser action:
- MessageAction(content) - Message action to run (e.g. ask for clarification)
+3 -30
View File
@@ -5,22 +5,18 @@ from agenthub.codeact_agent.action_parser import CodeActResponseParser
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.exceptions import OperationCancelled
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import ImageContent, Message, TextContent
from openhands.events.action import (
Action,
AgentDelegateAction,
AgentFinishAction,
CmdRunAction,
FileEditAction,
IPythonRunCellAction,
MessageAction,
)
from openhands.events.observation import (
AgentDelegateObservation,
CmdOutputObservation,
FileEditObservation,
IPythonRunCellObservation,
UserRejectObservation,
)
@@ -38,7 +34,7 @@ from openhands.utils.prompt import PromptManager
class CodeActAgent(Agent):
VERSION = '1.10'
VERSION = '1.9'
"""
The Code Act Agent is a minimalist agent.
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
@@ -106,8 +102,6 @@ class CodeActAgent(Agent):
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
elif isinstance(action, AgentDelegateAction):
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
elif isinstance(action, FileEditAction):
return f'{action.thought}\n<execute_edit>\n{action.diff_block}\n</execute_edit>'
elif isinstance(action, MessageAction):
return action.content
elif isinstance(action, AgentFinishAction) and action.source == 'agent':
@@ -117,7 +111,6 @@ class CodeActAgent(Agent):
def get_action_message(self, action: Action) -> Message | None:
if (
isinstance(action, AgentDelegateAction)
or isinstance(action, FileEditAction)
or isinstance(action, CmdRunAction)
or isinstance(action, IPythonRunCellAction)
or isinstance(action, MessageAction)
@@ -158,9 +151,6 @@ class CodeActAgent(Agent):
text = '\n'.join(splitted)
text = truncate_content(text, max_message_chars)
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, FileEditObservation):
text = obs_prefix + truncate_content(obs.content, max_message_chars)
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, AgentDelegateObservation):
text = obs_prefix + truncate_content(
obs.outputs['content'] if 'content' in obs.outputs else '',
@@ -172,7 +162,7 @@ class CodeActAgent(Agent):
text += '\n[Error occurred in processing last action]'
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, UserRejectObservation):
text = obs_prefix + truncate_content(obs.content, max_message_chars)
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
text += '\n[Last action has been rejected by the user]'
return Message(role='user', content=[TextContent(text=text)])
else:
@@ -195,7 +185,6 @@ class CodeActAgent(Agent):
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
- FileEditAction(diff_block) - Search/Replace block to edit.
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
@@ -212,26 +201,10 @@ class CodeActAgent(Agent):
'</execute_ipython>',
'</execute_bash>',
'</execute_browse>',
'</execute_edit>',
],
}
if self.llm.is_caching_prompt_active():
params['extra_headers'] = {
'anthropic-beta': 'prompt-caching-2024-07-31',
}
# TODO: move exception handling to agent_controller
try:
response = self.llm.completion(**params)
except OperationCancelled as e:
raise e
except Exception as e:
logger.error(f'{e}')
error_message = '{}: {}'.format(type(e).__name__, str(e).split('\n')[0])
return AgentFinishAction(
thought=f'Agent encountered an error while processing the last action.\nError: {error_message}\nPlease try again.'
)
response = self.llm.completion(**params)
return self.action_parser.parse(response)
+5 -27
View File
@@ -19,44 +19,22 @@ the assistant should retry running the command in the background.
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
{% endset %}
{% set EDIT_DIFF_PREFIX %}
The assistant can edit files with <execute_edit> and </execute_edit>. Each change must be described with a SEARCH/REPLACE block.
Every SEARCH section must EXACTLY MATCH the existing file content, character for character, including all comments, docstrings, etc. SEARCH/REPLACE blocks will replace all matching occurrences. Include enough lines to make the SEARCH blocks uniquely match the lines to change.
Keep SEARCH/REPLACE blocks as concise as possible. Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.
To move code within a file, use 2 SEARCH/REPLACE blocks: 1 to delete it from its current location, 1 to insert it in the new location.
If you want to put code in a new file, use a SEARCH/REPLACE block with: a new file path, an empty `SEARCH` section and the new file's contents in the `REPLACE` section.
Every SEARCH/REPLACE block must use this format:
1. The FULL file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE
For example,
<execute_edit>
demo.py
<<<<<<< SEARCH
print("hello")
=======
print("goodbye")
>>>>>>> REPLACE
</execute_edit>
{% endset %}
{% set PIP_INSTALL_PREFIX %}
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
{% endset %}
{% set SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + EDIT_DIFF_PREFIX + PIP_INSTALL_PREFIX %}
{% set SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX %}
{% set COMMAND_DOCS %}
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
{{ agent_skills_docs }}
IMPORTANT:
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
- The assistant shall adhere to THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRING PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write the line out, with all leading spaces before the code!
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
- Any code issued should be less than 50 lines to avoid context being cut off!
- After EVERY `create_file` the method `append_file` shall be used to write the FIRST content!
- For `edit_file_by_replace` NEVER provide empty parameters!
- For `edit_file_by_replace` the file must be read fully before any replacements!
{% endset %}
{% set SYSTEM_SUFFIX %}
Responses should be concise.
+58 -26
View File
@@ -4,12 +4,23 @@
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
ASSISTANT:
Sure! Let me write the Python code for starting a web server and save it to a new Python file `app.py`:
<execute_edit>
app.py
<<<<<<< SEARCH
=======
from flask import Flask
Sure! Let me create a Python file `app.py`:
<execute_ipython>
create_file('app.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/app.py (1 lines total)]
(this is the beginning of the file)
1|
(this is the end of the file)
[File app.py created.]
ASSISTANT:
Now I will write the Python code for starting a web server and save it to the file `app.py`:
<execute_ipython>
EDITED_CODE="""from flask import Flask
app = Flask(__name__)
@app.route('/')
@@ -18,17 +29,30 @@ def index():
return str(numbers)
if __name__ == '__main__':
app.run(port=5000)
>>>>>>> REPLACE
</execute_edit>
app.run(port=5000)"""
insert_content_at_line(
'app.py',
1,
EDITED_CODE,
)
</execute_ipython>
USER:
OBSERVATION:
I updated the file app.py by
appending:
from flask import Flask
app = Flask(__name__)
(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:
@@ -153,22 +177,30 @@ USER:
ASSISTANT:
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
<execute_edit>
app.py
<<<<<<< SEARCH
return str(numbers)
=======
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
>>>>>>> REPLACE
</execute_edit>
<execute_ipython>
edit_file_by_replace(
'app.py',
to_replace=" return str(numbers)",
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
)
</execute_ipython>
USER:
Observation:
I updated the file app.py by
replacing:
return str(numbers)
with:
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
[File: /workspace/app.py (10 lines total after edit)]
(this is the beginning of the file)
1|from flask import Flask
2|app = Flask(__name__)
3|
4|@app.route('/')
5|def index():
6| numbers = list(range(1, 11))
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
8|
9|if __name__ == '__main__':
10| app.run(port=5000)
(this is the end of the file)
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Running the updated file:
@@ -166,7 +166,6 @@ class CodeActSWEAgent(Agent):
'</execute_ipython>',
'</execute_bash>',
],
temperature=0.0,
)
return self.response_parser.parse(response)
-1
View File
@@ -78,7 +78,6 @@ class MicroAgent(Agent):
message = Message(role='user', content=content)
resp = self.llm.completion(
messages=self.llm.format_messages_for_llm(message),
temperature=0.0,
)
action_resp = resp['choices'][0]['message']['content']
action = parse_response(action_resp)
+5 -5
View File
@@ -112,7 +112,7 @@ api_key = "your-api-key"
#embedding_deployment_name = ""
# Embedding model to use
embedding_model = ""
embedding_model = "local"
# Maximum number of characters in an observation's content
#max_message_chars = 10000
@@ -146,8 +146,8 @@ model = "gpt-4o"
# Drop any unmapped (unsupported) params without causing an exception
#drop_params = false
# Using the prompt caching feature provided by the LLM
#caching_prompt = false
# Using the prompt caching feature if provided by the LLM and supported
#caching_prompt = true
# Base URL for the OLLAMA API
#ollama_base_url = ""
@@ -188,7 +188,7 @@ model = "gpt-4o-mini"
#memory_max_threads = 2
# LLM config group to use
#llm_config = 'llm'
#llm_config = 'your-llm-config-group'
[agent.RepoExplorerAgent]
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
@@ -232,7 +232,7 @@ llm_config = 'gpt3'
[security]
# Enable confirmation mode
#confirmation_mode = true
#confirmation_mode = false
# The security analyzer to use
#security_analyzer = ""
+5 -4
View File
@@ -70,10 +70,11 @@ RUN playwright install --with-deps chromium
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:app --chmod=770 ./agenthub ./agenthub
COPY --chown=openhands:app --chmod=770 ./pyproject.toml ./pyproject.toml
COPY --chown=openhands:app --chmod=770 ./poetry.lock ./poetry.lock
COPY --chown=openhands:app --chmod=770 ./README.md ./README.md
COPY --chown=openhands:app --chmod=770 ./MANIFEST.in ./MANIFEST.in
COPY --chown=openhands:app ./pyproject.toml ./pyproject.toml
COPY --chown=openhands:app ./poetry.lock ./poetry.lock
COPY --chown=openhands:app ./README.md ./README.md
COPY --chown=openhands:app ./MANIFEST.in ./MANIFEST.in
COPY --chown=openhands:app ./LICENSE ./LICENSE
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
RUN python openhands/core/download.py # No-op to download assets
+54 -6
View File
@@ -1,13 +1,40 @@
#!/bin/bash
set -eo pipefail
image_name=$1
org_name=$2
# Initialize variables with default values
image_name=""
org_name=""
push=0
if [[ $3 == "--push" ]]; then
push=1
load=0
tag_suffix=""
# Function to display usage information
usage() {
echo "Usage: $0 -i <image_name> [-o <org_name>] [--push] [--load] [-t <tag_suffix>]"
echo " -i: Image name (required)"
echo " -o: Organization name"
echo " --push: Push the image"
echo " --load: Load the image"
echo " -t: Tag suffix"
exit 1
}
# Parse command-line options
while [[ $# -gt 0 ]]; do
case $1 in
-i) image_name="$2"; shift 2 ;;
-o) org_name="$2"; shift 2 ;;
--push) push=1; shift ;;
--load) load=1; shift ;;
-t) tag_suffix="$2"; shift 2 ;;
*) usage ;;
esac
done
# Check if required arguments are provided
if [[ -z "$image_name" ]]; then
echo "Error: Image name is required."
usage
fi
tag_suffix=$4
echo "Building: $image_name"
tags=()
@@ -95,14 +122,35 @@ if [[ $push -eq 1 ]]; then
args+=" --cache-to=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag,mode=max"
fi
if [[ $load -eq 1 ]]; then
args+=" --load"
fi
echo "Args: $args"
# Modify the platform selection based on --load flag
if [[ $load -eq 1 ]]; then
# When loading, build only for the current platform
platform=$(docker version -f '{{.Server.Os}}/{{.Server.Arch}}')
else
# For push or without load, build for multiple platforms
platform="linux/amd64,linux/arm64"
fi
echo "Building for platform(s): $platform"
docker buildx build \
$args \
--build-arg OPENHANDS_BUILD_VERSION="$OPENHANDS_BUILD_VERSION" \
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag \
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag_base-main \
--platform linux/amd64,linux/arm64 \
--platform $platform \
--provenance=false \
-f "$dir/Dockerfile" \
"$DOCKER_BASE_DIR"
# If load was requested, print the loaded images
if [[ $load -eq 1 ]]; then
echo "Local images built:"
docker images "$DOCKER_REPOSITORY" --format "{{.Repository}}:{{.Tag}}"
fi
-44
View File
@@ -1,44 +0,0 @@
FROM ubuntu:22.04
# install basic packages
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
vim \
nano \
unzip \
zip \
python3 \
python3-pip \
python3-venv \
python3-dev \
build-essential \
openssh-server \
sudo \
gcc \
jq \
g++ \
make \
iproute2 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m0755 /var/run/sshd
# symlink python3 to python
RUN ln -s /usr/bin/python3 /usr/bin/python
# ==== OpenHands Runtime Client ====
RUN mkdir -p /openhands && mkdir -p /openhands/logs && chmod 777 /openhands/logs
RUN wget --progress=bar:force -O Miniforge3.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
RUN bash Miniforge3.sh -b -p /openhands/miniforge3
RUN chmod -R g+w /openhands/miniforge3
RUN bash -c ". /openhands/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"
RUN echo "" > /openhands/bash.bashrc
RUN rm -f Miniforge3.sh
# - agentskills dependencies
RUN /openhands/miniforge3/bin/pip install --upgrade pip
RUN /openhands/miniforge3/bin/pip install jupyterlab notebook jupyter_kernel_gateway flake8
RUN /openhands/miniforge3/bin/pip install python-docx PyPDF2 python-pptx pylatexenc openai
RUN /openhands/miniforge3/bin/pip install python-dotenv toml termcolor pydantic python-docx pyyaml docker pexpect tenacity e2b browsergym minio
-4
View File
@@ -1,4 +0,0 @@
DOCKER_REGISTRY=ghcr.io
DOCKER_ORG=all-hands-ai
DOCKER_IMAGE=sandbox
DOCKER_BASE_DIR="."
+3 -1
View File
@@ -18,6 +18,8 @@ existing code that you'd like to modify.
```bash
export WORKSPACE_BASE=$(pwd)/workspace
docker pull ghcr.io/all-hands-ai/runtime:0.9-nikolaik
docker run -it --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
@@ -54,7 +56,7 @@ The `Advanced Options` also allow you to specify a `Base URL` if required.
## Versions
The command above pulls the `0.9` tag, which represents the most recent stable release of OpenHands. You have other options as well:
The command above pulls the most recent stable release of OpenHands. You have other options as well:
- For a specific release, use `ghcr.io/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number.
- We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
- For the most up-to-date development version, you can use `ghcr.io/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only.
+71
View File
@@ -0,0 +1,71 @@
# Debugging
The following is intended as a primer on debugging OpenHands for Development purposes.
## Server / VSCode
The following `launch.json` will allow debugging the agent, controller and server elements, but not the sandbox (Which runs inside docker). It will ignore any changes inside the `workspace/` directory:
```
{
"version": "0.2.0",
"configurations": [
{
"name": "OpenHands CLI",
"type": "debugpy",
"request": "launch",
"module": "openhands.core.cli",
"justMyCode": false
},
{
"name": "OpenHands WebApp",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"openhands.server.listen:app",
"--reload",
"--reload-exclude",
"${workspaceFolder}/workspace",
"--port",
"3000"
],
"justMyCode": false
}
]
}
```
More specific debugging configurations which include more parameters may be specified:
```
...
{
"name": "Debug CodeAct",
"type": "debugpy",
"request": "launch",
"module": "openhands.core.main",
"args": [
"-t",
"Ask me what your task is.",
"-d",
"${workspaceFolder}/workspace",
"-c",
"CodeActAgent",
"-l",
"llm.o1",
"-n",
"prompts"
],
"justMyCode": false
}
...
```
Values in the snippet above can be updated such that:
* *t*: the task
* *d*: the openhands workspace directory
* *c*: the agent
* *l*: the LLM config (pre-defined in config.toml)
* *n*: session name (e.g. eventstream name)
@@ -177,6 +177,7 @@ spec:
claimName: docker-pvc
```
```bash
# create the pod
$ oc create -f pod.yaml
@@ -262,3 +263,167 @@ Events: <none>
6. Connect to OpenHands UI, configure the Agent, then test:
![image](https://github.com/user-attachments/assets/12f94804-a0c7-4744-b873-e003c9caf40e)
## GCP GKE Openhands deployment
**Warning**: this deployment grants the OpenHands application access to the Kubernetes docker socket, which creates security risk. Use at your own discretion.
1- Create policy for privillege access
2- Create gke credentials(optional)
3- Create openhands deployment
4- Verification and ui access commands
5- Tshoot pod to verify the internal container
1. create policy for privillege access
```bash
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: privileged-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: privileged-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: privileged-role
subjects:
- kind: ServiceAccount
name: default # Change to your service account name
namespace: default
```
2. create gke credentials(optional)
```bash
kubectl create secret generic google-cloud-key \
--from-file=key.json=/path/to/your/google-cloud-key.json
```
3. create openhands deployment
## as this is tested for the single worker node if you have multiple specify the flag for the single worker
```bash
kind: Deployment
metadata:
name: openhands-app-2024
labels:
app: openhands-app-2024
spec:
replicas: 1 # You can increase this number for multiple replicas
selector:
matchLabels:
app: openhands-app-2024
template:
metadata:
labels:
app: openhands-app-2024
spec:
containers:
- name: openhands-app-2024
image: ghcr.io/all-hands-ai/openhands:main
env:
- name: SANDBOX_USER_ID
value: "1000"
- name: SANDBOX_API_HOSTNAME
value: '10.164.0.4'
- name: WORKSPACE_MOUNT_PATH
value: "/tmp/workspace_base"
- name: GOOGLE_APPLICATION_CREDENTIALS
value: "/tmp/workspace_base/google-cloud-key.json"
volumeMounts:
- name: workspace-volume
mountPath: /tmp/workspace_base
- name: docker-sock
mountPath: /var/run/docker.sock
- name: google-credentials
mountPath: "/tmp/workspace_base/google-cloud-key.json"
securityContext:
privileged: true # Add this to allow privileged access
ports:
- containerPort: 3000
- name: openhands-sandbox-2024
image: ghcr.io/opendevin/sandbox:main
# securityContext:
# privileged: true # Add this to allow privileged access
ports:
- containerPort: 51963
command: ["/usr/sbin/sshd", "-D", "-p 51963", "-o", "PermitRootLogin=yes"]
volumes:
#- name: workspace-volume
# persistentVolumeClaim:
# claimName: workspace-pvc
- name: workspace-volume
emptyDir: {}
- name: docker-sock
hostPath:
path: /var/run/docker.sock # Use host's Docker socket
type: Socket
- name: google-credentials
secret:
secretName: google-cloud-key
---
apiVersion: v1
kind: Service
metadata:
name: openhands-app-2024-svc
spec:
selector:
app: openhands-app-2024
ports:
- name: http
protocol: TCP
port: 80
targetPort: 3000
- name: ssh
protocol: TCP
port: 51963
targetPort: 51963
type: LoadBalancer
```
5. Tshoot pod to verify the internal container
### if you want to know more regarding the internal container runtime use below mention pod deployment use kubectl exec -it to enter into container and you can check the contaienr run time using normal docker commands like "docker ps -a"
```bash
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-in-docker
spec:
replicas: 1
selector:
matchLabels:
app: docker-in-docker
template:
metadata:
labels:
app: docker-in-docker
spec:
containers:
- name: dind
image: docker:20.10-dind
securityContext:
privileged: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket
```
+14 -2
View File
@@ -59,9 +59,21 @@ We have a few guides for running OpenHands with specific model providers:
### API retries and rate limits
Some LLMs have rate limits and may require retries. OpenHands will automatically retry requests if it receives a 429 error or API connection error.
You can set the following environment variables to control the number of retries and the time between retries:
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically retry requests if it receives a Rate Limit Error (429 error code), API connection error, or other transient errors.
You can customize these options as you need for the provider you're using. Check their documentation, and set the following environment variables to control the number of retries and the time between retries:
* `LLM_NUM_RETRIES` (Default of 8)
* `LLM_RETRY_MIN_WAIT` (Default of 15 seconds)
* `LLM_RETRY_MAX_WAIT` (Default of 120 seconds)
* `LLM_RETRY_MULTIPLIER` (Default of 2)
If you running `openhands` in development mode, you can also set these options to the values you need in `config.toml` file:
```toml
[llm]
num_retries = 8
retry_min_wait = 15
retry_max_wait = 120
retry_multiplier = 2
```
+4
View File
@@ -83,6 +83,10 @@ const sidebars: SidebarsConfig = {
{
type: 'doc',
id: 'usage/how-to/openshift-example',
},
{
type: 'doc',
id: 'usage/how-to/debugging',
}
]
},
-1
View File
@@ -256,7 +256,6 @@ def process_instance(
if __name__ == '__main__':
args = parse_arguments()
dataset = load_dataset('RajMaheshwari/Exercism-Python')
dataset = dataset.shuffle(seed=42)
aider_bench_tests = dataset['train'].to_pandas()
llm_config = None
+1 -1
View File
@@ -106,7 +106,7 @@ if [ -z "$INSTANCE_ID" ]; then
rm -rf $RESULT_OUTPUT_DIR/eval_outputs
fi
mv run_instance_logs/$RUN_ID/$MODEL_NAME_OR_PATH $RESULT_OUTPUT_DIR
mv logs/run_evaluation/$RUN_ID/$MODEL_NAME_OR_PATH $RESULT_OUTPUT_DIR
mv $RESULT_OUTPUT_DIR/$MODEL_NAME_OR_PATH $RESULT_OUTPUT_DIR/eval_outputs
echo "RUN_ID: $RUN_ID" > $RESULT_OUTPUT_DIR/run_id.txt
+15 -6
View File
@@ -40,9 +40,9 @@ class EvalMetadata(BaseModel):
def model_dump_json(self, *args, **kwargs):
dumped = super().model_dump_json(*args, **kwargs)
dumped_dict = json.loads(dumped)
logger.debug(f'Dumped metadata: {dumped_dict}')
# avoid leaking sensitive information
dumped_dict['llm_config'] = self.llm_config.to_safe_dict()
logger.debug(f'Dumped metadata: {dumped_dict}')
return json.dumps(dumped_dict)
@@ -375,18 +375,27 @@ def reset_logger_for_multiprocessing(
# 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())
# add console handler to print ONE line
console_handler = get_console_handler(log_level=logging.INFO)
console_handler.setFormatter(
logging.Formatter(
f'Instance {instance_id} - ' + '%(asctime)s - %(levelname)s - %(message)s'
)
)
logger.addHandler(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)
# Only log WARNING or higher to console
console_handler.setLevel(logging.WARNING)
# Log INFO and above to file
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')
)
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
+51 -79
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.9.4",
"version": "0.9.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.9.4",
"version": "0.9.6",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8",
@@ -33,7 +33,7 @@
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.5.2",
"vite": "^5.4.7",
"vite": "^5.4.8",
"web-vitals": "^3.5.2"
},
"devDependencies": {
@@ -41,8 +41,8 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.6.1",
"@types/react": "^18.3.8",
"@types/node": "^22.7.3",
"@types/react": "^18.3.9",
"@types/react-dom": "^18.3.0",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -60,11 +60,11 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.12",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^1.6.0"
@@ -4860,9 +4860,9 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "22.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz",
"integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==",
"version": "22.7.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.3.tgz",
"integrity": "sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==",
"devOptional": true,
"dependencies": {
"undici-types": "~6.19.2"
@@ -4874,9 +4874,9 @@
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/react": {
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"version": "18.3.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz",
"integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6160,23 +6160,17 @@
}
},
"node_modules/cssstyle": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
"integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz",
"integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==",
"dev": true,
"dependencies": {
"rrweb-cssom": "^0.6.0"
"rrweb-cssom": "^0.7.1"
},
"engines": {
"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",
@@ -8872,12 +8866,12 @@
}
},
"node_modules/jsdom": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz",
"integrity": "sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==",
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
"dev": true,
"dependencies": {
"cssstyle": "^4.0.1",
"cssstyle": "^4.1.0",
"data-urls": "^5.0.0",
"decimal.js": "^10.4.3",
"form-data": "^4.0.0",
@@ -8890,7 +8884,7 @@
"rrweb-cssom": "^0.7.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.4",
"tough-cookie": "^5.0.0",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
@@ -11061,12 +11055,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"dev": true
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -11076,12 +11064,6 @@
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11593,12 +11575,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -12426,9 +12402,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -12566,6 +12542,24 @@
"node": ">=14.0.0"
}
},
"node_modules/tldts": {
"version": "6.1.47",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz",
"integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==",
"dev": true,
"dependencies": {
"tldts-core": "^6.1.47"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.47",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz",
"integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -12586,18 +12580,15 @@
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
"integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==",
"dev": true,
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
"tldts": "^6.1.32"
},
"engines": {
"node": ">=6"
"node": ">=16"
}
},
"node_modules/tr46": {
@@ -12936,15 +12927,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true,
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -12983,16 +12965,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dev": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/use-callback-ref": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
@@ -13112,9 +13084,9 @@
}
},
"node_modules/vite": {
"version": "5.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.9.4",
"version": "0.9.6",
"private": true,
"type": "module",
"engines": {
@@ -32,7 +32,7 @@
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.5.2",
"vite": "^5.4.7",
"vite": "^5.4.8",
"web-vitals": "^3.5.2"
},
"scripts": {
@@ -64,8 +64,8 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.6.1",
"@types/react": "^18.3.8",
"@types/node": "^22.7.3",
"@types/react": "^18.3.9",
"@types/react-dom": "^18.3.0",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -83,11 +83,11 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.12",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^1.6.0"
+2 -2
View File
@@ -94,13 +94,13 @@ function AgentStatusBar() {
const [statusMessage, setStatusMessage] = React.useState<string>("");
React.useEffect(() => {
const trimmedCustomMessage = curStatusMessage.message.trim();
const trimmedCustomMessage = curStatusMessage.status.trim();
if (trimmedCustomMessage) {
setStatusMessage(t(trimmedCustomMessage));
} else {
setStatusMessage(AgentStatusMap[curAgentState].message);
}
}, [curAgentState, curStatusMessage.message]);
}, [curAgentState, curStatusMessage.status]);
return (
<div className="flex flex-col items-center">
@@ -7,6 +7,7 @@ import { I18nKey } from "../../../i18n/declaration";
import { AutocompleteCombobox } from "./AutocompleteCombobox";
import { Settings } from "#/services/settings";
import { organizeModelsAndProviders } from "#/utils/organizeModelsAndProviders";
import { extractModelAndProvider } from "#/utils/extractModelAndProvider";
import { ModelSelector } from "./ModelSelector";
interface SettingsFormProps {
@@ -41,17 +42,29 @@ function SettingsForm({
}: SettingsFormProps) {
const { t } = useTranslation();
const { isOpen: isVisible, onOpenChange: onVisibleChange } = useDisclosure();
const advancedAlreadyInUse = React.useMemo(
() =>
const advancedAlreadyInUse = React.useMemo(() => {
const organizedModels = organizeModelsAndProviders(models);
const { provider, model } = extractModelAndProvider(
settings.LLM_MODEL || "",
);
const isKnownModel =
provider in organizedModels &&
organizedModels[provider].models.includes(model);
return (
!!settings.SECURITY_ANALYZER ||
!!settings.CONFIRMATION_MODE ||
!!settings.LLM_BASE_URL ||
(!!settings.LLM_MODEL && !models.includes(settings.LLM_MODEL)),
[],
);
(!!settings.LLM_MODEL && !isKnownModel)
);
}, [settings, models]);
const [enableAdvanced, setEnableAdvanced] =
React.useState(advancedAlreadyInUse);
React.useEffect(() => {
setEnableAdvanced(advancedAlreadyInUse);
}, [advancedAlreadyInUse]);
const handleAdvancedChange = (value: boolean) => {
setEnableAdvanced(value);
};
+5 -5
View File
@@ -140,11 +140,11 @@ export function handleActionMessage(message: ActionMessage) {
}
export function handleStatusMessage(message: StatusMessage) {
const msg = message.message == null ? "" : message.message.trim();
const msg = message.status == null ? "" : message.status.trim();
store.dispatch(
setCurStatusMessage({
...message,
message: msg,
status: msg,
}),
);
}
@@ -160,9 +160,9 @@ export function handleAssistantMessage(data: string | SocketMessage) {
if ("action" in socketMessage) {
handleActionMessage(socketMessage);
} else if ("observation" in socketMessage) {
handleObservationMessage(socketMessage);
} else if ("message" in socketMessage) {
} else if ("status" in socketMessage) {
handleStatusMessage(socketMessage);
} else {
handleObservationMessage(socketMessage);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { StatusMessage } from "#/types/Message";
const initialStatusMessage: StatusMessage = {
message: "",
status: "",
is_error: false,
};
+1 -1
View File
@@ -38,5 +38,5 @@ export interface StatusMessage {
is_error: boolean;
// A status message to display to the user
message: string;
status: string;
}
+13
View File
@@ -1,3 +1,6 @@
import os
def get_version():
try:
from importlib.metadata import PackageNotFoundError, version
@@ -19,6 +22,16 @@ def get_version():
except ImportError:
pass
# Try getting the version from pyproject.toml
try:
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
with open(os.path.join(root_dir, 'pyproject.toml'), 'r') as f:
for line in f:
if line.startswith('version ='):
return line.split('=')[1].strip().strip('"')
except FileNotFoundError:
pass
return 'unknown'
+9 -3
View File
@@ -1,4 +1,5 @@
import asyncio
import copy
import traceback
from typing import Type
@@ -36,6 +37,7 @@ from openhands.events.observation import (
ErrorObservation,
Observation,
)
from openhands.events.serialization.event import truncate_content
from openhands.llm.llm import LLM
from openhands.runtime.utils.shutdown_listener import should_continue
@@ -117,8 +119,6 @@ class AgentController:
async def close(self):
"""Closes the agent controller, canceling any ongoing tasks and unsubscribing from the event stream."""
if self.agent_task is not None:
self.agent_task.cancel()
await self.set_agent_state_to(AgentState.STOPPED)
self.event_stream.unsubscribe(EventStreamSubscriber.AGENT_CONTROLLER)
@@ -220,7 +220,13 @@ class AgentController:
):
return
logger.info(observation, extra={'msg_type': 'OBSERVATION'})
# Make sure we print the observation in the same way as the LLM sees it
observation_to_print = copy.deepcopy(observation)
if len(observation_to_print.content) > self.agent.llm.config.max_message_chars:
observation_to_print.content = truncate_content(
observation_to_print.content, self.agent.llm.config.max_message_chars
)
logger.info(observation_to_print, extra={'msg_type': 'OBSERVATION'})
if self._pending_action and self._pending_action.id == observation.cause:
self._pending_action = None
if self.state.agent_state == AgentState.USER_CONFIRMED:
+15 -8
View File
@@ -1,3 +1,4 @@
import os
from dataclasses import dataclass, fields
from openhands.core.config.config_utils import get_field_info
@@ -36,7 +37,7 @@ class LLMConfig:
ollama_base_url: The base URL for the OLLAMA API.
drop_params: Drop any unmapped (unsupported) params without causing an exception.
disable_vision: If model is vision capable, this option allows to disable image processing (useful for cost reduction).
caching_prompt: Using the prompt caching feature provided by the LLM.
caching_prompt: Use the prompt caching feature if provided by the LLM and supported by the provider.
log_completions: Whether to log LLM completions to the state.
"""
@@ -68,7 +69,7 @@ class LLMConfig:
ollama_base_url: str | None = None
drop_params: bool = True
disable_vision: bool | None = None
caching_prompt: bool = False
caching_prompt: bool = True
log_completions: bool = False
def defaults_to_dict(self) -> dict:
@@ -78,6 +79,18 @@ class LLMConfig:
result[f.name] = get_field_info(f)
return result
def __post_init__(self):
"""
Post-initialization hook to assign OpenRouter-related variables to environment variables.
This ensures that these values are accessible to litellm at runtime.
"""
# Assign OpenRouter-specific variables to environment variables
if self.openrouter_site_url:
os.environ['OR_SITE_URL'] = self.openrouter_site_url
if self.openrouter_app_name:
os.environ['OR_APP_NAME'] = self.openrouter_app_name
def __str__(self):
attr_str = []
for f in fields(self):
@@ -101,9 +114,3 @@ class LLMConfig:
if k in LLM_SENSITIVE_FIELDS:
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)
+14 -13
View File
@@ -50,6 +50,8 @@ class ImageContent(Content):
class Message(BaseModel):
role: Literal['user', 'system', 'assistant']
content: list[TextContent | ImageContent] = Field(default=list)
cache_enabled: bool = False
vision_enabled: bool = False
@property
def contains_image(self) -> bool:
@@ -58,23 +60,22 @@ class Message(BaseModel):
@model_serializer
def serialize_model(self) -> dict:
content: list[dict] | str
if self.role == 'system':
# For system role, concatenate all text content into a single string
content = '\n'.join(
item.text for item in self.content if isinstance(item, TextContent)
)
elif self.role == 'assistant' and not self.contains_image:
# For assistant role without vision, concatenate all text content into a single string
content = '\n'.join(
item.text for item in self.content if isinstance(item, TextContent)
)
else:
# For user role or assistant role with vision enabled, serialize each content item
# two kinds of serializer:
# 1. vision serializer: when prompt caching or vision is enabled
# 2. single text serializer: for other cases
# remove this when liteLLM or providers support this format translation
if self.cache_enabled or self.vision_enabled:
# when prompt caching or vision is enabled, use vision serializer
content = []
for item in self.content:
if isinstance(item, TextContent):
content.append(item.model_dump())
elif isinstance(item, ImageContent):
content.extend(item.model_dump())
else:
# for other cases, concatenate all text content
# into a single string per message
content = '\n'.join(
item.text for item in self.content if isinstance(item, TextContent)
)
return {'content': content, 'role': self.role}
-4
View File
@@ -24,10 +24,6 @@ class ActionTypeSchema(BaseModel):
"""Writes the content to a file.
"""
EDIT: str = Field(default='edit')
"""Edits the content of a file.
"""
RUN: str = Field(default='run')
"""Runs a command.
"""
-4
View File
@@ -10,10 +10,6 @@ class ObservationTypeSchema(BaseModel):
WRITE: str = Field(default='write')
EDIT: str = Field(default='edit')
"""The edited file
"""
BROWSE: str = Field(default='browse')
"""The HTML content of a URL
"""
+1 -6
View File
@@ -9,11 +9,7 @@ from openhands.events.action.agent import (
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
from openhands.events.action.commands import CmdRunAction, IPythonRunCellAction
from openhands.events.action.empty import NullAction
from openhands.events.action.files import (
FileEditAction,
FileReadAction,
FileWriteAction,
)
from openhands.events.action.files import FileReadAction, FileWriteAction
from openhands.events.action.message import MessageAction
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
@@ -25,7 +21,6 @@ __all__ = [
'BrowseInteractiveAction',
'FileReadAction',
'FileWriteAction',
'FileEditAction',
'AgentFinishAction',
'AgentRejectAction',
'AgentDelegateAction',
-20
View File
@@ -39,23 +39,3 @@ class FileWriteAction(Action):
@property
def message(self) -> str:
return f'Writing file: {self.path}'
@dataclass
class FileEditAction(Action):
diff_block: str
thought: str = ''
action: str = ActionType.EDIT
runnable: ClassVar[bool] = True
security_risk: ActionSecurityRisk | None = None
def __str__(self) -> str:
ret = '**EditFileAction**\n'
if self.thought:
ret += f'THOUGHT: {self.thought}\n'
ret += f'DIFF BLOCK:\n{self.diff_block}\n'
return ret
@property
def message(self) -> str:
return f'Edit Diff block: {self.diff_block}'
+1 -6
View File
@@ -7,11 +7,7 @@ from openhands.events.observation.commands import (
from openhands.events.observation.delegate import AgentDelegateObservation
from openhands.events.observation.empty import NullObservation
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.files import (
FileEditObservation,
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.files import FileReadObservation, FileWriteObservation
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
@@ -24,7 +20,6 @@ __all__ = [
'BrowserOutputObservation',
'FileReadObservation',
'FileWriteObservation',
'FileEditObservation',
'ErrorObservation',
'AgentStateChangedObservation',
'AgentDelegateObservation',
-27
View File
@@ -26,30 +26,3 @@ class FileWriteObservation(Observation):
@property
def message(self) -> str:
return f'I wrote to the file {self.path}.'
@dataclass
class FileEditObservation(Observation):
"""This data class represents a file edit operation"""
path: str
search_block: str
replace_block: str
observation: str = ObservationType.EDIT
@property
def message(self) -> str:
if self.search_block:
return (
f'I updated the file {self.path} by \n'
f'replacing:\n {self.search_block}\n'
f'with:\n {self.replace_block}\n'
)
else:
return (
f'I updated the file {self.path} by \n'
f'appending:\n {self.replace_block}\n'
)
def __str__(self) -> str:
return f'**FileEditObservation**\n' f'DIFF BLOCK: {self.content}\n'
+1 -6
View File
@@ -12,11 +12,7 @@ from openhands.events.action.commands import (
IPythonRunCellAction,
)
from openhands.events.action.empty import NullAction
from openhands.events.action.files import (
FileEditAction,
FileReadAction,
FileWriteAction,
)
from openhands.events.action.files import FileReadAction, FileWriteAction
from openhands.events.action.message import MessageAction
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
@@ -28,7 +24,6 @@ actions = (
BrowseInteractiveAction,
FileReadAction,
FileWriteAction,
FileEditAction,
AgentFinishAction,
AgentRejectAction,
AgentDelegateAction,
@@ -7,11 +7,7 @@ from openhands.events.observation.commands import (
from openhands.events.observation.delegate import AgentDelegateObservation
from openhands.events.observation.empty import NullObservation
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.files import (
FileEditObservation,
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.files import FileReadObservation, FileWriteObservation
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
@@ -23,7 +19,6 @@ observations = (
BrowserOutputObservation,
FileReadObservation,
FileWriteObservation,
FileEditObservation,
AgentDelegateObservation,
SuccessObservation,
ErrorObservation,
+5
View File
@@ -0,0 +1,5 @@
from openhands.llm.async_llm import AsyncLLM
from openhands.llm.llm import LLM
from openhands.llm.streaming_llm import StreamingLLM
__all__ = ['LLM', 'AsyncLLM', 'StreamingLLM']
+117
View File
@@ -0,0 +1,117 @@
import asyncio
from functools import partial
from typing import Any
from litellm import completion as litellm_acompletion
from openhands.core.exceptions import UserCancelledError
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM, LLM_RETRY_EXCEPTIONS
from openhands.runtime.utils.shutdown_listener import should_continue
class AsyncLLM(LLM):
"""Asynchronous LLM class."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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=self.config.drop_params,
)
async_completion_unwrapped = self._async_completion
@self.retry_decorator(
num_retries=self.config.num_retries,
retry_exceptions=LLM_RETRY_EXCEPTIONS,
retry_min_wait=self.config.retry_min_wait,
retry_max_wait=self.config.retry_max_wait,
retry_multiplier=self.config.retry_multiplier,
)
async def async_completion_wrapper(*args, **kwargs):
"""Wrapper for the litellm acompletion function."""
messages: list[dict[str, Any]] | dict[str, Any] = []
# some callers might send the model and messages directly
# litellm allows positional args, like completion(model, messages, **kwargs)
# see llm.py for more details
if len(args) > 1:
messages = args[1] if len(args) > 1 else args[0]
kwargs['messages'] = messages
# remove the first args, they're sent in kwargs
args = args[2:]
elif 'messages' in kwargs:
messages = kwargs['messages']
# ensure we work with a list of messages
messages = messages if isinstance(messages, list) else [messages]
# if we have no messages, something went very wrong
if not messages:
raise ValueError(
'The messages list is empty. At least one message is required.'
)
self.log_prompt(messages)
async def check_stopped():
while should_continue():
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)
message_back = resp['choices'][0]['message']['content']
self.log_response(message_back)
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 Exception 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
self._async_completion = async_completion_wrapper # type: ignore
async def _call_acompletion(self, *args, **kwargs):
"""Wrapper for the litellm acompletion function."""
# Used in testing?
return await litellm_acompletion(*args, **kwargs)
@property
def async_completion(self):
"""Decorator for the async litellm acompletion function."""
return self._async_completion
+51
View File
@@ -0,0 +1,51 @@
from typing import Any
from openhands.core.logger import llm_prompt_logger, llm_response_logger
from openhands.core.logger import openhands_logger as logger
MESSAGE_SEPARATOR = '\n\n----------\n\n'
class DebugMixin:
def log_prompt(self, messages: list[dict[str, Any]] | dict[str, Any]):
if not messages:
logger.debug('No completion messages!')
return
messages = messages if isinstance(messages, list) else [messages]
debug_message = MESSAGE_SEPARATOR.join(
self._format_message_content(msg) for msg in messages if msg['content']
)
if debug_message:
llm_prompt_logger.debug(debug_message)
else:
logger.debug('No completion messages!')
def log_response(self, message_back: str):
if message_back:
llm_response_logger.debug(message_back)
def _format_message_content(self, message: dict[str, Any]):
content = message['content']
if isinstance(content, list):
return '\n'.join(
self._format_content_element(element) for element in content
)
return str(content)
def _format_content_element(self, element: dict[str, Any]):
if isinstance(element, dict):
if 'text' in element:
return element['text']
if (
self.vision_is_active()
and 'image_url' in element
and 'url' in element['image_url']
):
return element['image_url']['url']
return str(element)
# This method should be implemented in the class that uses DebugMixin
def vision_is_active(self):
raise NotImplementedError
+114 -401
View File
@@ -1,59 +1,54 @@
import asyncio
import copy
import os
import time
import warnings
from functools import partial
from typing import Any
from openhands.core.config import LLMConfig
from openhands.runtime.utils.shutdown_listener import should_continue
with warnings.catch_warnings():
warnings.simplefilter('ignore')
import litellm
from litellm import ModelInfo
from litellm import completion as litellm_completion
from litellm import completion_cost as litellm_completion_cost
from litellm.exceptions import (
APIConnectionError,
ContentPolicyViolationError,
InternalServerError,
NotFoundError,
OpenAIError,
RateLimitError,
ServiceUnavailableError,
)
from litellm.types.utils import CostPerToken
from tenacity import (
retry,
retry_if_exception_type,
retry_if_not_exception_type,
stop_after_attempt,
wait_exponential,
)
from litellm.types.utils import CostPerToken, ModelResponse, Usage
from openhands.core.exceptions import (
LLMResponseError,
OperationCancelled,
UserCancelledError,
)
from openhands.core.logger import llm_prompt_logger, llm_response_logger
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import Message
from openhands.core.metrics import Metrics
from openhands.runtime.utils.shutdown_listener import should_exit
from openhands.llm.debug_mixin import DebugMixin
from openhands.llm.retry_mixin import RetryMixin
__all__ = ['LLM']
message_separator = '\n\n----------\n\n'
# tuple of exceptions to retry on
LLM_RETRY_EXCEPTIONS: tuple[type[Exception], ...] = (
APIConnectionError,
InternalServerError,
RateLimitError,
ServiceUnavailableError,
)
cache_prompting_supported_models = [
# cache prompt supporting models
# remove this when we gemini and deepseek are supported
CACHE_PROMPT_SUPPORTED_MODELS = [
'claude-3-5-sonnet-20240620',
'claude-3-haiku-20240307',
'claude-3-opus-20240229',
'anthropic/claude-3-opus-20240229',
'anthropic/claude-3-haiku-20240307',
'anthropic/claude-3-5-sonnet-20240620',
]
class LLM:
class LLM(RetryMixin, DebugMixin):
"""The LLM class represents a Language Model instance.
Attributes:
@@ -70,25 +65,20 @@ class LLM:
Passing simple parameters always overrides config.
Args:
config: The LLM configuration
config: The LLM configuration.
metrics: The metrics to use.
"""
self.metrics = metrics if metrics is not None else Metrics()
self.cost_metric_supported = True
self.config = copy.deepcopy(config)
os.environ['OR_SITE_URL'] = self.config.openrouter_site_url
os.environ['OR_APP_NAME'] = self.config.openrouter_app_name
self.metrics: Metrics = metrics if metrics is not None else Metrics()
self.cost_metric_supported: bool = True
self.config: LLMConfig = copy.deepcopy(config)
# list of LLM completions (for logging purposes). Each completion is a dict with the following keys:
# - 'messages': list of messages
# - 'response': response from the LLM
self.llm_completions: list[dict[str, Any]] = []
# 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
self.model_info: ModelInfo | None = None
try:
if self.config.model.startswith('openrouter'):
self.model_info = litellm.get_model_info(self.config.model)
@@ -100,15 +90,6 @@ class LLM:
except Exception as e:
logger.warning(f'Could not get model info for {config.model}:\n{e}')
# Tuple of exceptions to retry on
self.retry_exceptions = (
APIConnectionError,
ContentPolicyViolationError,
InternalServerError,
OpenAIError,
RateLimitError,
)
# Set the max tokens in an LM-specific way if not set
if self.config.max_input_tokens is None:
if (
@@ -136,30 +117,6 @@ class LLM:
):
self.config.max_output_tokens = self.model_info['max_tokens']
# This only seems to work with Google as the provider, not with OpenRouter!
gemini_safety_settings = (
[
{
'category': 'HARM_CATEGORY_HARASSMENT',
'threshold': 'BLOCK_NONE',
},
{
'category': 'HARM_CATEGORY_HATE_SPEECH',
'threshold': 'BLOCK_NONE',
},
{
'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
'threshold': 'BLOCK_NONE',
},
{
'category': 'HARM_CATEGORY_DANGEROUS_CONTENT',
'threshold': 'BLOCK_NONE',
},
]
if self.config.model.lower().startswith('gemini')
else None
)
self._completion = partial(
litellm_completion,
model=self.config.model,
@@ -172,84 +129,52 @@ class LLM:
temperature=self.config.temperature,
top_p=self.config.top_p,
drop_params=self.config.drop_params,
**(
{'safety_settings': gemini_safety_settings}
if gemini_safety_settings is not None
else {}
),
)
if self.vision_is_active():
logger.debug('LLM: model has vision enabled')
if self.is_caching_prompt_active():
logger.debug('LLM: caching prompt enabled')
completion_unwrapped = self._completion
def log_retry_attempt(retry_state):
"""With before_sleep, this is called before `custom_completion_wait` and
ONLY if the retry is triggered by an exception."""
if should_exit():
raise OperationCancelled(
'Operation cancelled.'
) # exits the @retry loop
exception = retry_state.outcome.exception()
logger.error(
f'{exception}. Attempt #{retry_state.attempt_number} | You can customize retry values in the configuration.',
exc_info=False,
)
def custom_completion_wait(retry_state):
"""Custom wait function for litellm completion."""
if not retry_state:
return 0
exception = retry_state.outcome.exception() if retry_state.outcome else None
if exception is None:
return 0
min_wait_time = self.config.retry_min_wait
max_wait_time = self.config.retry_max_wait
# for rate limit errors, wait 1 minute by default, max 4 minutes between retries
exception_type = type(exception).__name__
logger.error(f'\nexception_type: {exception_type}\n')
if exception_type == 'RateLimitError':
min_wait_time = 60
max_wait_time = 240
elif exception_type == 'BadRequestError' and exception.response:
# this should give us the burried, actual error message from
# the LLM model.
logger.error(f'\n\nBadRequestError: {exception.response}\n\n')
# Return the wait time using exponential backoff
exponential_wait = wait_exponential(
multiplier=self.config.retry_multiplier,
min=min_wait_time,
max=max_wait_time,
)
# Call the exponential wait function with retry_state to get the actual wait time
return exponential_wait(retry_state)
@retry(
before_sleep=log_retry_attempt,
stop=stop_after_attempt(self.config.num_retries),
reraise=True,
retry=(
retry_if_exception_type(self.retry_exceptions)
& retry_if_not_exception_type(OperationCancelled)
),
wait=custom_completion_wait,
@self.retry_decorator(
num_retries=self.config.num_retries,
retry_exceptions=LLM_RETRY_EXCEPTIONS,
retry_min_wait=self.config.retry_min_wait,
retry_max_wait=self.config.retry_max_wait,
retry_multiplier=self.config.retry_multiplier,
)
def wrapper(*args, **kwargs):
"""Wrapper for the litellm completion function. Logs the input and output of the completion function."""
# some callers might just send the messages directly
if 'messages' in kwargs:
messages = kwargs['messages']
else:
messages = args[1] if len(args) > 1 else []
messages: list[dict[str, Any]] | dict[str, Any] = []
# this serves to prevent empty messages and logging the messages
debug_message = self._get_debug_message(messages)
# some callers might send the model and messages directly
# litellm allows positional args, like completion(model, messages, **kwargs)
if len(args) > 1:
# ignore the first argument if it's provided (it would be the model)
# design wise: we don't allow overriding the configured values
# implementation wise: the partial function set the model as a kwarg already
# as well as other kwargs
messages = args[1] if len(args) > 1 else args[0]
kwargs['messages'] = messages
# remove the first args, they're sent in kwargs
args = args[2:]
elif 'messages' in kwargs:
messages = kwargs['messages']
# ensure we work with a list of messages
messages = messages if isinstance(messages, list) else [messages]
# if we have no messages, something went very wrong
if not messages:
raise ValueError(
'The messages list is empty. At least one message is required.'
)
# log the entire LLM prompt
self.log_prompt(messages)
if self.is_caching_prompt_active():
# Anthropic-specific prompt caching
@@ -258,239 +183,31 @@ class LLM:
'anthropic-beta': 'prompt-caching-2024-07-31',
}
# skip if messages is empty (thus debug_message is empty)
if debug_message:
llm_prompt_logger.debug(debug_message)
resp = completion_unwrapped(*args, **kwargs)
else:
logger.debug('No completion messages!')
resp = {'choices': [{'message': {'content': ''}}]}
# we don't support streaming here, thus we get a ModelResponse
resp: ModelResponse = completion_unwrapped(*args, **kwargs)
# log for evals or other scripts that need the raw completion
if self.config.log_completions:
self.llm_completions.append(
{
'messages': messages,
'response': resp,
'timestamp': time.time(),
'cost': self.completion_cost(resp),
'cost': self._completion_cost(resp),
}
)
# log the response
message_back = resp['choices'][0]['message']['content']
if message_back:
llm_response_logger.debug(message_back)
message_back: str = resp['choices'][0]['message']['content']
# post-process to log costs
self._post_completion(resp)
# log the LLM response
self.log_response(message_back)
# post-process the response
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=self.config.drop_params,
**(
{'safety_settings': gemini_safety_settings}
if gemini_safety_settings is not None
else {}
),
)
async_completion_unwrapped = self._async_completion
@retry(
before_sleep=log_retry_attempt,
stop=stop_after_attempt(self.config.num_retries),
reraise=True,
retry=(
retry_if_exception_type(self.retry_exceptions)
& retry_if_not_exception_type(OperationCancelled)
),
wait=custom_completion_wait,
)
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] if len(args) > 1 else []
# this serves to prevent empty messages and logging the messages
debug_message = self._get_debug_message(messages)
async def check_stopped():
while should_continue():
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
if debug_message:
llm_prompt_logger.debug(debug_message)
resp = await async_completion_unwrapped(*args, **kwargs)
else:
logger.debug('No completion messages!')
resp = {'choices': [{'message': {'content': ''}}]}
# 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 (
APIConnectionError,
ContentPolicyViolationError,
InternalServerError,
NotFoundError,
OpenAIError,
RateLimitError,
ServiceUnavailableError,
) 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(
before_sleep=log_retry_attempt,
stop=stop_after_attempt(self.config.num_retries),
reraise=True,
retry=(
retry_if_exception_type(self.retry_exceptions)
& retry_if_not_exception_type(OperationCancelled)
),
wait=custom_completion_wait,
)
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] if len(args) > 1 else []
# 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 (
APIConnectionError,
ContentPolicyViolationError,
InternalServerError,
NotFoundError,
OpenAIError,
RateLimitError,
ServiceUnavailableError,
) 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
def _get_debug_message(self, messages):
if not messages:
return ''
messages = messages if isinstance(messages, list) else [messages]
return message_separator.join(
self._format_message_content(msg) for msg in messages if msg['content']
)
def _format_message_content(self, message):
content = message['content']
if isinstance(content, list):
return self._format_list_content(content)
return str(content)
def _format_list_content(self, content_list):
return '\n'.join(
self._format_content_element(element) for element in content_list
)
def _format_content_element(self, element):
if isinstance(element, dict):
if 'text' in element:
return element['text']
if (
self.vision_is_active()
and 'image_url' in element
and 'url' in element['image_url']
):
return element['image_url']['url']
return str(element)
async def _call_acompletion(self, *args, **kwargs):
"""This is a wrapper for the litellm acompletion function which
makes it mockable for testing.
"""
return await litellm.acompletion(*args, **kwargs)
self._completion = wrapper
@property
def completion(self):
@@ -498,32 +215,7 @@ class LLM:
Check the complete documentation at https://litellm.vercel.app/docs/completion
"""
try:
return self._completion
except Exception as e:
raise LLMResponseError(e)
@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
"""
try:
return self._async_completion
except Exception as e:
raise LLMResponseError(e)
@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
"""
try:
return self._async_streaming_completion
except Exception as e:
raise LLMResponseError(e)
return self._completion
def vision_is_active(self):
return not self.config.disable_vision and self._supports_vision()
@@ -534,38 +226,50 @@ class LLM:
Returns:
bool: True if model is vision capable. If model is not supported by litellm, it will return False.
"""
try:
return litellm.supports_vision(self.config.model)
except Exception:
return False
def is_caching_prompt_active(self) -> bool:
"""Check if prompt caching is enabled and supported for current model.
Returns:
boolean: True if prompt caching is active for the given model.
"""
return self.config.caching_prompt is True and any(
model in self.config.model for model in cache_prompting_supported_models
# litellm.supports_vision currently returns False for 'openai/gpt-...' or 'anthropic/claude-...' (with prefixes)
# but model_info will have the correct value for some reason.
# we can go with it, but we will need to keep an eye if model_info is correct for Vertex or other providers
# remove when litellm is updated to fix https://github.com/BerriAI/litellm/issues/5608
return litellm.supports_vision(self.config.model) or (
self.model_info is not None
and self.model_info.get('supports_vision', False)
)
def _post_completion(self, response) -> None:
"""Post-process the completion response."""
def is_caching_prompt_active(self) -> bool:
"""Check if prompt caching is supported and enabled for current model.
Returns:
boolean: True if prompt caching is supported and enabled for the given model.
"""
return (
self.config.caching_prompt is True
and self.model_info is not None
and self.model_info.get('supports_prompt_caching', False)
and self.config.model in CACHE_PROMPT_SUPPORTED_MODELS
)
def _post_completion(self, response: ModelResponse) -> None:
"""Post-process the completion response.
Logs the cost and usage stats of the completion call.
"""
try:
cur_cost = self.completion_cost(response)
cur_cost = self._completion_cost(response)
except Exception:
cur_cost = 0
stats = ''
if self.cost_metric_supported:
# keep track of the cost
stats = 'Cost: %.2f USD | Accumulated Cost: %.2f USD\n' % (
cur_cost,
self.metrics.accumulated_cost,
)
usage = response.get('usage')
usage: Usage | None = response.get('usage')
if usage:
# keep track of the input and output tokens
input_tokens = usage.get('prompt_tokens')
output_tokens = usage.get('completion_tokens')
@@ -580,6 +284,7 @@ class LLM:
+ '\n'
)
# read the prompt caching status as received from the provider
model_extra = usage.get('model_extra', {})
cache_creation_input_tokens = model_extra.get('cache_creation_input_tokens')
@@ -596,6 +301,7 @@ class LLM:
'Input tokens (cache read): ' + str(cache_read_input_tokens) + '\n'
)
# log the stats
if stats:
logger.info(stats)
@@ -614,7 +320,7 @@ class LLM:
# TODO: this is to limit logspam in case token count is not supported
return 0
def is_local(self):
def _is_local(self):
"""Determines if the system is using a locally running LLM.
Returns:
@@ -629,7 +335,7 @@ class LLM:
return True
return False
def completion_cost(self, response):
def _completion_cost(self, response):
"""Calculate the cost of a completion response based on the model. Local models are treated as free.
Add the current cost into total cost in metrics.
@@ -654,7 +360,7 @@ class LLM:
logger.info(f'Using custom cost per token: {cost_per_token}')
extra_kwargs['custom_cost_per_token'] = cost_per_token
if not self.is_local():
if not self._is_local():
try:
cost = litellm_completion_cost(
completion_response=response, **extra_kwargs
@@ -682,5 +388,12 @@ class LLM:
def format_messages_for_llm(self, messages: Message | list[Message]) -> list[dict]:
if isinstance(messages, Message):
return [messages.model_dump()]
messages = [messages]
# set flags to know how to serialize the messages
for message in messages:
message.cache_enabled = self.is_caching_prompt_active()
message.vision_enabled = self.vision_is_active()
# let pydantic handle the serialization
return [message.model_dump() for message in messages]
+53
View File
@@ -0,0 +1,53 @@
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
from openhands.core.exceptions import OperationCancelled
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.utils.shutdown_listener import should_exit
class RetryMixin:
"""Mixin class for retry logic."""
def retry_decorator(self, **kwargs):
"""
Create a LLM retry decorator with customizable parameters. This is used for 429 errors, and a few other exceptions in LLM classes.
Args:
**kwargs: Keyword arguments to override default retry behavior.
Keys: num_retries, retry_exceptions, retry_min_wait, retry_max_wait, retry_multiplier
Returns:
A retry decorator with the parameters customizable in configuration.
"""
num_retries = kwargs.get('num_retries')
retry_exceptions = kwargs.get('retry_exceptions')
retry_min_wait = kwargs.get('retry_min_wait')
retry_max_wait = kwargs.get('retry_max_wait')
retry_multiplier = kwargs.get('retry_multiplier')
return retry(
before_sleep=self.log_retry_attempt,
stop=stop_after_attempt(num_retries),
reraise=True,
retry=(retry_if_exception_type(retry_exceptions)),
wait=wait_exponential(
multiplier=retry_multiplier,
min=retry_min_wait,
max=retry_max_wait,
),
)
def log_retry_attempt(self, retry_state):
"""Log retry attempts."""
if should_exit():
raise OperationCancelled('Operation cancelled.') # exits the @retry loop
exception = retry_state.outcome.exception()
logger.error(
f'{exception}. Attempt #{retry_state.attempt_number} | You can customize retry values in the configuration.',
exc_info=False,
)
+106
View File
@@ -0,0 +1,106 @@
import asyncio
from functools import partial
from typing import Any
from openhands.core.exceptions import UserCancelledError
from openhands.core.logger import openhands_logger as logger
from openhands.llm.async_llm import LLM_RETRY_EXCEPTIONS, AsyncLLM
class StreamingLLM(AsyncLLM):
"""Streaming LLM class."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._async_streaming_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=self.config.drop_params,
stream=True, # Ensure streaming is enabled
)
async_streaming_completion_unwrapped = self._async_streaming_completion
@self.retry_decorator(
num_retries=self.config.num_retries,
retry_exceptions=LLM_RETRY_EXCEPTIONS,
retry_min_wait=self.config.retry_min_wait,
retry_max_wait=self.config.retry_max_wait,
retry_multiplier=self.config.retry_multiplier,
)
async def async_streaming_completion_wrapper(*args, **kwargs):
messages: list[dict[str, Any]] | dict[str, Any] = []
# some callers might send the model and messages directly
# litellm allows positional args, like completion(model, messages, **kwargs)
# see llm.py for more details
if len(args) > 1:
messages = args[1] if len(args) > 1 else args[0]
kwargs['messages'] = messages
# remove the first args, they're sent in kwargs
args = args[2:]
elif 'messages' in kwargs:
messages = kwargs['messages']
# ensure we work with a list of messages
messages = messages if isinstance(messages, list) else [messages]
# if we have no messages, something went very wrong
if not messages:
raise ValueError(
'The messages list is empty. At least one message is required.'
)
self.log_prompt(messages)
try:
# Directly call and await litellm_acompletion
resp = await async_streaming_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'].get('content', '')
if message_back:
self.log_response(message_back)
self._post_completion(chunk)
yield chunk
except UserCancelledError:
logger.info('LLM request cancelled by user.')
raise
except Exception as e:
logger.error(f'Completion Error occurred:\n{e}')
raise
finally:
# sleep for 0.1 seconds to allow the stream to be flushed
if kwargs.get('stream', False):
await asyncio.sleep(0.1)
self._async_streaming_completion = async_streaming_completion_wrapper
@property
def async_streaming_completion(self):
"""Decorator for the async litellm acompletion function with streaming."""
return self._async_streaming_completion
+2 -1
View File
@@ -26,12 +26,13 @@ class RuntimeBuilder(abc.ABC):
pass
@abc.abstractmethod
def image_exists(self, image_name: str) -> bool:
def image_exists(self, image_name: str, pull_from_repo: bool = True) -> bool:
"""
Check if the runtime image exists.
Args:
image_name (str): The name of the runtime image (e.g., "repo:sha").
pull_from_repo (bool): Whether to pull from the remote repo if the image not present locally
Returns:
bool: Whether the runtime image exists.
+244 -56
View File
@@ -1,7 +1,12 @@
import datetime
import os
import subprocess
import sys
import time
import docker
from openhands import __version__ as oh_version
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.builder.base import RuntimeBuilder
@@ -10,45 +15,139 @@ class DockerRuntimeBuilder(RuntimeBuilder):
def __init__(self, docker_client: docker.DockerClient):
self.docker_client = docker_client
def build(self, path: str, tags: list[str]) -> str:
version_info = self.docker_client.version()
server_version = version_info.get('Version', '')
if tuple(map(int, server_version.split('.'))) < (18, 9):
raise RuntimeError('Docker server version must be >= 18.09 to use BuildKit')
self.max_lines = 10
self.log_lines = [''] * self.max_lines
def build(
self,
path: str,
tags: list[str],
use_local_cache: bool = False,
extra_build_args: list[str] | None = None,
) -> str:
"""Builds a Docker image using BuildKit and handles the build logs appropriately.
Args:
path (str): The path to the Docker build context.
tags (list[str]): A list of image tags to apply to the built image.
use_local_cache (bool, optional): Whether to use and update the local build cache. Defaults to True.
extra_build_args (list[str], optional): Additional arguments to pass to the Docker build command. Defaults to None.
Returns:
str: The name of the built Docker image.
Raises:
RuntimeError: If the Docker server version is incompatible or if the build process fails.
Note:
This method uses Docker BuildKit for improved build performance and caching capabilities.
If `use_local_cache` is True, it will attempt to use and update the build cache in a local directory.
The `extra_build_args` parameter allows for passing additional Docker build arguments as needed.
"""
self.docker_client = docker.from_env()
version_info = self.docker_client.version()
server_version = version_info.get('Version', '')
if tuple(map(int, server_version.split('.'))) < (18, 9):
raise RuntimeError('Docker server version must be >= 18.09 to use BuildKit')
target_image_hash_name = tags[0]
target_image_repo, target_image_hash_tag = target_image_hash_name.split(':')
target_image_tag = tags[1].split(':')[1] if len(tags) > 1 else None
try:
build_logs = self.docker_client.api.build(
path=path,
tag=target_image_hash_name,
rm=True,
decode=True,
)
except docker.errors.BuildError as e:
logger.error(f'Sandbox image build failed: {e}')
raise RuntimeError(f'Sandbox image build failed: {e}')
# Check if the image exists and pull if necessary
self.image_exists(target_image_repo)
layers: dict[str, dict[str, str]] = {}
previous_layer_count = 0
for log in build_logs:
if 'stream' in log:
logger.info(log['stream'].strip())
elif 'error' in log:
logger.error(log['error'].strip())
elif 'status' in log:
self._output_build_progress(log, layers, previous_layer_count)
previous_layer_count = len(layers)
else:
logger.info(str(log))
buildx_cmd = [
'docker',
'buildx',
'build',
'--progress=plain',
f'--build-arg=OPENHANDS_RUNTIME_VERSION={oh_version}',
f'--build-arg=OPENHANDS_RUNTIME_BUILD_TIME={datetime.datetime.now().isoformat()}',
f'--tag={target_image_hash_name}',
'--load',
]
cache_dir = '/tmp/.buildx-cache'
if use_local_cache and self._is_cache_usable(cache_dir):
buildx_cmd.extend(
[
f'--cache-from=type=local,src={cache_dir}',
f'--cache-to=type=local,dest={cache_dir},mode=max',
]
)
if extra_build_args:
buildx_cmd.extend(extra_build_args)
buildx_cmd.append(path) # must be last!
print('================ DOCKER BUILD STARTED ================')
if sys.stdout.isatty():
sys.stdout.write('\n' * self.max_lines)
sys.stdout.flush()
try:
process = subprocess.Popen(
buildx_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
)
if process.stdout:
for line in iter(process.stdout.readline, ''):
line = line.strip()
if line:
self._output_logs(line)
return_code = process.wait()
if return_code != 0:
raise subprocess.CalledProcessError(
return_code,
process.args,
output=None,
stderr=None,
)
except subprocess.CalledProcessError as e:
logger.error(f'Image build failed:\n{e}')
logger.error(f'Command output:\n{e.output}')
raise
except subprocess.TimeoutExpired:
logger.error('Image build timed out')
raise
except FileNotFoundError as e:
logger.error(f'Python executable not found: {e}')
raise
except PermissionError as e:
logger.error(
f'Permission denied when trying to execute the build command:\n{e}'
)
raise
except Exception as e:
logger.error(f'An unexpected error occurred during the build process: {e}')
raise
logger.info(f'Image [{target_image_hash_name}] build finished.')
assert (
target_image_tag
), f'Expected target image tag [{target_image_tag}] is None'
image = self.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}]'
)
if target_image_tag:
image = self.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 = self.docker_client.images.get(target_image_hash_name)
@@ -67,11 +166,12 @@ class DockerRuntimeBuilder(RuntimeBuilder):
)
return target_image_hash_name
def image_exists(self, image_name: str) -> bool:
def image_exists(self, image_name: str, pull_from_repo: bool = True) -> bool:
"""Check if the image exists in the registry (try to pull it first) or in the local store.
Args:
image_name (str): The Docker image to check (<image repo>:<image tag>)
pull_from_repo (bool): Whether to pull from the remote repo if the image not present locally
Returns:
bool: Whether the Docker image exists in the registry or in the local store
"""
@@ -80,13 +180,18 @@ class DockerRuntimeBuilder(RuntimeBuilder):
return False
try:
logger.info(f'Checking, if image exists locally:\n{image_name}')
logger.debug(f'Checking, if image exists locally:\n{image_name}')
self.docker_client.images.get(image_name)
logger.info('Image found locally.')
logger.debug('Image found locally.')
return True
except docker.errors.ImageNotFound:
if not pull_from_repo:
logger.debug(
f'Image {image_name} not found locally'
)
return False
try:
logger.info(
logger.debug(
'Image not found locally. Trying to pull it, please wait...'
)
@@ -97,10 +202,10 @@ class DockerRuntimeBuilder(RuntimeBuilder):
):
self._output_build_progress(line, layers, previous_layer_count)
previous_layer_count = len(layers)
logger.info('Image pulled')
logger.debug('Image pulled')
return True
except docker.errors.ImageNotFound:
logger.info('Could not find image locally or in registry.')
logger.debug('Could not find image locally or in registry.')
return False
except Exception as e:
msg = 'Image could not be pulled: '
@@ -109,9 +214,30 @@ class DockerRuntimeBuilder(RuntimeBuilder):
msg += 'image not found in registry.'
else:
msg += f'{ex_msg}'
logger.warning(msg)
logger.debug(msg)
return False
def _output_logs(self, new_line: str) -> None:
"""Display the last 10 log_lines in the console (not for file logging).
This will create the effect of a rolling display in the console.
'\033[F' moves the cursor up one line.
'\033[2K\r' clears the line and moves the cursor to the beginning of the line.
"""
if not sys.stdout.isatty():
logger.debug(new_line)
return
self.log_lines.pop(0)
self.log_lines.append(new_line[:80])
sys.stdout.write('\033[F' * (self.max_lines))
sys.stdout.flush()
for line in self.log_lines:
sys.stdout.write('\033[2K' + line + '\n')
sys.stdout.flush()
def _output_build_progress(
self, current_line: dict, layers: dict, previous_layer_count: int
) -> None:
@@ -126,31 +252,93 @@ class DockerRuntimeBuilder(RuntimeBuilder):
if 'progress' in current_line:
layers[layer_id]['progress'] = current_line['progress']
if (
'total' in current_line['progressDetail']
and 'current' in current_line['progressDetail']
):
total = current_line['progressDetail']['total']
current = current_line['progressDetail']['current']
percentage = (current / total) * 100
else:
percentage = 0
if 'progressDetail' in current_line:
progress_detail = current_line['progressDetail']
if 'total' in progress_detail and 'current' in progress_detail:
total = progress_detail['total']
current = progress_detail['current']
percentage = min(
(current / total) * 100, 100
) # Ensure it doesn't exceed 100%
else:
percentage = (
100 if layers[layer_id]['status'] == 'Download complete' else 0
)
# refresh process bar in console if stdout is a tty
if sys.stdout.isatty():
sys.stdout.write('\033[F' * previous_layer_count)
for lid, layer_data in sorted(layers.items()):
sys.stdout.write('\033[K')
print(
f'Layer {lid}: {layer_data["progress"]} {layer_data["status"]}'
)
sys.stdout.write('\033[2K\r')
status = layer_data['status']
progress = layer_data['progress']
if status == 'Download complete':
print(f'Layer {lid}: Download complete')
elif status == 'Already exists':
print(f'Layer {lid}: Already exists')
else:
print(f'Layer {lid}: {progress} {status}')
sys.stdout.flush()
# otherwise Log only if percentage is at least 10% higher than last logged
elif percentage != 0 and percentage - layers[layer_id]['last_logged'] >= 10:
logger.info(
elif percentage != 0 and (
percentage - layers[layer_id]['last_logged'] >= 10 or percentage == 100
):
logger.debug(
f'Layer {layer_id}: {layers[layer_id]["progress"]} {layers[layer_id]["status"]}'
)
layers[layer_id]['last_logged'] = percentage
elif 'status' in current_line:
logger.info(current_line['status'])
logger.debug(current_line['status'])
def _prune_old_cache_files(self, cache_dir: str, max_age_days: int = 7) -> None:
"""
Prune cache files older than the specified number of days.
Args:
cache_dir (str): The path to the cache directory.
max_age_days (int): The maximum age of cache files in days.
"""
try:
current_time = time.time()
max_age_seconds = max_age_days * 24 * 60 * 60
for root, _, files in os.walk(cache_dir):
for file in files:
file_path = os.path.join(root, file)
try:
file_age = current_time - os.path.getmtime(file_path)
if file_age > max_age_seconds:
os.remove(file_path)
logger.debug(f'Removed old cache file: {file_path}')
except Exception as e:
logger.warning(f'Error processing cache file {file_path}: {e}')
except Exception as e:
logger.warning(f'Error during build cache pruning: {e}')
def _is_cache_usable(self, cache_dir: str) -> bool:
"""
Check if the cache directory is usable (exists and is writable).
Args:
cache_dir (str): The path to the cache directory.
Returns:
bool: True if the cache directory is usable, False otherwise.
"""
if not os.path.exists(cache_dir):
try:
os.makedirs(cache_dir, exist_ok=True)
logger.debug(f'Created cache directory: {cache_dir}')
except OSError as e:
logger.debug(f'Failed to create cache directory {cache_dir}: {e}')
return False
if not os.access(cache_dir, os.W_OK):
logger.warning(
f'Cache directory {cache_dir} is not writable. Caches will not be used for Docker builds.'
)
return False
self._prune_old_cache_files(cache_dir)
logger.debug(f'Cache directory {cache_dir} is usable')
return True
+1 -1
View File
@@ -98,7 +98,7 @@ class RemoteRuntimeBuilder(RuntimeBuilder):
# Wait before polling again
sleep_if_should_continue(30)
def image_exists(self, image_name: str) -> bool:
def image_exists(self, image_name: str, pull_from_repo: bool = True) -> bool:
"""Checks if an image exists in the remote registry using the /image_exists endpoint."""
params = {'image': image_name}
response = send_request(
+20 -62
View File
@@ -11,6 +11,7 @@ import os
import re
import shutil
import subprocess
import time
from contextlib import asynccontextmanager
from pathlib import Path
@@ -28,7 +29,6 @@ from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
@@ -36,7 +36,6 @@ from openhands.events.action import (
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileEditObservation,
FileReadObservation,
FileWriteObservation,
IPythonRunCellObservation,
@@ -50,11 +49,6 @@ from openhands.runtime.plugins import (
JupyterPlugin,
Plugin,
)
from openhands.runtime.plugins.agent_skills.file_ops import (
append_file,
create_file,
edit_file_by_replace,
)
from openhands.runtime.utils import split_bash_commands
from openhands.runtime.utils.files import insert_lines, read_lines
@@ -69,10 +63,6 @@ INIT_COMMANDS = [
]
SOFT_TIMEOUT_SECONDS = 5
HEAD = '<<<<<<< SEARCH'
DIVIDER = '======='
TAIL = '>>>>>>> REPLACE'
class RuntimeClient:
"""RuntimeClient is running inside docker sandbox.
@@ -97,6 +87,8 @@ class RuntimeClient:
self.lock = asyncio.Lock()
self.plugins: dict[str, Plugin] = {}
self.browser = BrowserEnv(browsergym_eval_env)
self.start_time = time.time()
self.last_execution_time = self.start_time
@property
def initial_pwd(self):
@@ -331,7 +323,13 @@ class RuntimeClient:
logger.debug('Requesting exit code...')
self.shell.expect(self.__bash_expect_regex, timeout=timeout)
_exit_code_output = self.shell.before
exit_code = int(_exit_code_output.strip().split()[0])
try:
exit_code = int(_exit_code_output.strip().split()[0])
except:
logger.error('Error getting exit code from bash script')
# If we try to run an invalid shell script the output sometimes includes error text
# rather than the error code - we assume this is an error
exit_code = 2
except pexpect.TIMEOUT as e:
if kill_on_timeout:
@@ -520,55 +518,6 @@ class RuntimeClient:
return ErrorObservation(f'Malformed paths not permitted: {filepath}')
return FileWriteObservation(content='', path=filepath)
async def edit(self, action: FileEditAction) -> Observation:
diff_blocks = re.search(
f'(.*)\n{HEAD}(.*)\n{DIVIDER}(.*)\n{TAIL}', action.diff_block, re.DOTALL
)
if not diff_blocks or len(diff_blocks.groups()) < 3:
found_head = re.search(f'{HEAD}', action.diff_block) is not None
found_divider = re.search(f'{DIVIDER}', action.diff_block) is not None
found_tail = re.search(f'{TAIL}', action.diff_block) is not None
error_msg = 'Could not resolve diff block into search/replace blocks.'
if found_head and (not found_tail):
error_msg = 'The diff block got cut off because it is too long. Try breaking it into smaller SEARCH/REPLACE blocks.'
elif found_head and found_tail and (not found_divider):
error_msg = 'Could not find the divider between SEARCH/REPLACE blocks.'
return ErrorObservation(error_msg)
path = diff_blocks.group(1)
search_block = diff_blocks.group(2)
replace_block = diff_blocks.group(3)
if search_block:
search_block = search_block[1:]
if replace_block:
replace_block = replace_block[1:]
working_dir = self._get_working_directory()
filepath = self._resolve_path(path, working_dir)
if not search_block:
create_file(filename=filepath)
append_file(
file_name=filepath,
content=replace_block,
)
else:
if search_block == replace_block:
return ErrorObservation(
'Search block should not be same as Replace block.'
)
edit_file_by_replace(
file_name=filepath,
to_replace=search_block,
new_content=replace_block,
)
return FileEditObservation(
content=action.diff_block,
path=filepath,
search_block=search_block,
replace_block=replace_block,
)
async def browse(self, action: BrowseURLAction) -> Observation:
return await browse(action, self.browser)
@@ -660,6 +609,14 @@ if __name__ == '__main__':
response = await call_next(request)
return response
@app.get('/server_info')
async def get_server_info():
assert client is not None
current_time = time.time()
uptime = current_time - client.start_time
idle_time = current_time - client.last_execution_time
return {'uptime': uptime, 'idle_time': idle_time}
@app.post('/execute_action')
async def execute_action(action_request: ActionRequest):
assert client is not None
@@ -667,10 +624,11 @@ if __name__ == '__main__':
action = event_from_dict(action_request.action)
if not isinstance(action, Action):
raise HTTPException(status_code=400, detail='Invalid action type')
client.last_execution_time = time.time()
observation = await client.run_action(action)
return event_to_dict(observation)
except Exception as e:
logger.error(f'Error processing command: {str(e)}')
logger.error(f'Error processing command: {str(e)}', exc_info=True, stack_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.post('/upload_file')
+2 -5
View File
@@ -10,6 +10,7 @@ import requests
import tenacity
from openhands.core.config import AppConfig
from openhands.core.logger import DEBUG
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.events.action import (
@@ -17,7 +18,6 @@ from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
@@ -249,7 +249,7 @@ class EventStreamRuntime(Runtime):
'port': str(self._container_port),
'PYTHONUNBUFFERED': 1,
}
if self.config.debug:
if self.config.debug or DEBUG:
environment['DEBUG'] = 'true'
logger.debug(f'Workspace Base: {self.config.workspace_base}')
@@ -452,9 +452,6 @@ class EventStreamRuntime(Runtime):
def write(self, action: FileWriteAction) -> Observation:
return self.run_action(action)
def edit(self, action: FileEditAction) -> Observation:
return self.run_action(action)
def browse(self, action: BrowseURLAction) -> Observation:
return self.run_action(action)
@@ -10,18 +10,9 @@ import_functions(
module=file_reader, function_names=file_reader.__all__, target_globals=globals()
)
__all__ = file_ops.__all__ + file_reader.__all__
__except__ = [
'create_file',
'edit_file_by_replace',
'insert_content_at_line',
'append_file',
] ## DISABLED TEMPORARILY.
DOCUMENTATION = ''
for func_name in __all__:
if func_name in __except__:
continue
func = globals()[func_name]
cur_doc = func.__doc__
@@ -5,8 +5,3 @@ import_functions(
module=file_ops, function_names=file_ops.__all__, target_globals=globals()
)
__all__ = file_ops.__all__
create_file = file_ops.create_file
append_file = file_ops.append_file
edit_file_by_replace = file_ops.edit_file_by_replace
insert_content_at_line = file_ops.insert_content_at_line
@@ -786,7 +786,6 @@ def append_file(file_name: str, content: str) -> None:
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.
"""
ret_str = _edit_file_impl(
+1 -1
View File
@@ -61,7 +61,7 @@ class RemoteRuntime(Runtime):
self.config = config
if self.config.sandbox.api_hostname == 'localhost':
self.config.sandbox.api_hostname = 'api.all-hands.dev/v0/runtime'
logger.warning(
logger.info(
'Using localhost as the API hostname is not supported in the RemoteRuntime. Please set a proper hostname.\n'
'Setting it to default value: api.all-hands.dev/v0/runtime'
)
-5
View File
@@ -14,7 +14,6 @@ from openhands.events.action import (
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileEditAction,
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
@@ -180,10 +179,6 @@ class Runtime:
def write(self, action: FileWriteAction) -> Observation:
pass
@abstractmethod
def edit(self, action: FileEditAction) -> Observation:
pass
@abstractmethod
def browse(self, action: BrowseURLAction) -> Observation:
pass
+25 -24
View File
@@ -6,11 +6,11 @@ import subprocess
import tempfile
import docker
import toml
from dirhash import dirhash
from jinja2 import Environment, FileSystemLoader
import openhands
from openhands import __version__ as oh_version
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.builder import DockerRuntimeBuilder, RuntimeBuilder
@@ -19,19 +19,6 @@ def get_runtime_image_repo():
return os.getenv('OH_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/all-hands-ai/runtime')
def _get_package_version():
"""Read the version from pyproject.toml.
Returns:
- The version specified in pyproject.toml under [tool.poetry]
"""
project_root = os.path.dirname(os.path.dirname(os.path.abspath(openhands.__file__)))
pyproject_path = os.path.join(project_root, 'pyproject.toml')
with open(pyproject_path, 'r') as f:
pyproject_data = toml.load(f)
return pyproject_data['tool']['poetry']['version']
def _put_source_code_to_dir(temp_dir: str):
"""Builds the project source tarball directly in temp_dir and unpacks it.
The OpenHands source code ends up in the temp_dir/code directory.
@@ -46,7 +33,7 @@ def _put_source_code_to_dir(temp_dir: str):
logger.info(f'Building source distribution using project root: {project_root}')
# Fetch the correct version from pyproject.toml
package_version = _get_package_version()
package_version = oh_version
tarball_filename = f'openhands_ai-{package_version}.tar.gz'
tarball_path = os.path.join(temp_dir, tarball_filename)
@@ -154,14 +141,23 @@ def prep_docker_build_folder(
file.write(dockerfile_content)
# Get the MD5 hash of the dir_path directory
dist_hash = dirhash(dir_path, 'md5')
dir_hash = dirhash(
dir_path,
'md5',
ignore=[
'.*/', # hidden directories
'__pycache__/',
'*.pyc',
],
)
hash = f'v{oh_version}_{dir_hash}'
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)}): {dist_hash}\n'
f'Hash for docker build directory [{dir_path}] (contents: {os.listdir(dir_path)}): {hash}\n'
)
return dist_hash
return hash
def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]:
@@ -188,7 +184,6 @@ def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]:
if ':' not in base_image:
base_image = base_image + ':latest'
[repo, tag] = base_image.split(':')
oh_version = _get_package_version()
# Hash the repo if it's too long
if len(repo) > 32:
@@ -256,7 +251,7 @@ def build_runtime_image(
# Scenario 1: If we already have an image with the exact same hash, then it means the image is already built
# with the exact same source code and Dockerfile, so we will reuse it. Building it is not required.
if not force_rebuild and runtime_builder.image_exists(hash_runtime_image_name):
if not force_rebuild and runtime_builder.image_exists(hash_runtime_image_name, False):
logger.info(
f'Image [{hash_runtime_image_name}] already exists so we will reuse it.'
)
@@ -370,14 +365,20 @@ def _build_sandbox_image(
target_image_hash_name = f'{target_image_repo}:{target_image_hash_tag}'
target_image_generic_name = f'{target_image_repo}:{target_image_tag}'
tags_to_add = [target_image_hash_name]
# Only add the generic tag if the image does not exist
# so it does not get overwritten & only points to the earliest version
# to avoid "too many layers" after many re-builds
if not runtime_builder.image_exists(target_image_generic_name):
tags_to_add.append(target_image_generic_name)
try:
image_name = runtime_builder.build(
path=docker_folder, tags=[target_image_hash_name, target_image_generic_name]
)
image_name = runtime_builder.build(path=docker_folder, tags=tags_to_add)
if not image_name:
raise RuntimeError(f'Build failed for image {target_image_hash_name}')
except Exception as e:
logger.error(f'Sandbox image build failed: {e}')
logger.error(f'Sandbox image build failed: {str(e)}')
raise
return image_name
@@ -69,8 +69,7 @@ RUN \
/openhands/miniforge3/bin/mamba run -n base poetry run pip install playwright && \
/openhands/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium && \
# Set environment variables
export OH_INTERPRETER_PATH=$(/openhands/miniforge3/bin/mamba run -n base poetry run python -c "import sys; print(sys.executable)") && \
export OH_VENV_PATH=$(/openhands/miniforge3/bin/mamba run -n base poetry env info --path) && \
echo "OH_INTERPRETER_PATH=$(/openhands/miniforge3/bin/mamba run -n base poetry run python -c "import sys; print(sys.executable)")" >> /etc/environment && \
# Install extra dependencies if specified
{{ extra_deps }} {% if extra_deps %} && {% endif %} \
# Clear caches
@@ -81,16 +80,6 @@ RUN \
# Clean up
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/miniforge3/bin/mamba clean --all
{% if not skip_init %}
RUN \
# Add the Poetry virtual environment to the bashrc
echo "export OH_INTERPRETER_PATH=\"$OH_INTERPRETER_PATH\"" >> /etc/bash.bashrc && \
echo "export OH_VENV_PATH=\"$OH_VENV_PATH\"" >> /etc/bash.bashrc && \
# Activate the Poetry virtual environment
echo 'source "$OH_VENV_PATH/bin/activate"' >> /etc/bash.bashrc && \
# Use the Poetry virtual environment's Python interpreter
echo 'alias python="$OH_INTERPRETER_PATH"' >> /etc/bash.bashrc
{% endif %}
# ================================================================
# END: Copy Project and Install/Update Dependencies
# ================================================================
+34 -5
View File
@@ -1,5 +1,4 @@
import asyncio
from threading import Thread
from typing import Callable, Optional
@@ -8,6 +7,9 @@ from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig, AppConfig, LLMConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema.agent import AgentState
from openhands.events.action.agent import ChangeAgentStateAction
from openhands.events.event import EventSource
from openhands.events.stream import EventStream
from openhands.runtime import get_runtime_cls
from openhands.runtime.runtime import Runtime
@@ -29,6 +31,7 @@ class AgentSession:
runtime: Runtime | None = None
security_analyzer: SecurityAnalyzer | None = None
_closed: bool = False
loop: asyncio.AbstractEventLoop
def __init__(self, sid: str, file_store: FileStore):
"""Initializes a new instance of the Session class
@@ -41,6 +44,7 @@ class AgentSession:
self.sid = sid
self.event_stream = EventStream(sid, file_store)
self.file_store = file_store
self.loop = asyncio.new_event_loop()
async def start(
self,
@@ -68,10 +72,32 @@ class AgentSession:
'Session already started. You need to close this session and start a new one.'
)
self.loop = asyncio.new_event_loop()
self.thread = Thread(target=self._run, daemon=True)
self.thread.start()
coro = self._start(
runtime_name,
config,
agent,
max_iterations,
max_budget_per_task,
agent_to_llm_config,
agent_configs,
status_message_callback,
)
asyncio.run_coroutine_threadsafe(coro, self.loop) # type: ignore
async def _start(
self,
runtime_name: str,
config: AppConfig,
agent: Agent,
max_iterations: int,
max_budget_per_task: float | None = None,
agent_to_llm_config: dict[str, LLMConfig] | None = None,
agent_configs: dict[str, AgentConfig] | None = None,
status_message_callback: Optional[Callable] = None,
):
self._create_security_analyzer(config.security.security_analyzer)
self._create_runtime(runtime_name, config, agent, status_message_callback)
self._create_controller(
@@ -82,9 +108,12 @@ class AgentSession:
agent_to_llm_config=agent_to_llm_config,
agent_configs=agent_configs,
)
if self.controller is not None:
self.controller.agent_task = asyncio.run_coroutine_threadsafe(self.controller.start_step_loop(), self.loop) # type: ignore
self.event_stream.add_event(
ChangeAgentStateAction(AgentState.INIT), EventSource.USER
)
if self.controller:
self.controller.agent_task = self.controller.start_step_loop()
await self.controller.agent_task # type: ignore
def _run(self):
asyncio.set_event_loop(self.loop)
+8 -4
View File
@@ -123,9 +123,6 @@ class Session:
f'Error creating controller. Please check Docker is running and visit `{TROUBLESHOOTING_URL}` for more debugging information..'
)
return
self.agent_session.event_stream.add_event(
ChangeAgentStateAction(AgentState.INIT), EventSource.USER
)
async def on_event(self, event: Event):
"""Callback function for events that mainly come from the agent.
@@ -165,6 +162,9 @@ class Session:
'Model does not support image upload, change to a different model or try without an image.'
)
return
asyncio.run_coroutine_threadsafe(self._add_event(event, EventSource.USER), self.agent_session.loop) # type: ignore
async def _add_event(self, event, event_source):
self.agent_session.event_stream.add_event(event, EventSource.USER)
async def send(self, data: dict[str, object]) -> bool:
@@ -190,6 +190,10 @@ class Session:
"""Sends a message to the client."""
return await self.send({'message': message})
async def send_status_message(self, message: str) -> bool:
"""Sends a status message to the client."""
return await self.send({'status': message})
def update_connection(self, ws: WebSocket):
self.websocket = ws
self.is_alive = True
@@ -205,4 +209,4 @@ class Session:
def queue_status_message(self, message: str):
"""Queues a status message to be sent asynchronously."""
# Ensure the coroutine runs in the main event loop
asyncio.run_coroutine_threadsafe(self.send_message(message), self.loop)
asyncio.run_coroutine_threadsafe(self.send_status_message(message), self.loop)
Generated
+93 -93
View File
@@ -571,17 +571,17 @@ files = [
[[package]]
name = "boto3"
version = "1.35.25"
version = "1.35.28"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "boto3-1.35.25-py3-none-any.whl", hash = "sha256:b1cfad301184cdd44dfd4805187ccab12de8dd28dd12a11a5cfdace17918c6de"},
{file = "boto3-1.35.25.tar.gz", hash = "sha256:5df4e2cbe3409db07d3a0d8d63d5220ce3202a78206ad87afdbb41519b26ce45"},
{file = "boto3-1.35.28-py3-none-any.whl", hash = "sha256:dc088b86a14f17d3cd2e96915c6ccfd31bce640dfe9180df579ed311bc6bf0fc"},
{file = "boto3-1.35.28.tar.gz", hash = "sha256:8960fc458b9ba3c8a9890a607c31cee375db821f39aefaec9ff638248e81644a"},
]
[package.dependencies]
botocore = ">=1.35.25,<1.36.0"
botocore = ">=1.35.28,<1.36.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@@ -590,13 +590,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.35.25"
version = "1.35.28"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
files = [
{file = "botocore-1.35.25-py3-none-any.whl", hash = "sha256:e58d60260abf10ccc4417967923117c9902a6a0cff9fddb6ea7ff42dc1bd4630"},
{file = "botocore-1.35.25.tar.gz", hash = "sha256:76c5706b2c6533000603ae8683a297c887abbbaf6ee31e1b2e2863b74b2989bc"},
{file = "botocore-1.35.28-py3-none-any.whl", hash = "sha256:b66c78f3d6379bd16f0362f07168fa7699cdda3921fc880047192d96f2c8c527"},
{file = "botocore-1.35.28.tar.gz", hash = "sha256:115d13f2172d8e9fa92e8d913f0e80092b97624d190f46772ed2930d4a355d55"},
]
[package.dependencies]
@@ -609,32 +609,32 @@ crt = ["awscrt (==0.21.5)"]
[[package]]
name = "browsergym"
version = "0.7.0"
version = "0.7.1"
description = "BrowserGym: a gym environment for web task automation in the Chromium browser"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym-0.7.0-py3-none-any.whl", hash = "sha256:e2b98d2990ec1bfd80fd3e8034e60a60f363a5240be794e0ace975f24601d1a8"},
{file = "browsergym-0.7.0.tar.gz", hash = "sha256:e1cd9812b32a9387bac42b726bf7669c35a46b5fe6d1faf939333f095d5a6ba5"},
{file = "browsergym-0.7.1-py3-none-any.whl", hash = "sha256:af216abf3e1ad538e4d31e5bf96da03768ac4aabc9a566159355fa2b6af093da"},
{file = "browsergym-0.7.1.tar.gz", hash = "sha256:c269eb8b6da4bd186c05529f3492a9bef2210a89e2cdae4b7557b6ae7091c28e"},
]
[package.dependencies]
browsergym-core = "0.7.0"
browsergym-experiments = "0.7.0"
browsergym-miniwob = "0.7.0"
browsergym-visualwebarena = "0.7.0"
browsergym-webarena = "0.7.0"
browsergym-core = "0.7.1"
browsergym-experiments = "0.7.1"
browsergym-miniwob = "0.7.1"
browsergym-visualwebarena = "0.7.1"
browsergym-webarena = "0.7.1"
browsergym-workarena = "*"
[[package]]
name = "browsergym-core"
version = "0.7.0"
version = "0.7.1"
description = "BrowserGym: a gym environment for web task automation in the Chromium browser"
optional = false
python-versions = ">3.9"
files = [
{file = "browsergym_core-0.7.0-py3-none-any.whl", hash = "sha256:4f4c7a153daa984701f76e81eaa358b4a9684e8f3fb4dcd80c807e7ed8112914"},
{file = "browsergym_core-0.7.0.tar.gz", hash = "sha256:069987057dcdea2c25b1b631691f93d77c2d042108079c16874128dcc459d809"},
{file = "browsergym_core-0.7.1-py3-none-any.whl", hash = "sha256:28a79537e91fd0dff639fbed9d1f3318b99f8aa5efe054f2468fc0bf2d220ba6"},
{file = "browsergym_core-0.7.1.tar.gz", hash = "sha256:da6bdd190a8ccdc8394e68a2a17701b7af3208e0267ca7ed9fd33dc4c2c7ea99"},
]
[package.dependencies]
@@ -643,67 +643,67 @@ gymnasium = ">=0.27"
lxml = ">=4.9"
numpy = ">=1.14"
pillow = ">=10.1"
playwright = ">=1.32,<1.40"
playwright = ">=1.39,<2.0"
pyparsing = ">=3"
[[package]]
name = "browsergym-experiments"
version = "0.7.0"
version = "0.7.1"
description = "Experimentation tools for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_experiments-0.7.0-py3-none-any.whl", hash = "sha256:c10f810eb631622804ebbf5e5783636cf8aff2a53ea0e38bfcfb129273865b1b"},
{file = "browsergym_experiments-0.7.0.tar.gz", hash = "sha256:9ee937720d2b84563851a2ae2c94c685da299fbadd957ba743ef7f1351fd0e23"},
{file = "browsergym_experiments-0.7.1-py3-none-any.whl", hash = "sha256:0f3104da708436fe93460cd609590d28aa9dcbeda68d8a51599a56daaea7cd96"},
{file = "browsergym_experiments-0.7.1.tar.gz", hash = "sha256:75f9e5676e625cb7ec4a5fbce8d6832708b01cb4e5af9c150cfd27182af60a74"},
]
[package.dependencies]
browsergym-core = "0.7.0"
browsergym-core = "0.7.1"
tiktoken = ">=0.4"
[[package]]
name = "browsergym-miniwob"
version = "0.7.0"
version = "0.7.1"
description = "MiniWoB++ benchmark for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_miniwob-0.7.0-py3-none-any.whl", hash = "sha256:9223400aa737dcbca79884a6174b67635ec5b913f490232b60e5391fc34eecb4"},
{file = "browsergym_miniwob-0.7.0.tar.gz", hash = "sha256:b4d248541a86f9dc21c9fc5a03699ef16dfd96a97d9347d3c6ef4ae9145f691f"},
{file = "browsergym_miniwob-0.7.1-py3-none-any.whl", hash = "sha256:69f560b5d0210a5db3b2672d0ac48e274170f765832e3628da3fd0ba694d3f40"},
{file = "browsergym_miniwob-0.7.1.tar.gz", hash = "sha256:635909cbe0646985699fc65715c463e258c04dad16521bfa59cb0ae4ec797f8f"},
]
[package.dependencies]
browsergym-core = "0.7.0"
browsergym-core = "0.7.1"
[[package]]
name = "browsergym-visualwebarena"
version = "0.7.0"
version = "0.7.1"
description = "VisualWebArena benchmark for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_visualwebarena-0.7.0-py3-none-any.whl", hash = "sha256:499124dd8a0619905049598428205cad4d3237e6acef80225f3c734f428b16b9"},
{file = "browsergym_visualwebarena-0.7.0.tar.gz", hash = "sha256:78fd89a922b94b7de912b6ab44d48845a25283eb7265c526811542f6833edbaa"},
{file = "browsergym_visualwebarena-0.7.1-py3-none-any.whl", hash = "sha256:bf9bb0d2f406276531aee10dd04371b152cfa2c703402291e57a04ea47847c43"},
{file = "browsergym_visualwebarena-0.7.1.tar.gz", hash = "sha256:b26db1d75a9ecae7d97a1bbefad2d7ea10e49119e4aec8320cf6f8bcd265e45c"},
]
[package.dependencies]
browsergym-core = "0.7.0"
browsergym-core = "0.7.1"
libvisualwebarena = "0.0.8"
requests = "*"
[[package]]
name = "browsergym-webarena"
version = "0.7.0"
version = "0.7.1"
description = "WebArena benchmark for BrowserGym"
optional = false
python-versions = ">3.7"
files = [
{file = "browsergym_webarena-0.7.0-py3-none-any.whl", hash = "sha256:d04b2cdadce47ffc9b4d6751f7f5dbd403e561cf4bf2b80801edcbb03bcf8ce6"},
{file = "browsergym_webarena-0.7.0.tar.gz", hash = "sha256:f7b0839ca009962457a03c948261fb36fbcbababd60208132ec77f92c6a19a59"},
{file = "browsergym_webarena-0.7.1-py3-none-any.whl", hash = "sha256:117cb2946d8a9b3536d0a55300eb28b7650c0e70339855bed8890f0f8fc887e9"},
{file = "browsergym_webarena-0.7.1.tar.gz", hash = "sha256:568de29ab0a7a1a569855e1bb71af5cfd4e982156988a64e30ada3bfe309b399"},
]
[package.dependencies]
browsergym-core = "0.7.0"
browsergym-core = "0.7.1"
libwebarena = "0.0.3"
[[package]]
@@ -1014,13 +1014,13 @@ numpy = "*"
[[package]]
name = "chromadb"
version = "0.5.7"
version = "0.5.11"
description = "Chroma."
optional = false
python-versions = ">=3.8"
files = [
{file = "chromadb-0.5.7-py3-none-any.whl", hash = "sha256:2358f92804cd198b125de73076ec48f9f55c729df119919a76a6716ad0e465f6"},
{file = "chromadb-0.5.7.tar.gz", hash = "sha256:3432865025ef3ceeaee0a59b265a784d8b5978cb7c41593c74ddd2427c776c94"},
{file = "chromadb-0.5.11-py3-none-any.whl", hash = "sha256:f02d9326869cea926f980bd6c9a0150a0ef2e151072f325998c16a9502fb4b25"},
{file = "chromadb-0.5.11.tar.gz", hash = "sha256:252e970b3e1a27b594cc7b3685238691bf8eaa232225d4dee9e33ec83580775f"},
]
[package.dependencies]
@@ -1351,13 +1351,13 @@ typing-inspect = ">=0.4.0,<1"
[[package]]
name = "datasets"
version = "3.0.0"
version = "3.0.1"
description = "HuggingFace community-driven open-source library of datasets"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "datasets-3.0.0-py3-none-any.whl", hash = "sha256:c23fefb6c953dcb1cd5f6deb6c502729c733ef98791e0c3f2d80c7ca2d9a01dd"},
{file = "datasets-3.0.0.tar.gz", hash = "sha256:592317eb137f0fc5aac068ff283ba13c3c66d10c9c034d44bc8aa584126cf3e2"},
{file = "datasets-3.0.1-py3-none-any.whl", hash = "sha256:db080aab41c8cc68645117a0f172e5c6789cbc672f066de0aa5a08fc3eebc686"},
{file = "datasets-3.0.1.tar.gz", hash = "sha256:40d63b09e76a3066c32e746d6fdc36fd3f29ed2acd49bf5b1a2100da32936511"},
]
[package.dependencies]
@@ -1379,15 +1379,15 @@ xxhash = "*"
[package.extras]
audio = ["librosa", "soundfile (>=0.12.1)", "soxr (>=0.4.0)"]
benchmarks = ["tensorflow (==2.12.0)", "torch (==2.0.1)", "transformers (==4.30.1)"]
dev = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch", "torch (>=2.0.0)", "transformers", "transformers (>=4.42.0)", "zstandard"]
dev = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch", "torch (>=2.0.0)", "torchdata", "transformers", "transformers (>=4.42.0)", "zstandard"]
docs = ["s3fs", "tensorflow (>=2.6.0)", "torch", "transformers"]
jax = ["jax (>=0.3.14)", "jaxlib (>=0.3.14)"]
quality = ["ruff (>=0.3.0)"]
s3 = ["s3fs"]
tensorflow = ["tensorflow (>=2.6.0)"]
tensorflow-gpu = ["tensorflow (>=2.6.0)"]
tests = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch (>=2.0.0)", "transformers (>=4.42.0)", "zstandard"]
tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "transformers (>=4.42.0)", "zstandard"]
tests = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch (>=2.0.0)", "torchdata", "transformers (>=4.42.0)", "zstandard"]
tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (<8.0.0)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "torchdata", "transformers (>=4.42.0)", "zstandard"]
torch = ["torch"]
vision = ["Pillow (>=9.4.0)"]
@@ -2142,13 +2142,13 @@ test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit",
[[package]]
name = "google-ai-generativelanguage"
version = "0.6.9"
version = "0.6.10"
description = "Google Ai Generativelanguage API client library"
optional = false
python-versions = ">=3.7"
files = [
{file = "google_ai_generativelanguage-0.6.9-py3-none-any.whl", hash = "sha256:50360cd80015d1a8cc70952e98560f32fa06ddee2e8e9f4b4b98e431dc561e0b"},
{file = "google_ai_generativelanguage-0.6.9.tar.gz", hash = "sha256:899f1d3a06efa9739f1cd9d2788070178db33c89d4a76f2e8f4da76f649155fa"},
{file = "google_ai_generativelanguage-0.6.10-py3-none-any.whl", hash = "sha256:854a2bf833d18be05ad5ef13c755567b66a4f4a870f099b62c61fe11bddabcf4"},
{file = "google_ai_generativelanguage-0.6.10.tar.gz", hash = "sha256:6fa642c964d8728006fe7e8771026fc0b599ae0ebeaf83caf550941e8e693455"},
]
[package.dependencies]
@@ -2240,13 +2240,13 @@ httplib2 = ">=0.19.0"
[[package]]
name = "google-cloud-aiplatform"
version = "1.67.1"
version = "1.68.0"
description = "Vertex AI API client library"
optional = false
python-versions = ">=3.8"
files = [
{file = "google-cloud-aiplatform-1.67.1.tar.gz", hash = "sha256:701a19061c8c670baa93464ca0b8a1a8720494f802187cef06bc9fcf952db315"},
{file = "google_cloud_aiplatform-1.67.1-py2.py3-none-any.whl", hash = "sha256:2ff0e1794839fcf74d644f3f54ff2de5d8099b3e388edecc48f6d620c1f3582c"},
{file = "google-cloud-aiplatform-1.68.0.tar.gz", hash = "sha256:d74e9f33707c7a14c6a32a7cfe9acd32b90975dfba9fac487d105c8ba5197f40"},
{file = "google_cloud_aiplatform-1.68.0-py2.py3-none-any.whl", hash = "sha256:24dacc34457665ab6054bdf47e2475793dcf2d865b568420a909b452a477b3e6"},
]
[package.dependencies]
@@ -2277,7 +2277,7 @@ pipelines = ["pyyaml (>=5.3.1,<7)"]
prediction = ["docker (>=5.0.3)", "fastapi (>=0.71.0,<=0.114.0)", "httpx (>=0.23.0,<0.25.0)", "starlette (>=0.17.1)", "uvicorn[standard] (>=0.16.0)"]
private-endpoints = ["requests (>=2.28.1)", "urllib3 (>=1.21.1,<1.27)"]
ray = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=6.0.1)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "setuptools (<70.0.0)"]
ray-testing = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=6.0.1)", "pytest-xdist", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "ray[train] (==2.9.3)", "scikit-learn", "setuptools (<70.0.0)", "tensorflow", "torch (>=2.0.0,<2.1.0)", "xgboost", "xgboost-ray"]
ray-testing = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=6.0.1)", "pytest-xdist", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "ray[train]", "scikit-learn", "setuptools (<70.0.0)", "tensorflow", "torch (>=2.0.0,<2.1.0)", "xgboost", "xgboost-ray"]
reasoningengine = ["cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<3)"]
tensorboard = ["tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"]
testing = ["bigframes", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.114.0)", "google-api-core (>=2.11,<3.0.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "grpcio-testing", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "ipython", "kfp (>=2.6.0,<3.0.0)", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.16.0)", "nltk", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pytest-asyncio", "pytest-xdist", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || >=2.33.dev0,<=2.33.0)", "ray[default] (>=2.5,<=2.33.0)", "requests (>=2.28.1)", "requests-toolbelt (<1.0.0)", "scikit-learn", "sentencepiece (>=0.2.0)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (==2.13.0)", "tensorflow (==2.16.1)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "torch (>=2.0.0,<2.1.0)", "torch (>=2.2.0)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)", "xgboost"]
@@ -2457,16 +2457,16 @@ testing = ["pytest"]
[[package]]
name = "google-generativeai"
version = "0.8.1"
version = "0.8.2"
description = "Google Generative AI High level API client library and tools."
optional = false
python-versions = ">=3.9"
files = [
{file = "google_generativeai-0.8.1-py3-none-any.whl", hash = "sha256:b031877f24d51af0945207657c085896a0a886eceec7a1cb7029327b0aa6e2f6"},
{file = "google_generativeai-0.8.2-py3-none-any.whl", hash = "sha256:fabc0e2e8d2bfb6fdb1653e91dba83fecb2a2a6878883b80017def90fda8032d"},
]
[package.dependencies]
google-ai-generativelanguage = "0.6.9"
google-ai-generativelanguage = "0.6.10"
google-api-core = "*"
google-api-python-client = "*"
google-auth = ">=2.15.0"
@@ -3234,13 +3234,13 @@ files = [
[[package]]
name = "json-repair"
version = "0.29.4"
version = "0.29.5"
description = "A package to repair broken json strings"
optional = false
python-versions = ">=3.8"
files = [
{file = "json_repair-0.29.4-py3-none-any.whl", hash = "sha256:2d7addfa01e3b4c295c4ebabd5f393127adae0d345616d3a2517df8260429dae"},
{file = "json_repair-0.29.4.tar.gz", hash = "sha256:2921760e707ac0d0b63478402fd6ea3162d4191adf873b396becb31c47a1ac30"},
{file = "json_repair-0.29.5-py3-none-any.whl", hash = "sha256:4932e65ce9d61f820e1741c9fff4e9801db87cd542166bfff56f2ea4f2a94104"},
{file = "json_repair-0.29.5.tar.gz", hash = "sha256:a6bb7999ee179856efc404fe1b96c15b1e26bb371dd2f246382464b5332d4d1a"},
]
[[package]]
@@ -3762,13 +3762,13 @@ types-tqdm = "*"
[[package]]
name = "litellm"
version = "1.48.0"
version = "1.48.5"
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.48.0-py3-none-any.whl", hash = "sha256:7765e8a92069778f5fc66aacfabd0e2f8ec8d74fb117f5e475567d89b0d376b9"},
{file = "litellm-1.48.0.tar.gz", hash = "sha256:31a9b8a25a9daf44c24ddc08bf74298da920f2c5cea44135e5061278d0aa6fc9"},
{file = "litellm-1.48.5-py3-none-any.whl", hash = "sha256:d69ae3809047922f1051f0c501759be3363421e1e17230dc4483f8e630f364d1"},
{file = "litellm-1.48.5.tar.gz", hash = "sha256:7b698623647c588e7740ada3b1faea2ee005804d93c4e6c3c975a7f0a77b751a"},
]
[package.dependencies]
@@ -3805,19 +3805,19 @@ pydantic = ">=1.10"
[[package]]
name = "llama-index"
version = "0.11.12"
version = "0.11.14"
description = "Interface between LLMs and your data"
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "llama_index-0.11.12-py3-none-any.whl", hash = "sha256:a7d0b4065df2689cec1baeab9bfaed4d94e4ddc7e941df2ee47abfb218ce3ea1"},
{file = "llama_index-0.11.12.tar.gz", hash = "sha256:6b9220bf4c76a4ac0a82ccc642c3ea94f51381a9718ac601021f2fa95b74aab1"},
{file = "llama_index-0.11.14-py3-none-any.whl", hash = "sha256:69447a25cb73f910146200e8f45579e0a6e5e390bb2818f229e68fbb625e0a2d"},
{file = "llama_index-0.11.14.tar.gz", hash = "sha256:6d18093550bdf92442dc7aa0e4d9fef2616941e3d101409340d47c7a99b9f739"},
]
[package.dependencies]
llama-index-agent-openai = ">=0.3.4,<0.4.0"
llama-index-cli = ">=0.3.1,<0.4.0"
llama-index-core = ">=0.11.11,<0.12.0"
llama-index-core = ">=0.11.14,<0.12.0"
llama-index-embeddings-openai = ">=0.2.4,<0.3.0"
llama-index-indices-managed-llama-cloud = ">=0.3.0"
llama-index-legacy = ">=0.9.48,<0.10.0"
@@ -3863,13 +3863,13 @@ llama-index-llms-openai = ">=0.2.0,<0.3.0"
[[package]]
name = "llama-index-core"
version = "0.11.12"
version = "0.11.14"
description = "Interface between LLMs and your data"
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "llama_index_core-0.11.12-py3-none-any.whl", hash = "sha256:7dc7ead649bac8f09e61c6c8bf93d257f68a7315223552421be4f0ffc3a8054d"},
{file = "llama_index_core-0.11.12.tar.gz", hash = "sha256:ce2dd037ff889d9ea6b25872228cc9de614c10445d19377f6ae5c66b93a50c61"},
{file = "llama_index_core-0.11.14-py3-none-any.whl", hash = "sha256:e63e5b1f4daa56952a7846cbbf0265b1288909efaea866216a4c6fb65daa2923"},
{file = "llama_index_core-0.11.14.tar.gz", hash = "sha256:6ff7be9f5bbb04be0d8064f76510edf79f8a9833ebae28b46261b274556827ca"},
]
[package.dependencies]
@@ -5365,13 +5365,13 @@ sympy = "*"
[[package]]
name = "openai"
version = "1.47.1"
version = "1.50.2"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.7.1"
files = [
{file = "openai-1.47.1-py3-none-any.whl", hash = "sha256:34277583bf268bb2494bc03f48ac123788c5e2a914db1d5a23d5edc29d35c825"},
{file = "openai-1.47.1.tar.gz", hash = "sha256:62c8f5f478f82ffafc93b33040f8bb16a45948306198bd0cba2da2ecd9cf7323"},
{file = "openai-1.50.2-py3-none-any.whl", hash = "sha256:822dd2051baa3393d0d5406990611975dd6f533020dc9375a34d4fe67e8b75f7"},
{file = "openai-1.50.2.tar.gz", hash = "sha256:3987ae027152fc8bea745d60b02c8f4c4a76e1b5c70e73565fa556db6f78c9e6"},
]
[package.dependencies]
@@ -7123,13 +7123,13 @@ files = [
[[package]]
name = "reportlab"
version = "4.2.2"
version = "4.2.4"
description = "The Reportlab Toolkit"
optional = false
python-versions = "<4,>=3.7"
files = [
{file = "reportlab-4.2.2-py3-none-any.whl", hash = "sha256:927616931637e2f13e2ee3b3b6316d7a07803170e258621cff7d138bde17fbb5"},
{file = "reportlab-4.2.2.tar.gz", hash = "sha256:765eecbdd68491c56947e29c38b8b69b834ee5dbbdd2fb7409f08ebdebf04428"},
{file = "reportlab-4.2.4-py3-none-any.whl", hash = "sha256:6e4d86647b8bfd772f475a58f9b0dcba4b340b1969f0db36333089f6ca9ab362"},
{file = "reportlab-4.2.4.tar.gz", hash = "sha256:a00b57292e156a7bda84edf31d60c25578153076c8fb96331d0c59eddda052c8"},
]
[package.dependencies]
@@ -7367,29 +7367,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.6.7"
version = "0.6.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"},
{file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"},
{file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"},
{file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"},
{file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"},
{file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"},
{file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"},
{file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"},
{file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"},
{file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
{file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
{file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
{file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
{file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
{file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
{file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
{file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
{file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
]
[[package]]
@@ -9688,4 +9688,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "90636ce436e5c05146a69730f461f46fd3185b595be37d3eafd8aef36667db81"
content-hash = "78e09d0b5c33f39ec951659658b5b4b46ba206d8f95e9a154be4e0ef869b7c79"
+5 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openhands-ai"
version = "0.9.4"
version = "0.9.6"
description = "OpenHands: Code Less, Make More"
authors = ["OpenHands"]
license = "MIT"
@@ -27,7 +27,7 @@ uvicorn = "*"
types-toml = "*"
numpy = "*"
json-repair = "*"
browsergym = "0.7.0" # integrate browsergym as the browsing interface
browsergym = "0.7.1" # integrate browsergym as the browsing interface
html2text = "*"
e2b = "^0.17.1"
pexpect = "*"
@@ -65,7 +65,7 @@ llama-index-embeddings-azure-openai = "*"
llama-index-embeddings-ollama = "*"
[tool.poetry.group.dev.dependencies]
ruff = "0.6.7"
ruff = "0.6.8"
mypy = "1.11.2"
pre-commit = "3.8.0"
build = "*"
@@ -85,6 +85,7 @@ reportlab = "*"
[tool.coverage.run]
concurrency = ["gevent"]
[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
@@ -115,6 +116,7 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"
+5 -5
View File
@@ -11,7 +11,7 @@ from http.server import HTTPServer, SimpleHTTPRequestHandler
import pytest
from litellm import completion
from openhands.llm.llm import message_separator
from openhands.llm.debug_mixin import MESSAGE_SEPARATOR
script_dir = os.environ.get('SCRIPT_DIR')
project_root = os.environ.get('PROJECT_ROOT')
@@ -81,19 +81,19 @@ def _format_messages(messages):
message_str = ''
for message in messages:
if isinstance(message, str):
message_str += message_separator + message if message_str else message
message_str += MESSAGE_SEPARATOR + message if message_str else message
elif isinstance(message, dict):
if isinstance(message['content'], list):
for m in message['content']:
if isinstance(m, str):
message_str += message_separator + m if message_str else m
message_str += MESSAGE_SEPARATOR + m if message_str else m
elif isinstance(m, dict) and m['type'] == 'text':
message_str += (
message_separator + m['text'] if message_str else m['text']
MESSAGE_SEPARATOR + m['text'] if message_str else m['text']
)
elif isinstance(message['content'], str):
message_str += (
message_separator + message['content']
MESSAGE_SEPARATOR + message['content']
if message_str
else message['content']
)
@@ -117,7 +117,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -117,7 +117,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -117,7 +117,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -117,7 +117,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -117,7 +117,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -116,7 +116,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -116,7 +116,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -116,7 +116,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -116,7 +116,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -466,11 +465,11 @@ ls -la
----------
OBSERVATION:
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -466,11 +465,11 @@ ls -la
----------
OBSERVATION:
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
@@ -485,9 +484,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -466,11 +465,11 @@ ls -la
----------
OBSERVATION:
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
@@ -485,9 +484,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -504,7 +503,7 @@ mkdir my_project && cd my_project
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -466,11 +465,11 @@ ls -la
----------
OBSERVATION:
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
@@ -485,9 +484,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -504,7 +503,7 @@ mkdir my_project && cd my_project
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -521,7 +520,7 @@ echo "Hello, this is a test file." > test.txt
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -466,11 +465,11 @@ ls -la
----------
OBSERVATION:
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
@@ -485,9 +484,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -504,7 +503,7 @@ mkdir my_project && cd my_project
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -521,7 +520,7 @@ echo "Hello, this is a test file." > test.txt
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -538,19 +537,19 @@ git init && git add test.txt && git status
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/my_project/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/my_project/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 128]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -418,9 +417,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -437,19 +436,19 @@ git init
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -466,11 +465,11 @@ ls -la
----------
OBSERVATION:
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
total 4
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .
drwxr-xr-x 1 root root 4096 Sep 8 15:57 ..
drwxrwxrwx 1 pn pn 4096 Sep 8 15:57 .git
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 0]
@@ -485,9 +484,9 @@ git status
----------
OBSERVATION:
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 128]
@@ -504,7 +503,7 @@ mkdir my_project && cd my_project
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -521,7 +520,7 @@ echo "Hello, this is a test file." > test.txt
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -538,19 +537,19 @@ git init && git add test.txt && git status
----------
OBSERVATION:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/my_project/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
error: chmod on /workspace/my_project/.git/config.lock failed: Operation not permitted
fatal: could not set 'core.filemode' to 'false'
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 128]
@@ -570,11 +569,11 @@ echo "Initial commit: Add test.txt
----------
OBSERVATION:
Initial commit: Add test.txt
- Created a new file 'test.txt' with a simple hello message
- This is the first commit in the repository
Initial commit: Add test.txt
- Created a new file 'test.txt' with a simple hello message
- This is the first commit in the repository
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace/my_project $
[Command -1 finished with exit code 0]
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -121,7 +121,6 @@ append_file(file_name: str, content: str) -> None:
It appends text `content` to the end of the specified file, ideal after a `create_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:
@@ -420,14 +419,14 @@ chmod +x hello.sh
----------
OBSERVATION:
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $ echo 'echo "hello"' >> hello.sh
openhands@docker-desktop:/workspace $ echo 'echo "hello"' >> hello.sh
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $ chmod +x hello.sh
chmod: changing permissions of 'hello.sh': Operation not permitted
openhands@docker-desktop:/workspace $ chmod +x hello.sh
chmod: changing permissions of 'hello.sh': Operation not permitted
[Python Interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.11/bin/python]
openhands@docker-desktop:/workspace $
[Command -1 finished with exit code 1]

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