Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ef79cbe22 | |||
| 1f13d80ddc | |||
| 58de5221f5 | |||
| 9bbb35ec18 | |||
| 797f02ff6f | |||
| 47d9621742 | |||
| 8c12f5b67d | |||
| 6132a58367 | |||
| 2fe2f4c530 | |||
| 4d8f812f88 | |||
| e5cb80d59d | |||
| ee9bea393f | |||
| 940de86caa | |||
| fe5ecb6da8 | |||
| 41b8f3e4a7 | |||
| 93ecd82e61 | |||
| 052149ccf3 | |||
| 3aba722a59 | |||
| 4738640f4e | |||
| bf6f7b9943 | |||
| 0f20965999 | |||
| e6034b301b | |||
| 9e50b88f9f | |||
| fd0ecdd4e0 | |||
| c6105f264f | |||
| 93f271579c | |||
| 1976763152 | |||
| 516ca701d4 | |||
| f3b2085f9b | |||
| 97a03faf33 | |||
| 06ed142191 | |||
| dbd0786345 | |||
| 3bc9a485b8 | |||
| 2b845d9568 | |||
| d7e0db0b35 | |||
| 508681691a | |||
| 1c9b4ad78a | |||
| ecb1b9b2a0 | |||
| e4f8708656 | |||
| 822de89394 | |||
| c5e89be6de | |||
| 386688da5f | |||
| 6c01d25976 | |||
| 05e2b0c352 | |||
| 5066468c36 | |||
| 9349009074 | |||
| 5ffff742de | |||
| 50dc17c65c | |||
| f4d6b3262d | |||
| cb3168da12 | |||
| 2b7517e542 | |||
| dadada18ce | |||
| ab3851593d | |||
| 5100d12cea | |||
| a9cf7e6ee6 | |||
| 8392a3fb6b | |||
| c376b81505 | |||
| 8440604dd1 | |||
| a4d75cd190 | |||
| 4db929b986 | |||
| b6b38fcd37 | |||
| d619be96d1 | |||
| 6f44ea0115 | |||
| 0f118df910 | |||
| 43c4a7fff4 | |||
| 57187417b7 | |||
| 03b5b03bb2 | |||
| 681276f27c | |||
| 82a154f7e7 | |||
| 5b7ab28511 | |||
| ca3f39e918 | |||
| 6c2630e506 | |||
| 260e41486e | |||
| 9b0fb8f81a | |||
| cd360ef6aa | |||
| 5bb46525a4 | |||
| f80e4a9e5d | |||
| 60c5fd41ec | |||
| 688068a44e | |||
| ee158feb15 | |||
| ec25abd98b | |||
| bb28dea51f | |||
| 2a99aa6679 | |||
| add4653335 | |||
| 2557c18fb1 | |||
| bc31fb15fe | |||
| 1b66f2e777 | |||
| d1a741792f | |||
| 2bc3e8d584 | |||
| 0bb0903a22 | |||
| dd3a701b93 | |||
| d4e6ea5e49 | |||
| 510a82a039 | |||
| d8a87d7ccb | |||
| ff64085042 | |||
| 31a2dbb372 | |||
| 7df3ca4ac8 | |||
| 0a9369df5e | |||
| 327393670a | |||
| d283420ac2 | |||
| 57ad0583b7 | |||
| 2f359b4f29 | |||
| 5f3ee286bf | |||
| f979d612ec | |||
| 75d5591816 | |||
| 7068a73ae7 | |||
| 15a32e973e | |||
| 6fcc4ca052 | |||
| c83fab8a00 | |||
| 1e2796e168 | |||
| e5bff4bca8 | |||
| 72a705f9a5 | |||
| 6b8adde7bc | |||
| a130367ec7 | |||
| 52cc0977ca | |||
| 1955fba2ca | |||
| 46dae57cf2 |
@@ -38,10 +38,6 @@ jobs:
|
||||
base_image:
|
||||
- image: 'nikolaik/python-nodejs:python3.11-nodejs22'
|
||||
tag: nikolaik
|
||||
- image: 'python:3.11-bookworm'
|
||||
tag: python
|
||||
- image: 'node:22-bookworm'
|
||||
tag: node
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -70,31 +66,39 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'poetry'
|
||||
- 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: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Create source distribution and Dockerfile
|
||||
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
|
||||
- name: Build and push runtime image ${{ matrix.base_image.image }}
|
||||
if: "!github.event.pull_request.head.repo.fork"
|
||||
if: github.event.pull_request.head.repo.fork != true
|
||||
run: |
|
||||
./containers/build.sh runtime ${{ github.repository_owner }} --push ${{ 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"
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: ghcr.io/all-hands-ai/runtime:${{ github.sha }}-${{ matrix.base_image.tag }}
|
||||
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
context: containers/runtime
|
||||
- name: Upload runtime image for fork
|
||||
if: "github.event.pull_request.head.repo.fork"
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
@@ -103,11 +107,12 @@ jobs:
|
||||
# Run unit tests with the EventStream runtime Docker images
|
||||
test_runtime:
|
||||
name: Test Runtime
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
base_image: ['nikolaik', 'python', 'node']
|
||||
base_image: ['nikolaik']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
@@ -121,26 +126,41 @@ jobs:
|
||||
swap-storage: true
|
||||
# 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"
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: "github.event.pull_request.head.repo.fork"
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- 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'
|
||||
cache: 'poetry'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run runtime tests
|
||||
run: |
|
||||
# We install pytest-xdist in order to run tests across CPUs. However, tests start to fail when we run
|
||||
# then across more than 2 CPUs for some reason
|
||||
poetry run pip install pytest-xdist
|
||||
|
||||
# Install to be able to retry on failures for flaky tests
|
||||
poetry run pip install pytest-rerunfailures
|
||||
|
||||
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ github.sha }}-${{ matrix.base_image }}
|
||||
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
@@ -148,7 +168,7 @@ jobs:
|
||||
SANDBOX_USER_ID=$(id -u) \
|
||||
SANDBOX_BASE_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
poetry run pytest --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
|
||||
poetry run pytest -n 2 --reruns 2 --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
@@ -162,27 +182,35 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
base_image: ['nikolaik', 'python', 'node']
|
||||
base_image: ['nikolaik']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# 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"
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: "github.event.pull_request.head.repo.fork"
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- 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'
|
||||
cache: 'poetry'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run integration tests
|
||||
@@ -201,11 +229,26 @@ jobs:
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
# Checks that all runtime tests have passed
|
||||
all_runtime_tests_passed:
|
||||
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
|
||||
# "All Runtime Tests Passed" is a required job for PRs to merge
|
||||
# Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
|
||||
# prerequisites have been cancelled or failed so merging is disallowed, otherwise Github considers "skipped" as "success"
|
||||
runtime_tests_check_success:
|
||||
name: All Runtime Tests Passed
|
||||
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime, runtime_integration_tests_on_linux]
|
||||
steps:
|
||||
- name: All tests passed
|
||||
run: echo "All runtime tests have passed successfully!"
|
||||
|
||||
runtime_tests_check_fail:
|
||||
name: All Runtime Tests Passed
|
||||
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime, runtime_integration_tests_on_linux]
|
||||
steps:
|
||||
- name: Some tests failed
|
||||
run: |
|
||||
echo "Some runtime tests failed or were cancelled"
|
||||
exit 1
|
||||
|
||||
@@ -22,13 +22,21 @@ jobs:
|
||||
python-version: ['3.11']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
- 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: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
- name: Install & Start Docker
|
||||
|
||||
@@ -3,6 +3,23 @@ name: Regenerate Integration Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug:
|
||||
description: 'Enable debug mode'
|
||||
type: boolean
|
||||
default: true
|
||||
log_to_file:
|
||||
description: 'Enable logging to file'
|
||||
type: boolean
|
||||
default: true
|
||||
force_regenerate_tests:
|
||||
description: 'Force regeneration of tests'
|
||||
type: boolean
|
||||
default: false
|
||||
force_use_llm:
|
||||
description: 'Force use of LLM'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
regenerate_integration_tests:
|
||||
@@ -12,21 +29,32 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
python-version: "3.11"
|
||||
- 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: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
run: make install-python-dependencies
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
|
||||
- name: Regenerate integration tests
|
||||
run: ./tests/integration/regenerate.sh
|
||||
|
||||
run: |
|
||||
DEBUG=${{ inputs.debug }} \
|
||||
LOG_TO_FILE=${{ inputs.log_to_file }} \
|
||||
FORCE_REGENERATE_TESTS=${{ inputs.force_regenerate_tests }} \
|
||||
FORCE_USE_LLM=${{ inputs.force_use_llm }} \
|
||||
./tests/integration/regenerate.sh
|
||||
- name: Commit changes
|
||||
run: |
|
||||
if git diff --quiet --exit-code; then
|
||||
@@ -37,5 +65,6 @@ jobs:
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git add .
|
||||
git commit -m "Regenerate integration tests"
|
||||
# run it twice in case pre-commit makes changes
|
||||
git commit -am "Regenerate integration tests" || git commit -am "Regenerate integration tests"
|
||||
git push
|
||||
|
||||
@@ -1,65 +1,49 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<!--
|
||||
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
||||
*** that would make this better, please fork the repo and create a pull request
|
||||
*** or simply open an issue with the tag "enhancement".
|
||||
*** Don't forget to give the project a star!
|
||||
*** Thanks again! Now go create something AMAZING! :D
|
||||
-->
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
|
||||
<h1 align="center">OpenHands: Code Less, Make More</h1>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
<!--
|
||||
*** I'm using markdown "reference style" links for readability.
|
||||
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
||||
*** See the bottom of this document for the declaration of the reference variables
|
||||
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
||||
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
||||
-->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/network/members"><img src="https://img.shields.io/github/forks/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Forks"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/issues"><img src="https://img.shields.io/github/issues/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Issues"></a>
|
||||
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=blue" alt="Credits"></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
|
||||
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge"></a>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200" height="200">
|
||||
<h1 align="center">OpenHands: Code Less, Make More</h1>
|
||||
<a href="https://docs.all-hands.dev/modules/usage/intro"><img src="https://img.shields.io/badge/Documentation-OpenHands-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper-%20on%20Arxiv-red?logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
|
||||
<br/>
|
||||
<a href="https://huggingface.co/spaces/OpenDevin/evaluation"><img src="https://img.shields.io/badge/Evaluation-Benchmark%20on%20HF%20Space-green?logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark"></a>
|
||||
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
|
||||
<a href="https://huggingface.co/spaces/OpenHands/evaluation"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
|
||||
<hr>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
Welcome to OpenHands, a platform for autonomous software engineers, powered by AI and LLMs (previously called "OpenDevin").
|
||||
Welcome to OpenHands (formerly OpenDevin), a platform for software development agents powered by AI.
|
||||
|
||||
OpenHands agents collaborate with human developers to write code, fix bugs, and ship features.
|
||||
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
|
||||
call APIs, and yes—even copy code snippets from StackOverflow.
|
||||
|
||||
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [Quick Start](#-quick-start).
|
||||
|
||||

|
||||
|
||||
## ⚡ Getting Started
|
||||
OpenHands works best with Docker version 26.0.0+ (Docker Desktop 4.31.0+).
|
||||
You must be using Linux, Mac OS, or WSL on Windows.
|
||||
## ⚡ Quick Start
|
||||
|
||||
To start OpenHands in a docker container, run the following commands in your terminal:
|
||||
The easiest way to run OpenHands is in Docker. You can change `WORKSPACE_BASE` below to
|
||||
point OpenHands to existing code that you'd like to modify.
|
||||
|
||||
> [!WARNING]
|
||||
> When you run the following command, files in `./workspace` may be modified or deleted.
|
||||
See the [Getting Started](https://docs.all-hands.dev/modules/usage/getting-started) guide for
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
WORKSPACE_BASE=$(pwd)/workspace
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
export WORKSPACE_BASE=$(pwd)/workspace
|
||||
|
||||
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) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
@@ -70,29 +54,23 @@ docker run -it \
|
||||
ghcr.io/all-hands-ai/openhands:0.9
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This command pulls the `0.9` tag, which represents the most recent stable release of OpenHands. You have other options as well:
|
||||
> - For a specific release version, use `ghcr.io/all-hands-ai/openhands:<OpenHands_version>` (replace <OpenHands_version> with the desired version number).
|
||||
> - For the most up-to-date development version, use `ghcr.io/all-hands-ai/openhands:main`. This version may be **(unstable!)** and is recommended for testing or development purposes only.
|
||||
>
|
||||
> Choose the tag that best suits your needs based on stability requirements and desired features.
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000) with access to `./workspace`. To have OpenHands operate on your code, place it in `./workspace`.
|
||||
OpenHands will only have access to this workspace folder. The rest of your system will not be affected as it runs in a secured docker sandbox.
|
||||
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).
|
||||
|
||||
Upon opening OpenHands, you must select the appropriate `Model` and enter the `API Key` within the settings that should pop up automatically. These can be set at any time by selecting
|
||||
the `Settings` button (gear icon) in the UI. If the required `Model` does not exist in the list, you can manually enter it in the text box.
|
||||
Visit [Getting Started](https://docs.all-hands.dev/modules/usage/getting-started) for more information and setup instructions.
|
||||
|
||||
For the development workflow, see [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
|
||||
Are you having trouble? Check out our [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting).
|
||||
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting) can help.
|
||||
|
||||
## 🚀 Documentation
|
||||
## 📖 Documentation
|
||||
|
||||
To learn more about the project, and for tips on using OpenHands,
|
||||
**check out our [documentation](https://docs.all-hands.dev/modules/usage/intro)**.
|
||||
**check out our [documentation](https://docs.all-hands.dev/modules/usage/getting-started)**.
|
||||
|
||||
There you'll find resources on how to use different LLM providers (like ollama and Anthropic's Claude),
|
||||
There you'll find resources on how to use different LLM providers,
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
## 🤝 How to Contribute
|
||||
@@ -127,17 +105,6 @@ Let's make software engineering better together!
|
||||
|
||||
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[contributors-url]: https://github.com/All-Hands-AI/OpenHands/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[forks-url]: https://github.com/All-Hands-AI/OpenHands/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[stars-url]: https://github.com/All-Hands-AI/OpenHands/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[issues-url]: https://github.com/All-Hands-AI/OpenHands/issues
|
||||
[license-shield]: https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[license-url]: https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
OpenHands is built by a large number of contributors, and every contribution is greatly appreciated! We also build upon other open source projects, and we are deeply thankful for their work.
|
||||
|
||||
@@ -65,10 +65,15 @@ In order to accomplish my goal I need to send the information asked back to the
|
||||
"""
|
||||
|
||||
|
||||
def get_prompt(error_prefix: str, cur_axtree_txt: str, prev_action_str: str) -> str:
|
||||
def get_prompt(
|
||||
error_prefix: str, cur_url: str, cur_axtree_txt: str, prev_action_str: str
|
||||
) -> str:
|
||||
prompt = f"""\
|
||||
{error_prefix}
|
||||
|
||||
# Current Page URL:
|
||||
{cur_url}
|
||||
|
||||
# Current Accessibility Tree:
|
||||
{cur_axtree_txt}
|
||||
|
||||
@@ -139,6 +144,7 @@ class BrowsingAgent(Agent):
|
||||
"""
|
||||
messages: list[Message] = []
|
||||
prev_actions = []
|
||||
cur_url = ''
|
||||
cur_axtree_txt = ''
|
||||
error_prefix = ''
|
||||
last_obs = None
|
||||
@@ -179,6 +185,9 @@ class BrowsingAgent(Agent):
|
||||
self.error_accumulator += 1
|
||||
if self.error_accumulator > 5:
|
||||
return MessageAction('Too many errors encountered. Task failed.')
|
||||
|
||||
cur_url = last_obs.url
|
||||
|
||||
try:
|
||||
cur_axtree_txt = flatten_axtree_to_str(
|
||||
last_obs.axtree_object,
|
||||
@@ -204,11 +213,13 @@ class BrowsingAgent(Agent):
|
||||
|
||||
messages.append(Message(role='system', content=[TextContent(text=system_msg)]))
|
||||
|
||||
prompt = get_prompt(error_prefix, cur_axtree_txt, prev_action_str)
|
||||
prompt = get_prompt(error_prefix, cur_url, cur_axtree_txt, prev_action_str)
|
||||
messages.append(Message(role='user', content=[TextContent(text=prompt)]))
|
||||
logger.debug(prompt)
|
||||
|
||||
flat_messages = self.llm.format_messages_for_llm(messages)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=[message.model_dump() for message in messages],
|
||||
messages=flat_messages,
|
||||
temperature=0.0,
|
||||
stop=[')```', ')\n```'],
|
||||
)
|
||||
|
||||
@@ -354,7 +354,7 @@ and executed by a program, make sure to follow the formatting instructions.
|
||||
self._prompt += '\n'.join(
|
||||
[
|
||||
f"""\
|
||||
- [{msg['role']}] {msg['message']}"""
|
||||
- [{msg['role']}], {msg['message']}"""
|
||||
for msg in chat_messages
|
||||
]
|
||||
)
|
||||
|
||||
@@ -24,9 +24,9 @@ class BrowsingResponseParser(ResponseParser):
|
||||
if action_str is None:
|
||||
return ''
|
||||
action_str = action_str.strip()
|
||||
if not action_str.endswith('```'):
|
||||
if action_str and not action_str.endswith('```'):
|
||||
action_str = action_str + ')```'
|
||||
logger.info(action_str)
|
||||
logger.debug(action_str)
|
||||
return action_str
|
||||
|
||||
def parse_action(self, action_str: str) -> Action:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
from itertools import islice
|
||||
|
||||
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.logger import openhands_logger as logger
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
@@ -17,6 +19,7 @@ from openhands.events.observation import (
|
||||
AgentDelegateObservation,
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.events.observation.error import ErrorObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
@@ -116,7 +119,11 @@ class CodeActAgent(Agent):
|
||||
):
|
||||
content = [TextContent(text=self.action_to_str(action))]
|
||||
|
||||
if isinstance(action, MessageAction) and action.images_urls:
|
||||
if (
|
||||
self.llm.vision_is_active()
|
||||
and isinstance(action, MessageAction)
|
||||
and action.images_urls
|
||||
):
|
||||
content.append(ImageContent(image_urls=action.images_urls))
|
||||
|
||||
return Message(
|
||||
@@ -126,14 +133,15 @@ class CodeActAgent(Agent):
|
||||
|
||||
def get_observation_message(self, obs: Observation) -> Message | None:
|
||||
max_message_chars = self.llm.config.max_message_chars
|
||||
obs_prefix = 'OBSERVATION:\n'
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
|
||||
text = obs_prefix + truncate_content(obs.content, max_message_chars)
|
||||
text += (
|
||||
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
|
||||
)
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
text = 'OBSERVATION:\n' + obs.content
|
||||
text = obs_prefix + obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splitted = text.split('\n')
|
||||
for i, line in enumerate(splitted):
|
||||
@@ -145,14 +153,16 @@ class CodeActAgent(Agent):
|
||||
text = truncate_content(text, max_message_chars)
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, AgentDelegateObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(
|
||||
str(obs.outputs), max_message_chars
|
||||
)
|
||||
text = obs_prefix + truncate_content(str(obs.outputs), max_message_chars)
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
|
||||
text = obs_prefix + truncate_content(obs.content, max_message_chars)
|
||||
text += '\n[Error occurred in processing last action]'
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, UserRejectObservation):
|
||||
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:
|
||||
# If an observation message is not returned, it will cause an error
|
||||
# when the LLM tries to return the next message
|
||||
@@ -183,9 +193,8 @@ class CodeActAgent(Agent):
|
||||
|
||||
# prepare what we want to send to the LLM
|
||||
messages = self._get_messages(state)
|
||||
|
||||
params = {
|
||||
'messages': [message.model_dump() for message in messages],
|
||||
'messages': self.llm.format_messages_for_llm(messages),
|
||||
'stop': [
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
@@ -194,12 +203,19 @@ class CodeActAgent(Agent):
|
||||
'temperature': 0.0,
|
||||
}
|
||||
|
||||
if self.llm.supports_prompt_caching:
|
||||
if self.llm.is_caching_prompt_active():
|
||||
params['extra_headers'] = {
|
||||
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||
}
|
||||
|
||||
response = self.llm.completion(**params)
|
||||
try:
|
||||
response = self.llm.completion(**params)
|
||||
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.'
|
||||
)
|
||||
|
||||
return self.action_parser.parse(response)
|
||||
|
||||
@@ -210,7 +226,7 @@ class CodeActAgent(Agent):
|
||||
content=[
|
||||
TextContent(
|
||||
text=self.prompt_manager.system_message,
|
||||
cache_prompt=self.llm.supports_prompt_caching, # Cache system prompt
|
||||
cache_prompt=self.llm.is_caching_prompt_active(), # Cache system prompt
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -219,7 +235,7 @@ class CodeActAgent(Agent):
|
||||
content=[
|
||||
TextContent(
|
||||
text=self.prompt_manager.initial_user_message,
|
||||
cache_prompt=self.llm.supports_prompt_caching, # if the user asks the same query,
|
||||
cache_prompt=self.llm.is_caching_prompt_active(), # if the user asks the same query,
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -238,14 +254,14 @@ class CodeActAgent(Agent):
|
||||
if message:
|
||||
# handle error if the message is the SAME role as the previous message
|
||||
# litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
|
||||
# there should not have two consecutive messages from the same role
|
||||
# there shouldn't be two consecutive messages from the same role
|
||||
if messages and messages[-1].role == message.role:
|
||||
messages[-1].content.extend(message.content)
|
||||
else:
|
||||
messages.append(message)
|
||||
|
||||
# Add caching to the last 2 user messages
|
||||
if self.llm.supports_prompt_caching:
|
||||
if self.llm.is_caching_prompt_active():
|
||||
user_turns_processed = 0
|
||||
for message in reversed(messages):
|
||||
if message.role == 'user' and user_turns_processed < 2:
|
||||
@@ -254,14 +270,17 @@ class CodeActAgent(Agent):
|
||||
].cache_prompt = True # Last item inside the message content
|
||||
user_turns_processed += 1
|
||||
|
||||
# the latest user message is important:
|
||||
# The latest user message is important:
|
||||
# we want to remind the agent of the environment constraints
|
||||
latest_user_message = next(
|
||||
(
|
||||
m
|
||||
for m in reversed(messages)
|
||||
if m.role == 'user'
|
||||
and any(isinstance(c, TextContent) for c in m.content)
|
||||
islice(
|
||||
(
|
||||
m
|
||||
for m in reversed(messages)
|
||||
if m.role == 'user'
|
||||
and any(isinstance(c, TextContent) for c in m.content)
|
||||
),
|
||||
1,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
{% set MINIMAL_SYSTEM_PREFIX %}
|
||||
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||
The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with <execute_ipython>.
|
||||
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
|
||||
The assistant can use a Python environment with <execute_ipython>, e.g.:
|
||||
<execute_ipython>
|
||||
print("Hello World!")
|
||||
</execute_ipython>
|
||||
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
|
||||
The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
|
||||
If a bash command returns exit code `-1`, this means the process is not yet finished.
|
||||
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
|
||||
(which will retrieve any additional logs), or it can contain text to be sent to STDIN of the running process,
|
||||
or it can contain the text `ctrl+c` to interrupt the process.
|
||||
|
||||
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
|
||||
Important, however: do not run interactive commands. You do not have access to stdin.
|
||||
Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
|
||||
For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
|
||||
Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
|
||||
For commands that may run indefinitely, the output should be redirected to a file and the command run
|
||||
in the background, e.g. <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
|
||||
If a command execution result says "Command timed out. Sending SIGINT to the process",
|
||||
the assistant should retry running the command in the background.
|
||||
{% endset %}
|
||||
{% set BROWSING_PREFIX %}
|
||||
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
|
||||
@@ -24,7 +27,14 @@ The assistant can install Python packages using the %pip magic command in an IPy
|
||||
{% 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 }}
|
||||
Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
|
||||
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.
|
||||
@@ -32,7 +42,8 @@ The assistant should attempt fewer things at a time instead of putting too many
|
||||
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
|
||||
If the assistant is finished with the task you MUST include <finish></finish> in your response.
|
||||
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
|
||||
The assistant should utilize full file paths and the 'pwd' command to prevent path-related errors. The assistant should refrain from excessive apologies in its responses.
|
||||
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
|
||||
The assistant must avoid apologies and thanks in its responses.
|
||||
|
||||
{% endset %}
|
||||
{# Combine all parts without newlines between them #}
|
||||
|
||||
@@ -94,7 +94,11 @@ class CodeActSWEAgent(Agent):
|
||||
):
|
||||
content = [TextContent(text=self.action_to_str(action))]
|
||||
|
||||
if isinstance(action, MessageAction) and action.images_urls:
|
||||
if (
|
||||
self.llm.vision_is_active()
|
||||
and isinstance(action, MessageAction)
|
||||
and action.images_urls
|
||||
):
|
||||
content.append(ImageContent(image_urls=action.images_urls))
|
||||
|
||||
return Message(
|
||||
@@ -156,9 +160,8 @@ class CodeActSWEAgent(Agent):
|
||||
|
||||
# prepare what we want to send to the LLM
|
||||
messages: list[Message] = self._get_messages(state)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=[message.model_dump() for message in messages],
|
||||
messages=self.llm.format_messages_for_llm(messages),
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
|
||||
@@ -73,10 +73,13 @@ class MicroAgent(Agent):
|
||||
latest_user_message=last_user_message,
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if last_image_urls:
|
||||
if self.llm.vision_is_active() and last_image_urls:
|
||||
content.append(ImageContent(image_urls=last_image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(messages=[message.model_dump()])
|
||||
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)
|
||||
return action
|
||||
|
||||
@@ -46,8 +46,8 @@ class PlannerAgent(Agent):
|
||||
state, self.llm.config.max_message_chars
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if image_urls:
|
||||
if self.llm.vision_is_active() and image_urls:
|
||||
content.append(ImageContent(image_urls=image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(messages=[message.model_dump()])
|
||||
resp = self.llm.completion(messages=self.llm.format_messages_for_llm(message))
|
||||
return self.response_parser.parse(resp)
|
||||
|
||||
@@ -126,21 +126,29 @@ embedding_model = ""
|
||||
# Model to use
|
||||
model = "gpt-4o"
|
||||
|
||||
# Number of retries to attempt
|
||||
#num_retries = 5
|
||||
# Number of retries to attempt when an operation fails with the LLM.
|
||||
# Increase this value to allow more attempts before giving up
|
||||
#num_retries = 8
|
||||
|
||||
# Retry maximum wait time
|
||||
#retry_max_wait = 60
|
||||
# Maximum wait time (in seconds) between retry attempts
|
||||
# This caps the exponential backoff to prevent excessively long
|
||||
#retry_max_wait = 120
|
||||
|
||||
# Retry minimum wait time
|
||||
#retry_min_wait = 3
|
||||
# Minimum wait time (in seconds) between retry attempts
|
||||
# This sets the initial delay before the first retry
|
||||
#retry_min_wait = 15
|
||||
|
||||
# Retry multiplier for exponential backoff
|
||||
# Multiplier for exponential backoff calculation
|
||||
# The wait time increases by this factor after each failed attempt
|
||||
# A value of 2.0 means each retry waits twice as long as the previous one
|
||||
#retry_multiplier = 2.0
|
||||
|
||||
# Drop any unmapped (unsupported) params without causing an exception
|
||||
#drop_params = false
|
||||
|
||||
# Using the prompt caching feature provided by the LLM
|
||||
#caching_prompt = false
|
||||
|
||||
# Base URL for the OLLAMA API
|
||||
#ollama_base_url = ""
|
||||
|
||||
@@ -153,6 +161,9 @@ model = "gpt-4o"
|
||||
# Top p for the API
|
||||
#top_p = 0.5
|
||||
|
||||
# If model is vision capable, this option allows to disable image processing (useful for cost reduction).
|
||||
#disable_vision = true
|
||||
|
||||
[llm.gpt3]
|
||||
# API key to use
|
||||
api_key = "your-api-key"
|
||||
|
||||
@@ -4,8 +4,8 @@ import { themes as prismThemes } from "prism-react-renderer";
|
||||
|
||||
const config: Config = {
|
||||
title: "OpenHands",
|
||||
tagline: "An Open Platform for AI Software Developers as Generalist Agents",
|
||||
favicon: "img/logo.png",
|
||||
tagline: "Code Less, Make More",
|
||||
favicon: "img/logo-square.png",
|
||||
|
||||
// Set the production url of your site here
|
||||
url: "https://docs.all-hands.dev",
|
||||
@@ -73,23 +73,28 @@ const config: Config = {
|
||||
type: "docSidebar",
|
||||
sidebarId: "docsSidebar",
|
||||
position: "left",
|
||||
label: "Docs",
|
||||
label: "User Guides",
|
||||
},
|
||||
{
|
||||
type: "docSidebar",
|
||||
sidebarId: "apiSidebar",
|
||||
position: "left",
|
||||
label: "Codebase",
|
||||
label: "Python API",
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
href: "https://all-hands.dev",
|
||||
label: "Company",
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/All-Hands-AI/OpenHands",
|
||||
label: "GitHub",
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'left',
|
||||
},
|
||||
],
|
||||
},
|
||||
prism: {
|
||||
|
||||
@@ -41,4 +41,4 @@ ne peut être aussi puissant que les modèles qui le pilotent -- heureusement, l
|
||||
|
||||
Certains LLM ont des limites de taux et peuvent nécessiter des réessais. OpenHands réessaiera automatiquement les demandes s'il reçoit une erreur 429 ou une erreur de connexion API.
|
||||
Vous pouvez définir les variables d'environnement `LLM_NUM_RETRIES`, `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` pour contrôler le nombre de réessais et le temps entre les réessais.
|
||||
Par défaut, `LLM_NUM_RETRIES` est 5 et `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` sont respectivement de 3 secondes et 60 secondes.
|
||||
Par défaut, `LLM_NUM_RETRIES` est 8 et `LLM_RETRY_MIN_WAIT`, `LLM_RETRY_MAX_WAIT` sont respectivement de 15 secondes et 120 secondes.
|
||||
|
||||
@@ -43,4 +43,4 @@ OpenHands 将向你配置的 LLM 发出许多提示。大多数这些 LLM 都是
|
||||
|
||||
一些 LLM 有速率限制,可能需要重试操作。OpenHands 会在收到 429 错误或 API 连接错误时自动重试请求。
|
||||
你可以设置 `LLM_NUM_RETRIES`,`LLM_RETRY_MIN_WAIT`,`LLM_RETRY_MAX_WAIT` 环境变量来控制重试次数和重试之间的时间。
|
||||
默认情况下,`LLM_NUM_RETRIES` 为 5,`LLM_RETRY_MIN_WAIT` 和 `LLM_RETRY_MAX_WAIT` 分别为 3 秒和 60 秒。
|
||||
默认情况下,`LLM_NUM_RETRIES` 为 8,`LLM_RETRY_MIN_WAIT` 和 `LLM_RETRY_MAX_WAIT` 分别为 15 秒和 120 秒。
|
||||
|
||||
@@ -74,7 +74,7 @@ WORKSPACE_DIR="$(pwd)/workspace"
|
||||
|
||||
如有需要,可以替换您选择的 `LLM_MODEL`。
|
||||
|
||||
完成!现在您可以通过 `make run` 启动 Devin 而无需 Docker。现在您应该可以连接到 `http://localhost:3000/`
|
||||
完成!现在您可以通过 `make run` 启动 OpenHands 而无需 Docker。现在您应该可以连接到 `http://localhost:3000/`
|
||||
|
||||
## 选择您的模型
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ sidebar_position: 8
|
||||
# 📚 Misc
|
||||
|
||||
## ⭐️ Research Strategy
|
||||
|
||||
Achieving full replication of production-grade applications with LLMs is a complex endeavor. Our strategy involves:
|
||||
|
||||
1. **Core Technical Research:** Focusing on foundational research to understand and improve the technical aspects of code generation and handling
|
||||
@@ -13,9 +14,11 @@ Achieving full replication of production-grade applications with LLMs is a compl
|
||||
4. **Evaluation:** Establishing comprehensive evaluation metrics to better understand and improve our models
|
||||
|
||||
## 🚧 Default Agent
|
||||
|
||||
Our default Agent is currently the [CodeActAgent](agents), which is capable of generating code and handling files.
|
||||
|
||||
## 🤝 How to Contribute
|
||||
|
||||
OpenHands is a community-driven project, and we welcome contributions from everyone. Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of software engineering with AI, there are many ways to get involved:
|
||||
|
||||
- **Code Contributions:** Help us develop the core functionalities, frontend interface, or sandboxing solutions
|
||||
@@ -25,6 +28,7 @@ OpenHands is a community-driven project, and we welcome contributions from every
|
||||
For details, please check [this document](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## 🤖 Join Our Community
|
||||
|
||||
We have both Slack workspace for the collaboration on building OpenHands and Discord server for discussion about anything related, e.g., this project, LLM, agent, etc.
|
||||
|
||||
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA)
|
||||
@@ -37,6 +41,7 @@ If you would love to contribute, feel free to join our community. Let's simplify
|
||||
[](https://star-history.com/#All-Hands-AI/OpenHands&Date)
|
||||
|
||||
## 🛠️ Built With
|
||||
|
||||
OpenHands is built using a combination of powerful frameworks and libraries, providing a robust foundation for its development. Here are the key technologies used in the project:
|
||||
|
||||
       
|
||||
@@ -44,4 +49,5 @@ OpenHands is built using a combination of powerful frameworks and libraries, pro
|
||||
Please note that the selection of these technologies is in progress, and additional technologies may be added or existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to enhance the capabilities of OpenHands.
|
||||
|
||||
## 📜 License
|
||||
|
||||
Distributed under the MIT License. See [our license](https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE) for more information.
|
||||
|
||||
@@ -3,8 +3,11 @@ sidebar_position: 3
|
||||
---
|
||||
|
||||
# 🧠 Main Agent and Capabilities
|
||||
|
||||
## CodeActAgent
|
||||
|
||||
### Description
|
||||
|
||||
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a
|
||||
unified **code** action space for both _simplicity_ and _performance_.
|
||||
|
||||
@@ -19,6 +22,7 @@ The conceptual idea is illustrated below. At each turn, the agent can:
|
||||

|
||||
|
||||
### 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)_.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
## System Requirements
|
||||
|
||||
* Docker version 26.0.0+ or Docker Desktop 4.31.0+
|
||||
* You must be using Linux or Mac OS
|
||||
* If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
|
||||
|
||||
## Installation
|
||||
|
||||
The easiest way to run OpenHands is in Docker. You can change `WORKSPACE_BASE` below to point OpenHands to
|
||||
existing code that you'd like to modify.
|
||||
|
||||
```bash
|
||||
export WORKSPACE_BASE=$(pwd)/workspace
|
||||
|
||||
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) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
ghcr.io/all-hands-ai/openhands:0.9
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Setup
|
||||
|
||||
After running the command above, you'll find OpenHands running at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
The agent will have access to the `./workspace` folder to do its work. You can copy existing code here, or change `WORKSPACE_BASE` in the
|
||||
command to point to an existing folder.
|
||||
|
||||
Upon launching OpenHands, you'll see a settings modal. You must select an LLM backend using `Model`, and enter a corresponding `API Key`.
|
||||
These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
|
||||
If the required `Model` does not exist in the list, you can toggle `Use custom model` and manually enter it in the text box.
|
||||
|
||||
<img src="/img/settings-screenshot.png" alt="settings-modal" width="340" />
|
||||
|
||||
## Versions
|
||||
|
||||
The command above pulls the `0.9` tag, which represents 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.
|
||||
|
||||
You can choose the tag that best suits your needs based on stability requirements and desired features.
|
||||
|
||||
For the development workflow, see [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
|
||||
Are you having trouble? Check out our [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting).
|
||||
@@ -0,0 +1,108 @@
|
||||
# CLI Mode
|
||||
|
||||
OpenHands can be run in an interactive CLI mode, which allows users to start an interactive session via the command line.
|
||||
|
||||
This mode is different from the [headless mode](headless-mode), which is non-interactive and better for scripting.
|
||||
|
||||
## With Python
|
||||
|
||||
To start an interactive OpenHands session via the command line, follow these steps:
|
||||
|
||||
1. Ensure you have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)
|
||||
|
||||
2. Run the following command:
|
||||
|
||||
```bash
|
||||
poetry run python -m openhands.core.cli
|
||||
```
|
||||
|
||||
This command will start an interactive session where you can input tasks and receive responses from OpenHands.
|
||||
|
||||
You'll need to be sure to set your model, API key, and other settings via environment variables
|
||||
[or the `config.toml` file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml).
|
||||
|
||||
|
||||
## With Docker
|
||||
|
||||
To run OpenHands in CLI mode with Docker, follow these steps:
|
||||
|
||||
1. Set `WORKSPACE_BASE` to the directory you want OpenHands to edit:
|
||||
|
||||
```bash
|
||||
WORKSPACE_BASE=$(pwd)/workspace
|
||||
```
|
||||
|
||||
2. Set `LLM_MODEL` to the model you want to use:
|
||||
|
||||
```bash
|
||||
LLM_MODEL="anthropic/claude-3-5-sonnet-20240620"
|
||||
```
|
||||
|
||||
3. Set `LLM_API_KEY` to your API key:
|
||||
|
||||
```bash
|
||||
LLM_API_KEY="abcde"
|
||||
```
|
||||
|
||||
4. Run the following Docker command:
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
-e LLM_MODEL=$LLM_MODEL \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
ghcr.io/all-hands-ai/openhands:0.9 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
This command will start an interactive session in Docker where you can input tasks and receive responses from OpenHands.
|
||||
|
||||
## Examples of CLI Commands and Expected Outputs
|
||||
|
||||
Here are some examples of CLI commands and their expected outputs:
|
||||
|
||||
### Example 1: Simple Task
|
||||
|
||||
```bash
|
||||
How can I help? >> Write a Python script that prints "Hello, World!"
|
||||
```
|
||||
|
||||
Expected Output:
|
||||
|
||||
```bash
|
||||
🤖 Sure! Here is a Python script that prints "Hello, World!":
|
||||
|
||||
❯ print("Hello, World!")
|
||||
```
|
||||
|
||||
### Example 2: Bash Command
|
||||
|
||||
```bash
|
||||
How can I help? >> Create a directory named "test_dir"
|
||||
```
|
||||
|
||||
Expected Output:
|
||||
|
||||
```bash
|
||||
🤖 Creating a directory named "test_dir":
|
||||
|
||||
❯ mkdir test_dir
|
||||
```
|
||||
|
||||
### Example 3: Error Handling
|
||||
|
||||
```bash
|
||||
How can I help? >> Delete a non-existent file
|
||||
```
|
||||
|
||||
Expected Output:
|
||||
|
||||
```bash
|
||||
🤖 An error occurred. Please try again.
|
||||
```
|
||||
@@ -1,26 +1,19 @@
|
||||
# Create and Use a Custom Docker Sandbox
|
||||
# Custom Sandbox
|
||||
|
||||
The default OpenHands sandbox comes with a [minimal ubuntu configuration](https://github.com/All-Hands-AI/OpenHands/blob/main/containers/sandbox/Dockerfile).
|
||||
Your use case may need additional software installed by default.
|
||||
The sandbox is where the agent does its work. Instead of running commands directly on your computer
|
||||
(which could be dangerous), the agent runs them inside of a Docker container.
|
||||
|
||||
The default OpenHands sandbox (`python-nodejs:python3.11-nodejs22`
|
||||
from [nikolaik/python-nodejs](https://hub.docker.com/r/nikolaik/python-nodejs)) comes with some packages installed such
|
||||
as python and Node.js but your use case may need additional software installed by default.
|
||||
|
||||
There are two ways you can do so:
|
||||
|
||||
1. Use an existing image from docker hub. For instance, if you want to have `nodejs` installed, you can do so by using the `node:20` image
|
||||
1. Use an existing image from docker hub
|
||||
2. Creating your own custom docker image and using it
|
||||
|
||||
If you want to take the first approach, you can skip the `Create Your Docker Image` section.
|
||||
|
||||
For a more feature-rich environment, you might consider using pre-built images like **[nikolaik/python-nodejs](https://hub.docker.com/r/nikolaik/python-nodejs)**, which comes with both Python and Node.js pre-installed, along with many other useful tools and libraries, like:
|
||||
|
||||
- Node.js: 22.x
|
||||
- npm: 10.x
|
||||
- yarn: stable
|
||||
- Python: latest
|
||||
- pip: latest
|
||||
- pipenv: latest
|
||||
- poetry: latest
|
||||
- uv: latest
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure you are able to run OpenHands using the [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) first.
|
||||
@@ -79,7 +72,7 @@ Run OpenHands by running ```make run``` in the top level directory.
|
||||
|
||||
Navigate to ```localhost:3001``` and check if your desired dependencies are available.
|
||||
|
||||
In the case of the example above, running ```node -v``` in the terminal produces ```v20.15.0```
|
||||
In the case of the example above, running ```node -v``` in the terminal produces ```v20.15.0```.
|
||||
|
||||
Congratulations!
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contribute to OpenHands Evaluation Harness
|
||||
# Evaluation
|
||||
|
||||
This guide provides an overview of how to integrate your own evaluation benchmark into the OpenHands framework.
|
||||
|
||||
@@ -12,7 +12,7 @@ Here's an example configuration file you can use to define and use multiple LLMs
|
||||
```toml
|
||||
[llm]
|
||||
# IMPORTANT: add your API key here, and set the model to the one you want to evaluate
|
||||
model = "gpt-4o-2024-05-13"
|
||||
model = "claude-3-5-sonnet-20240620"
|
||||
api_key = "sk-XXX"
|
||||
|
||||
[llm.eval_gpt4_1106_preview_llm]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Running in Headless Mode
|
||||
# Headless Mode
|
||||
|
||||
You can run OpenHands via a CLI, without starting the web application. This makes it easy
|
||||
to automate tasks with OpenHands.
|
||||
You can run OpenHands with a single command, without starting the web application.
|
||||
This makes it easy to write scripts and automate tasks with OpenHands.
|
||||
|
||||
This is different from [CLI Mode](cli-mode), which is interactive, and better for active development.
|
||||
|
||||
## With Python
|
||||
|
||||
To run OpenHands in headless mode with Python,
|
||||
[follow the Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md),
|
||||
and then run:
|
||||
@@ -12,19 +15,32 @@ and then run:
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
You'll need to be sure to set your model, API key, and other settings via environment variables
|
||||
[or the `config.toml` file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml).
|
||||
|
||||
## With Docker
|
||||
To run OpenHands in headless mode with Docker, run:
|
||||
|
||||
1. Set `WORKSPACE_BASE` to the directory you want OpenHands to edit:
|
||||
|
||||
```bash
|
||||
# Set WORKSPACE_BASE to the directory you want OpenHands to edit
|
||||
WORKSPACE_BASE=$(pwd)/workspace
|
||||
```
|
||||
|
||||
# Set LLM_API_KEY to an API key, e.g. for OpenAI or Anthropic
|
||||
2. Set `LLM_MODEL` to the model you want to use:
|
||||
|
||||
```bash
|
||||
LLM_MODEL="anthropic/claude-3-5-sonnet-20240620"
|
||||
```
|
||||
|
||||
3. Set `LLM_API_KEY` to your API key:
|
||||
|
||||
```bash
|
||||
LLM_API_KEY="abcde"
|
||||
```
|
||||
|
||||
# Set LLM_MODEL to the model you want to use
|
||||
LLM_MODEL="gpt-4o"
|
||||
4. Run the following Docker command:
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
@@ -35,7 +51,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
ghcr.io/all-hands-ai/openhands:main \ # TODO: pin a version here
|
||||
python -m openhands.core.main \
|
||||
-t "Write a bash script that prints Hello World"
|
||||
ghcr.io/all-hands-ai/openhands:0.9 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# 🔎 How To Section
|
||||
@@ -1,11 +1,12 @@
|
||||
# Use OpenHands in OpenShift/K8S
|
||||
# Kubernetes
|
||||
|
||||
There are different ways this can be accomplished. This guide goes through one possible way:
|
||||
There are different ways you might run OpenHands on Kubernetes or OpenShift. This guide goes through one possible way:
|
||||
1. Create a PV "as a cluster admin" to map workspace_base data and docker directory to the pod through the worker node
|
||||
2. Create a PVC to be able to mount those PVs to the pod
|
||||
3. Create a pod which contains two containers; the OpenHands and Sandbox containers
|
||||
|
||||
## Detailed Steps for the Example Above
|
||||
|
||||
> Note: Make sure you are logged in to the cluster first with the proper account for each step. PV creation requires cluster administrator!
|
||||
|
||||
> Make sure you have read/write permissions on the hostPath used below (i.e. /tmp/workspace)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# 💻 OpenHands
|
||||
|
||||
OpenHands is an **autonomous AI software engineer** capable of executing complex engineering tasks and collaborating actively with users on software development projects.
|
||||
This project is fully open-source, so you can use and modify it however you like.
|
||||
|
||||
:::tip
|
||||
Explore the codebase of OpenHands on [GitHub](https://github.com/All-Hands-AI/OpenHands) or join one of our communities!
|
||||
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors">
|
||||
<img
|
||||
src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge"
|
||||
alt="Contributors"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/network/members">
|
||||
<img
|
||||
src="https://img.shields.io/github/forks/All-Hands-AI/OpenHands?style=for-the-badge"
|
||||
alt="Forks"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers">
|
||||
<img
|
||||
src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge"
|
||||
alt="Stargazers"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/issues">
|
||||
<img
|
||||
src="https://img.shields.io/github/issues/All-Hands-AI/OpenHands?style=for-the-badge"
|
||||
alt="Issues"
|
||||
/>
|
||||
</a>
|
||||
<br></br>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE">
|
||||
<img
|
||||
src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge"
|
||||
alt="MIT License"
|
||||
/>
|
||||
</a>
|
||||
<br></br>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA">
|
||||
<img
|
||||
src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge"
|
||||
alt="Join our Slack community"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://discord.gg/ESHStjSjD4">
|
||||
<img
|
||||
src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge"
|
||||
alt="Join our Discord community"
|
||||
/>
|
||||
</a>
|
||||
:::
|
||||
|
||||
## 🛠️ Getting Started
|
||||
[Check out the getting started guide on Github](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-getting-started)
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[contributors-url]: https://github.com/All-Hands-AI/OpenHands/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[forks-url]: https://github.com/All-Hands-AI/OpenHands/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[stars-url]: https://github.com/All-Hands-AI/OpenHands/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[issues-url]: https://github.com/All-Hands-AI/OpenHands/issues
|
||||
[license-shield]: https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[license-url]: https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Completion
|
||||
|
||||
OpenHands uses LiteLLM for completion calls. You can find their documentation on Azure [here](https://docs.litellm.ai/docs/providers/azure)
|
||||
OpenHands uses LiteLLM for completion calls. You can find their documentation on Azure [here](https://docs.litellm.ai/docs/providers/azure).
|
||||
|
||||
### Azure openai configs
|
||||
|
||||
@@ -12,7 +12,7 @@ When running the OpenHands Docker image, you'll need to set the following enviro
|
||||
LLM_BASE_URL="<azure-api-base-url>" # e.g. "https://openai-gpt-4-test-v-1.openai.azure.com/"
|
||||
LLM_API_KEY="<azure-api-key>"
|
||||
LLM_MODEL="azure/<your-gpt-deployment-name>"
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
|
||||
```
|
||||
|
||||
Example:
|
||||
@@ -31,15 +31,18 @@ docker run -it \
|
||||
ghcr.io/all-hands-ai/openhands:main
|
||||
```
|
||||
|
||||
You can set the LLM_MODEL and LLM_API_KEY in the OpenHands UI itself.
|
||||
You can also set the model and API key in the OpenHands UI through the Settings.
|
||||
|
||||
:::note
|
||||
You can find your ChatGPT deployment name on the deployments page in Azure. It could be the same with the chat model name (e.g. 'GPT4-1106-preview'), by default or initially set, but it doesn't have to be the same. Run openhands, and when you load it in the browser, go to Settings and set model as above: "azure/<your-actual-gpt-deployment-name>". If it's not in the list, enter your own text and save it.
|
||||
You can find your ChatGPT deployment name on the deployments page in Azure. It could be the same with the chat model
|
||||
name (e.g. 'GPT4-1106-preview'), by default or initially set, but it doesn't have to be the same. Run OpenHands,
|
||||
and when you load it in the browser, go to Settings and set model as above: "azure/<your-actual-gpt-deployment-name>".
|
||||
If it's not in the list, you can open the Settings modal, switch to "Custom Model", and enter your model name.
|
||||
:::
|
||||
|
||||
## Embeddings
|
||||
|
||||
OpenHands uses llama-index for embeddings. You can find their documentation on Azure [here](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/)
|
||||
OpenHands uses llama-index for embeddings. You can find their documentation on Azure [here](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/).
|
||||
|
||||
### Azure openai configs
|
||||
|
||||
@@ -50,6 +53,6 @@ When running OpenHands in Docker, set the following environment variables using
|
||||
|
||||
```
|
||||
LLM_EMBEDDING_MODEL="azureopenai"
|
||||
LLM_EMBEDDING_DEPLOYMENT_NAME="<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
|
||||
LLM_EMBEDDING_DEPLOYMENT_NAME="<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Completion
|
||||
|
||||
OpenHands uses LiteLLM for completion calls. The following resources are relevant for using OpenHands with Google's LLMs
|
||||
OpenHands uses LiteLLM for completion calls. The following resources are relevant for using OpenHands with Google's LLMs:
|
||||
|
||||
- [Gemini - Google AI Studio](https://docs.litellm.ai/docs/providers/gemini)
|
||||
- [VertexAI - Google Cloud Platform](https://docs.litellm.ai/docs/providers/vertex)
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# 🤖 LLM Backends
|
||||
|
||||
OpenHands can connect to many LLMs. However, the recommended models to use are GPT-4 and Claude 3.5.
|
||||
OpenHands can connect to any LLM supported by LiteLLM. However, it requires a powerful model to work.
|
||||
The following are verified by the community to work with OpenHands:
|
||||
|
||||
Current local and open source models are not nearly as powerful. When using an alternative model, you may see long
|
||||
wait times between messages, poor responses, or errors about malformed JSON. OpenHands can only be as powerful as the
|
||||
models driving it.
|
||||
For a full list of the LM providers and models available, please consult the
|
||||
[litellm documentation](https://docs.litellm.ai/docs/providers).
|
||||
* claude-3-5-sonnet
|
||||
* gemini-1.5-pro / gemini-1.5-flash
|
||||
* gpt-4 / gpt-4o
|
||||
* llama-3.1-405b / hermes-3-llama-3.1-405b
|
||||
* wizardlm-2-8x22b
|
||||
|
||||
:::warning
|
||||
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money--be sure to set spending limits and monitor usage.
|
||||
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
|
||||
limits and monitor usage.
|
||||
:::
|
||||
|
||||
If you have successfully run OpenHands with specific LLMs not in the list, please add them to the verified list. We
|
||||
also encourage you to open a PR to share your setup process to help others using the same provider and LLM!
|
||||
|
||||
For a full list of the providers and models available, please consult the
|
||||
[litellm documentation](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
## Local and Open Source Models
|
||||
|
||||
Most current local and open source models are not as powerful. When using such models, you may see long
|
||||
wait times between messages, poor responses, or errors about malformed JSON. OpenHands can only be as powerful as the
|
||||
models driving it. However, if you do find ones that work, please add them to the verified list above.
|
||||
|
||||
## LLM Configuration
|
||||
|
||||
The `LLM_MODEL` environment variable controls which model is used in programmatic interactions.
|
||||
But when using the OpenHands UI, you'll need to choose your model in the settings window.
|
||||
|
||||
The following environment variables might be necessary for some LLMs/providers:
|
||||
|
||||
- `LLM_API_KEY`
|
||||
- `LLM_BASE_URL`
|
||||
- `LLM_EMBEDDING_MODEL`
|
||||
- `LLM_EMBEDDING_DEPLOYMENT_NAME`
|
||||
- `LLM_API_VERSION`
|
||||
- `LLM_DROP_PARAMS`
|
||||
* `LLM_API_KEY`
|
||||
* `LLM_API_VERSION`
|
||||
* `LLM_BASE_URL`
|
||||
* `LLM_EMBEDDING_MODEL`
|
||||
* `LLM_EMBEDDING_DEPLOYMENT_NAME`
|
||||
* `LLM_DROP_PARAMS`
|
||||
* `LLM_DISABLE_VISION`
|
||||
* `LLM_CACHING_PROMPT`
|
||||
|
||||
We have a few guides for running OpenHands with specific model providers:
|
||||
|
||||
- [OpenAI](llms/openai-llms)
|
||||
- [ollama](llms/local-llms)
|
||||
- [Azure](llms/azure-llms)
|
||||
- [Google](llms/google-llms)
|
||||
* [Azure](llms/azure-llms)
|
||||
* [Google](llms/google-llms)
|
||||
* [ollama](llms/local-llms)
|
||||
* [OpenAI](llms/openai-llms)
|
||||
|
||||
If you're using another provider, we encourage you to open a PR to share your setup!
|
||||
|
||||
## API retries and rate limits
|
||||
### 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_NUM_RETRIES` (Default of 5)
|
||||
* `LLM_RETRY_MIN_WAIT` (Default of 3 seconds)
|
||||
* `LLM_RETRY_MAX_WAIT` (Default of 60 seconds)
|
||||
|
||||
* `LLM_NUM_RETRIES` (Default of 8)
|
||||
* `LLM_RETRY_MIN_WAIT` (Default of 15 seconds)
|
||||
* `LLM_RETRY_MAX_WAIT` (Default of 120 seconds)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Local LLM with Ollama
|
||||
|
||||
:::warning
|
||||
When using a Local LLM, OpenHands may have limited functionality.
|
||||
:::
|
||||
|
||||
Ensure that you have the Ollama server up and running.
|
||||
For detailed startup instructions, refer to [here](https://github.com/ollama/ollama)
|
||||
For detailed startup instructions, refer to [here](https://github.com/ollama/ollama).
|
||||
|
||||
This guide assumes you've started ollama with `ollama serve`. If you're running ollama differently (e.g. inside docker), the instructions might need to be modified. Please note that if you're running WSL the default ollama configuration blocks requests from docker containers. See [here](#configuring-ollama-service-wsl-en).
|
||||
|
||||
@@ -28,7 +32,7 @@ starcoder2:latest f67ae0f64584 1.7 GB 19 hours ago
|
||||
|
||||
### Docker
|
||||
|
||||
Use the instructions [here](../intro) to start OpenHands using Docker.
|
||||
Use the instructions [here](../getting-started) to start OpenHands using Docker.
|
||||
But when running `docker run`, you'll need to add a few more arguments:
|
||||
|
||||
```bash
|
||||
@@ -196,9 +200,9 @@ base_url="http://localhost:1234/v1"
|
||||
custom_llm_provider="openai"
|
||||
```
|
||||
|
||||
Done! Now you can start Devin by: `make run` without Docker. You now should be able to connect to `http://localhost:3000/`
|
||||
Done! Now you can start OpenHands by: `make run` without Docker. You now should be able to connect to `http://localhost:3000/`
|
||||
|
||||
# Note:
|
||||
# Note
|
||||
|
||||
For WSL, run the following commands in cmd to set up the networking mode to mirrored:
|
||||
|
||||
|
||||
@@ -4,72 +4,20 @@ OpenHands uses [LiteLLM](https://www.litellm.ai/) to make calls to OpenAI's chat
|
||||
|
||||
## Configuration
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
When running the OpenHands Docker image, you'll need to set the following environment variables:
|
||||
|
||||
```sh
|
||||
LLM_MODEL="openai/<gpt-model-name>" # e.g. "openai/gpt-4o"
|
||||
LLM_API_KEY="<your-openai-project-api-key>"
|
||||
```
|
||||
When running the OpenHands Docker image, you'll need to choose a model and set your API key in the OpenHands UI through the Settings.
|
||||
|
||||
To see a full list of OpenAI models that LiteLLM supports, please visit https://docs.litellm.ai/docs/providers/openai#openai-chat-completion-models.
|
||||
|
||||
To find or create your OpenAI Project API Key, please visit https://platform.openai.com/api-keys.
|
||||
|
||||
**Example**:
|
||||
|
||||
```sh
|
||||
export WORKSPACE_BASE=$(pwd)/workspace
|
||||
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e LLM_MODEL="openai/<gpt-model-name>" \
|
||||
-e LLM_API_KEY="<your-openai-project-api-key>" \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
ghcr.io/opendevin/opendevin:0.8
|
||||
```
|
||||
|
||||
### UI Configuration
|
||||
|
||||
You can also directly set the `LLM_MODEL` and `LLM_API_KEY` in the OpenHands client itself. Follow this guide to get up and running with the OpenHands client.
|
||||
|
||||
From there, you can set your model and API key in the settings window.
|
||||
|
||||
## Using OpenAI-Compatible Endpoints
|
||||
|
||||
Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoints. You can find their full documentation on this topic [here](https://docs.litellm.ai/docs/providers/openai_compatible).
|
||||
|
||||
When running the OpenHands Docker image, you'll need to set the following environment variables:
|
||||
When running the OpenHands Docker image, you'll need to set the following environment variables using `-e`:
|
||||
|
||||
```sh
|
||||
LLM_BASE_URL="<api-base-url>" # e.g. "http://0.0.0.0:3000"
|
||||
LLM_MODEL="openai/<model-name>" # e.g. "openai/mistral"
|
||||
LLM_API_KEY="<your-api-key>"
|
||||
LLM_BASE_URL="<api-base-url>" # e.g. "http://0.0.0.0:3000"
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```sh
|
||||
export WORKSPACE_BASE=$(pwd)/workspace
|
||||
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_BASE_URL="<api-base-url>" \
|
||||
-e LLM_MODEL="openai/<model-name>" \
|
||||
-e LLM_API_KEY="<your-api-key>" \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
ghcr.io/opendevin/opendevin:0.8
|
||||
```
|
||||
Then set your model and API key in the OpenHands UI through the Settings.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@docusaurus/theme-mermaid": "^3.5.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.5.2",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"typescript": "~5.5.4"
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
@@ -12640,9 +12640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prism-react-renderer": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz",
|
||||
"integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.0.tgz",
|
||||
"integrity": "sha512-327BsVCD/unU4CNLZTWVHyUHKnsqcvj2qbPlQ8MiBE2eq2rgctjigPA1Gp9HLF83kZ20zNN6jgizHJeEsyFYOw==",
|
||||
"dependencies": {
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"clsx": "^2.0.0"
|
||||
@@ -14853,9 +14853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@docusaurus/theme-mermaid": "^3.5.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.5.2",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"typescript": "~5.5.4"
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -1,8 +1,79 @@
|
||||
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
|
||||
|
||||
const sidebars: SidebarsConfig = {
|
||||
docsSidebar: [{ type: "autogenerated", dirName: "usage" }],
|
||||
apiSidebar: [require("./modules/python/sidebar.json")],
|
||||
docsSidebar: [{
|
||||
type: 'doc',
|
||||
label: 'Getting Started',
|
||||
id: 'usage/getting-started',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'Troubleshooting',
|
||||
id: 'usage/troubleshooting/troubleshooting',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'Feedback',
|
||||
id: 'usage/feedback',
|
||||
}, {
|
||||
type: 'category',
|
||||
label: 'How-to Guides',
|
||||
items: [{
|
||||
type: 'doc',
|
||||
id: 'usage/how-to/cli-mode',
|
||||
}, {
|
||||
type: 'doc',
|
||||
id: 'usage/how-to/headless-mode',
|
||||
}, {
|
||||
type: 'doc',
|
||||
id: 'usage/how-to/custom-sandbox-guide',
|
||||
}, {
|
||||
type: 'doc',
|
||||
id: 'usage/how-to/evaluation-harness',
|
||||
}, {
|
||||
type: 'doc',
|
||||
id: 'usage/how-to/openshift-example',
|
||||
}]
|
||||
}, {
|
||||
type: 'category',
|
||||
label: 'LLMs',
|
||||
items: [{
|
||||
type: 'doc',
|
||||
label: 'Overview',
|
||||
id: 'usage/llms/llms',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'OpenAI',
|
||||
id: 'usage/llms/openai-llms',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'Azure',
|
||||
id: 'usage/llms/azure-llms',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'Google',
|
||||
id: 'usage/llms/google-llms',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'Local/ollama',
|
||||
id: 'usage/llms/local-llms',
|
||||
}],
|
||||
}, {
|
||||
type: 'category',
|
||||
label: 'Architecture',
|
||||
items: [{
|
||||
type: 'doc',
|
||||
label: 'Backend',
|
||||
id: 'usage/architecture/backend',
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'Runtime',
|
||||
id: 'usage/architecture/runtime',
|
||||
}],
|
||||
}, {
|
||||
type: 'doc',
|
||||
label: 'About',
|
||||
id: 'usage/about',
|
||||
}],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
|
||||
@@ -7,17 +7,6 @@ function CustomFooter() {
|
||||
return (
|
||||
<footer className="custom-footer">
|
||||
<div className="footer-content">
|
||||
<div className="footer-top">
|
||||
<div className="footer-title">
|
||||
<Translate id="footer.title">OpenHands</Translate>
|
||||
</div>
|
||||
<div className="footer-link">
|
||||
<a href="/modules/usage/intro">
|
||||
<Translate id="footer.docs">Docs</Translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-icons">
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA" target="_blank" rel="noopener noreferrer">
|
||||
<FaSlack />
|
||||
@@ -32,7 +21,7 @@ function CustomFooter() {
|
||||
<div className="footer-bottom">
|
||||
<p>
|
||||
<Translate id="footer.copyright" values={{ year: new Date().getFullYear() }}>
|
||||
{'Copyright © {year} OpenHands'}
|
||||
{'Copyright © {year} All Hands AI, Inc'}
|
||||
</Translate>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -17,23 +17,19 @@ export function HomepageHeader() {
|
||||
|
||||
<p className="header-subtitle">{siteConfig.tagline}</p>
|
||||
|
||||
<div className="header-links">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands">
|
||||
<img src="https://img.shields.io/badge/Code-Github-purple?logo=github&logoColor=white&style=for-the-badge" alt="Code" />
|
||||
</a>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA">
|
||||
<img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community" />
|
||||
</a>
|
||||
<a href="https://discord.gg/ESHStjSjD4">
|
||||
<img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community" />
|
||||
</a>
|
||||
|
||||
<a href="https://arxiv.org/abs/2407.16741">
|
||||
<img src="https://img.shields.io/badge/Paper-%20on%20Arxiv-red?logo=arxiv&style=for-the-badge" alt="Paper on Arxiv" />
|
||||
</a>
|
||||
<a href="https://huggingface.co/spaces/OpenDevin/evaluation">
|
||||
<img src="https://img.shields.io/badge/Evaluation-Benchmark%20on%20HF%20Space-green?logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark" />
|
||||
</a>
|
||||
<div align="center" className="header-links">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors" /></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers" /></a>
|
||||
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" /></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License" /></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community" /></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community" /></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits" /></a>
|
||||
<br/>
|
||||
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation" /></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv" /></a>
|
||||
<a href="https://huggingface.co/spaces/OpenHands/evaluation"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score" /></a>
|
||||
</div>
|
||||
|
||||
<Demo />
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
/* You can override the default Infima variables here. */
|
||||
|
||||
:root {
|
||||
--ifm-color-primary: #4465db;
|
||||
--ifm-code-font-size: 95%;
|
||||
--ifm-color-primary: #000;
|
||||
--ifm-background-color: #F1EAE0;
|
||||
--ifm-navbar-background-color: #F1EAE0;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
--secondary: #171717;
|
||||
--secondary-dark: #0a0a0a;
|
||||
@@ -17,21 +19,15 @@
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme="dark"] {
|
||||
--ifm-color-primary: #4465db;
|
||||
--ifm-color-primary: #FFF;
|
||||
--ifm-background-color: #000;
|
||||
--ifm-navbar-background-color: #000;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
--secondary: #737373;
|
||||
--secondary-dark: #171717;
|
||||
--secondary-light: #d4d4d4;
|
||||
--secondary-light: #ccc;
|
||||
}
|
||||
|
||||
.footer--dark {
|
||||
background-image: linear-gradient(
|
||||
140deg,
|
||||
var(--secondary) 20%,
|
||||
var(--secondary-light) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.a {
|
||||
p a, .a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
|
||||
.custom-footer {
|
||||
background-color: dark;
|
||||
color: white;
|
||||
height: 200px;
|
||||
color: #000;
|
||||
height: 100px;
|
||||
/* background: linear-gradient(to bottom, #1a1a1a, #1a1a1a); */
|
||||
background: linear-gradient(to bottom, #1f2937, #000000);
|
||||
background-color: #F1EAE0;
|
||||
|
||||
}
|
||||
|
||||
[data-theme="dark"] .custom-footer {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -47,7 +53,6 @@
|
||||
}
|
||||
|
||||
.footer-community {
|
||||
text-transform: uppercase;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@@ -65,7 +70,3 @@
|
||||
.footer-icons a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
.homepage-header {
|
||||
height: 800px;
|
||||
color: white;
|
||||
background: linear-gradient(to top, #64748b, #000000);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
|
||||
@@ -20,8 +20,7 @@ export default function Home(): JSX.Element {
|
||||
title={`${siteConfig.title}`}
|
||||
description={translate({
|
||||
id: 'homepage.description',
|
||||
message: 'An Open Platform for AI Software Developers as Generalist Agents',
|
||||
description: 'The homepage description',
|
||||
message: 'Code Less, Make More',
|
||||
})}
|
||||
>
|
||||
<HomepageHeader />
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 95 KiB |
@@ -9,12 +9,12 @@ To better organize the evaluation folder, we should follow the rules below:
|
||||
- Each subfolder contains a specific benchmark or experiment. For example, `evaluation/swe_bench` should contain
|
||||
all the preprocessing/evaluation/analysis scripts.
|
||||
- Raw data and experimental records should not be stored within this repo.
|
||||
- For model outputs, they should be stored at [this huggingface space](https://huggingface.co/spaces/OpenDevin/evaluation) for visualization.
|
||||
- For model outputs, they should be stored at [this huggingface space](https://huggingface.co/spaces/OpenHands/evaluation) for visualization.
|
||||
- Important data files of manageable size and analysis scripts (e.g., jupyter notebooks) can be directly uploaded to this repo.
|
||||
|
||||
## Supported Benchmarks
|
||||
|
||||
To learn more about how to integrate your benchmark into OpenHands, check out [tutorial here](https://docs.all-hands.dev/modules/usage/evaluation_harness).
|
||||
To learn more about how to integrate your benchmark into OpenHands, check out [tutorial here](https://docs.all-hands.dev/modules/usage/how-to/evaluation-harness).
|
||||
|
||||
### Software Engineering
|
||||
|
||||
@@ -69,8 +69,8 @@ temperature = 0.0
|
||||
|
||||
### Result Visualization
|
||||
|
||||
Check [this huggingface space](https://huggingface.co/spaces/OpenDevin/evaluation) for visualization of existing experimental results.
|
||||
Check [this huggingface space](https://huggingface.co/spaces/OpenHands/evaluation) for visualization of existing experimental results.
|
||||
|
||||
### Upload your results
|
||||
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenDevin/evaluation) and submit a PR of your evaluation results to our hosted huggingface repo via PR following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenHands/evaluation) and submit a PR of your evaluation results to our hosted huggingface repo via PR following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
|
||||
@@ -26,7 +26,7 @@ poetry run python evaluation/miniwob/get_success_rate.py evaluation/evaluation_o
|
||||
|
||||
## Submit your evaluation results
|
||||
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenDevin/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenHands/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
|
||||
|
||||
## BrowsingAgent V1.0 result
|
||||
|
||||
@@ -19,27 +19,16 @@ Please follow instruction [here](../README.md#setup) to setup your local develop
|
||||
OpenHands now support using the [official evaluation docker](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md) for both **[inference](#run-inference-on-swe-bench-instances) and [evaluation](#evaluate-generated-patches)**.
|
||||
This is now the default behavior.
|
||||
|
||||
### Download Docker Images
|
||||
|
||||
**(Recommended for reproducibility)** If you have extra local space (e.g., 100GB), you can try pull the [instance-level docker images](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level) we've prepared by running:
|
||||
|
||||
```bash
|
||||
evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh instance
|
||||
```
|
||||
|
||||
If you want to save disk space a bit (e.g., with ~50GB free disk space), while speeding up the image pre-build process, you can pull the environment-level docker images:
|
||||
|
||||
```bash
|
||||
evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh env
|
||||
```
|
||||
|
||||
## Run Inference on SWE-Bench Instances
|
||||
|
||||
Make sure your Docker daemon is running, and you have pulled the [instance-level docker image](#openhands-swe-bench-instance-level-docker-support).
|
||||
Make sure your Docker daemon is running, and you have ample disk space (at least 200-500GB, depends on the SWE-Bench set you are running on) for the [instance-level docker image](#openhands-swe-bench-instance-level-docker-support).
|
||||
|
||||
When the `run_infer.sh` script is started, it will automatically pull the relavant SWE-Bench images. For example, for instance ID `django_django-11011`, it will try to pull our pre-build docker image `sweb.eval.x86_64.django_s_django-11011` from DockerHub. This image will be used create an OpenHands runtime image where the agent will operate on.
|
||||
|
||||
```bash
|
||||
./evaluation/swe_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers]
|
||||
# e.g., ./evaluation/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 300
|
||||
./evaluation/swe_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split]
|
||||
# e.g., ./evaluation/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 300 30 1 princeton-nlp/SWE-bench_Lite test
|
||||
```
|
||||
|
||||
where `model_config` is mandatory, and the rest are optional.
|
||||
@@ -57,6 +46,8 @@ in order to use `eval_limit`, you must also set `agent`.
|
||||
default, it is set to 30.
|
||||
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
|
||||
default, it is set to 1.
|
||||
- `dataset`, a huggingface dataset name. e.g. `princeton-nlp/SWE-bench` or `princeton-nlp/SWE-bench_Lite`, specifies which dataset to evaluate on.
|
||||
- `dataset_split`, split for the huggingface dataset. e.g., `test`, `dev`. Default to `test`.
|
||||
|
||||
There are also two optional environment variables you can set.
|
||||
```
|
||||
@@ -72,11 +63,22 @@ then your command would be:
|
||||
./evaluation/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 10
|
||||
```
|
||||
|
||||
**Evaluate on `RemoteRuntime` (alpha)** (contact Xingyao over slack if you want to try this out!)
|
||||
### Run Inference on `RemoteRuntime`
|
||||
|
||||
This is in limited beta. Contact Xingyao over slack if you want to try this out!
|
||||
|
||||
```bash
|
||||
SANDBOX_API_KEY="CONTACT-XINGYAO-TO-GET-A-TESTING-API-KEY" RUNTIME=remote EVAL_DOCKER_IMAGE_PREFIX="us-docker.pkg.dev/evaluation-428620/swe-bench-images" ./evaluation/swe_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 300
|
||||
# ./evaluation/swe_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split]
|
||||
ALLHANDS_API_KEY="YOUR-API-KEY" RUNTIME=remote EVAL_DOCKER_IMAGE_PREFIX="us-docker.pkg.dev/evaluation-428620/swe-bench-images" \
|
||||
./evaluation/swe_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 300 30 16 "princeton-nlp/SWE-bench_Lite" test
|
||||
# This example runs evaluation on CodeActAgent for 300 instances on "princeton-nlp/SWE-bench_Lite"'s test set, with max 30 iteration per instances, with 16 number of workers running in parallel
|
||||
```
|
||||
|
||||
To clean-up all existing runtime you've already started, run:
|
||||
|
||||
```bash
|
||||
ALLHANDS_API_KEY="YOUR-API-KEY" ./evaluation/swe_bench/scripts/cleanup_remote_runtime.sh
|
||||
```
|
||||
Multi-processing is still WIP.
|
||||
|
||||
### Specify a subset of tasks to run infer
|
||||
|
||||
@@ -95,13 +97,35 @@ After running the inference, you will obtain a `output.jsonl` (by default it wil
|
||||
|
||||
## Evaluate Generated Patches
|
||||
|
||||
### Download Docker Images
|
||||
|
||||
**(Recommended for reproducibility)** If you have extra local space (e.g., 200GB), you can try pull the [instance-level docker images](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level) we've prepared by running:
|
||||
|
||||
```bash
|
||||
evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh instance
|
||||
```
|
||||
|
||||
If you want to save disk space a bit (e.g., with ~50GB free disk space), while speeding up the image pre-build process, you can pull the environment-level docker images:
|
||||
|
||||
```bash
|
||||
evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh env
|
||||
```
|
||||
|
||||
If you want to evaluate on the full SWE-Bench test set:
|
||||
|
||||
```bash
|
||||
evaluation/swe_bench/scripts/docker/pull_all_eval_docker.sh instance full
|
||||
```
|
||||
|
||||
### Run evaluation
|
||||
|
||||
With `output.jsonl` file, you can run `eval_infer.sh` to evaluate generated patches, and produce a fine-grained report.
|
||||
|
||||
**This evaluation is performed using the official dockerized evaluation announced [here](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md).**
|
||||
|
||||
> If you want to evaluate existing results, you should first run this to clone existing outputs
|
||||
>```bash
|
||||
>git clone https://huggingface.co/spaces/OpenDevin/evaluation evaluation/evaluation_outputs
|
||||
>git clone https://huggingface.co/spaces/OpenHands/evaluation evaluation/evaluation_outputs
|
||||
>```
|
||||
|
||||
NOTE, you should have already pulled the instance-level OR env-level docker images following [this section](#openhands-swe-bench-instance-level-docker-support).
|
||||
@@ -135,10 +159,10 @@ The final results will be saved to `evaluation/evaluation_outputs/outputs/swe_be
|
||||
|
||||
## Visualize Results
|
||||
|
||||
First you need to clone `https://huggingface.co/spaces/OpenDevin/evaluation` and add your own running results from openhands into the `outputs` of the cloned repo.
|
||||
First you need to clone `https://huggingface.co/spaces/OpenHands/evaluation` and add your own running results from openhands into the `outputs` of the cloned repo.
|
||||
|
||||
```bash
|
||||
git clone https://huggingface.co/spaces/OpenDevin/evaluation
|
||||
git clone https://huggingface.co/spaces/OpenHands/evaluation
|
||||
```
|
||||
|
||||
**(optional) setup streamlit environment with conda**:
|
||||
@@ -162,4 +186,4 @@ Then you can access the SWE-Bench trajectory visualizer at `localhost:8501`.
|
||||
|
||||
## Submit your evaluation results
|
||||
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenDevin/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenHands/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
|
||||
@@ -25,8 +25,7 @@ from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
load_from_env,
|
||||
parse_arguments,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
@@ -109,6 +108,11 @@ def get_config(
|
||||
if USE_INSTANCE_IMAGE:
|
||||
# We use a different instance image for the each instance of swe-bench eval
|
||||
base_container_image = get_instance_docker_image(instance['instance_id'])
|
||||
logger.info(
|
||||
f'Using instance container image: {base_container_image}. '
|
||||
f'Please make sure this image exists. '
|
||||
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
|
||||
)
|
||||
else:
|
||||
base_container_image = SWE_BENCH_CONTAINER_IMAGE
|
||||
logger.info(f'Using swe-bench container image: {base_container_image}')
|
||||
@@ -118,26 +122,19 @@ def get_config(
|
||||
run_as_openhands=False,
|
||||
max_budget_per_task=4,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'eventstream'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
),
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
selected_env_vars = {'runtime', 'sandbox_api_key'}
|
||||
selected_env_vars = {
|
||||
k: v for k, v in os.environ.items() if k.lower() in selected_env_vars
|
||||
}
|
||||
if selected_env_vars:
|
||||
logger.info(
|
||||
f'Loading config keys from env vars: {list(selected_env_vars.keys())}'
|
||||
)
|
||||
load_from_env(config, selected_env_vars)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
return config
|
||||
|
||||
@@ -160,12 +157,14 @@ def initialize_runtime(
|
||||
action = CmdRunAction(
|
||||
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc"""
|
||||
)
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -204,18 +203,21 @@ def initialize_runtime(
|
||||
'/swe_util/',
|
||||
)
|
||||
action = CmdRunAction(command='cat ~/.bashrc')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='source ~/.bashrc')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='source /swe_util/instance_swe_entry.sh')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -237,6 +239,7 @@ def initialize_runtime(
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='git reset --hard')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -245,6 +248,7 @@ def initialize_runtime(
|
||||
action = CmdRunAction(
|
||||
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
|
||||
)
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -272,18 +276,21 @@ def complete_runtime(
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='git config --global core.pager ""')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='git add -A')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -411,12 +418,26 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_arguments()
|
||||
parser = get_parser()
|
||||
parser.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
default='princeton-nlp/SWE-bench',
|
||||
help='data set to evaluate on, either full-test or lite-test',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--split',
|
||||
type=str,
|
||||
default='test',
|
||||
help='split to evaluate on',
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
|
||||
# so we don't need to manage file uploading to OpenHands's repo
|
||||
dataset = load_dataset('princeton-nlp/SWE-bench_Lite')
|
||||
swe_bench_tests = filter_dataset(dataset['test'].to_pandas(), 'instance_id')
|
||||
dataset = load_dataset(args.dataset, split=args.split)
|
||||
logger.info(f'Loaded dataset {args.dataset} with split {args.split}')
|
||||
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
@@ -445,6 +466,12 @@ if __name__ == '__main__':
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
|
||||
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
run_evaluation(
|
||||
instances, metadata, output_file, args.eval_num_workers, process_instance
|
||||
)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# API base URL
|
||||
BASE_URL="https://api.all-hands.dev/v0"
|
||||
|
||||
# Get the list of runtimes
|
||||
runtimes=$(curl --silent --location --request GET "${BASE_URL}/runtime/list" \
|
||||
--header "X-API-Key: ${ALLHANDS_API_KEY}" | jq -r '.runtimes | .[].runtime_id')
|
||||
|
||||
# Loop through each runtime and stop it
|
||||
for runtime_id in $runtimes; do
|
||||
echo "Stopping runtime: ${runtime_id}"
|
||||
curl --silent --location --request POST "${BASE_URL}/runtime/stop" \
|
||||
--header "X-API-Key: ${ALLHANDS_API_KEY}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data-raw "{\"runtime_id\": \"${runtime_id}\"}"
|
||||
echo
|
||||
done
|
||||
|
||||
echo "All runtimes have been stopped."
|
||||
@@ -6,19 +6,33 @@ LEVEL=$1
|
||||
# - base, keyword "sweb.base"
|
||||
# - env, keyword "sweb.env"
|
||||
# - instance, keyword "sweb.eval"
|
||||
SET=$2
|
||||
|
||||
if [ -z "$LEVEL" ]; then
|
||||
echo "Usage: $0 <cache_level>"
|
||||
echo "Usage: $0 <cache_level> <set>"
|
||||
echo "cache_level: base, env, or instance"
|
||||
echo "set: lite, full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SET" ]; then
|
||||
echo "Usage: $0 <cache_level> <set>"
|
||||
echo "cache_level: base, env, or instance"
|
||||
echo "set: lite, full, default is lite"
|
||||
SET="lite"
|
||||
fi
|
||||
|
||||
NAMESPACE=$2 # xingyaoww
|
||||
if [ -z "$NAMESPACE" ]; then
|
||||
echo "Default to namespace: xingyaoww"
|
||||
NAMESPACE="xingyaoww"
|
||||
fi
|
||||
IMAGE_FILE="$(dirname "$0")/all-swebench-lite-instance-images.txt"
|
||||
|
||||
if [ "$SET" == "lite" ]; then
|
||||
IMAGE_FILE="$(dirname "$0")/all-swebench-lite-instance-images.txt"
|
||||
else
|
||||
IMAGE_FILE="$(dirname "$0")/all-swebench-full-instance-images.txt"
|
||||
fi
|
||||
|
||||
# Define a pattern based on the level
|
||||
case $LEVEL in
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""You should first perform the following steps:
|
||||
|
||||
1. Build the docker images. Install SWE-Bench first (https://github.com/princeton-nlp/SWE-bench). Then run:
|
||||
```bash
|
||||
export DATASET_NAME=princeton-nlp/SWE-bench_Lite
|
||||
export SPLIT=test
|
||||
export MAX_WORKERS=4
|
||||
export RUN_ID=some-random-ID
|
||||
python -m swebench.harness.run_evaluation \
|
||||
--dataset_name $DATASET_NAME \
|
||||
--split $SPLIT \
|
||||
--predictions_path gold \
|
||||
--max_workers $MAX_WORKERS \
|
||||
--run_id $RUN_ID \
|
||||
--cache_level instance
|
||||
```
|
||||
|
||||
2. Then run this script to push the docker images to the docker hub. Some of the docker images might fail to build in the previous step - start an issue in the SWE-Bench repo for possible fixes.
|
||||
|
||||
To push the docker images for "princeton-nlp/SWE-bench_Lite" test set to the docker hub (e.g., under `docker.io/xingyaoww/`), run:
|
||||
```bash
|
||||
EVAL_DOCKER_IMAGE_PREFIX='docker.io/xingyaoww/' python3 evaluation/swe_bench/scripts/docker/push_docker_instance_images.py --dataset princeton-nlp/SWE-bench_Lite --split test
|
||||
```
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import docker
|
||||
from datasets import load_dataset
|
||||
from tqdm import tqdm
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
logger.setLevel('ERROR')
|
||||
from evaluation.swe_bench.run_infer import get_instance_docker_image # noqa
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dataset', type=str, default='princeton-nlp/SWE-bench_Lite')
|
||||
parser.add_argument('--split', type=str, default='test')
|
||||
args = parser.parse_args()
|
||||
|
||||
dataset = load_dataset(args.dataset, split=args.split)
|
||||
client = docker.from_env()
|
||||
|
||||
pbar = tqdm(total=len(dataset))
|
||||
counter = {'success': 0, 'failed': 0}
|
||||
|
||||
failed_instances = []
|
||||
for instance in dataset:
|
||||
instance_id = instance['instance_id']
|
||||
image_name = f'sweb.eval.x86_64.{instance_id}'
|
||||
target_image_name = get_instance_docker_image(instance_id)
|
||||
|
||||
print('-' * 100)
|
||||
# check if image exists
|
||||
try:
|
||||
image: docker.models.images.Image = client.images.get(image_name)
|
||||
image.tag(target_image_name)
|
||||
print(f'Image {image_name} -- tagging to --> {target_image_name}')
|
||||
ret_push = client.images.push(target_image_name)
|
||||
if isinstance(ret_push, str):
|
||||
print(ret_push)
|
||||
else:
|
||||
for line in ret_push:
|
||||
print(line)
|
||||
print(f'Image {image_name} -- pushed to --> {target_image_name}')
|
||||
counter['success'] += 1
|
||||
except docker.errors.ImageNotFound:
|
||||
print(f'ERROR: Image {image_name} does not exist')
|
||||
counter['failed'] += 1
|
||||
failed_instances.append(instance_id)
|
||||
finally:
|
||||
pbar.update(1)
|
||||
pbar.set_postfix(counter)
|
||||
|
||||
print(f'Success: {counter["success"]}, Failed: {counter["failed"]}')
|
||||
print('Failed instances IDs:')
|
||||
for failed_instance in failed_instances:
|
||||
print(failed_instance)
|
||||
@@ -9,6 +9,8 @@ AGENT=$3
|
||||
EVAL_LIMIT=$4
|
||||
MAX_ITER=$5
|
||||
NUM_WORKERS=$6
|
||||
DATASET=$7
|
||||
SPLIT=$8
|
||||
|
||||
if [ -z "$NUM_WORKERS" ]; then
|
||||
NUM_WORKERS=1
|
||||
@@ -31,6 +33,17 @@ if [ -z "$USE_INSTANCE_IMAGE" ]; then
|
||||
USE_INSTANCE_IMAGE=true
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$DATASET" ]; then
|
||||
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
|
||||
DATASET="princeton-nlp/SWE-bench_Lite"
|
||||
fi
|
||||
|
||||
if [ -z "$SPLIT" ]; then
|
||||
echo "SPLIT not specified, use default test"
|
||||
SPLIT="test"
|
||||
fi
|
||||
|
||||
export USE_INSTANCE_IMAGE=$USE_INSTANCE_IMAGE
|
||||
echo "USE_INSTANCE_IMAGE: $USE_INSTANCE_IMAGE"
|
||||
|
||||
@@ -39,6 +52,8 @@ get_agent_version
|
||||
echo "AGENT: $AGENT"
|
||||
echo "AGENT_VERSION: $AGENT_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "DATASET: $DATASET"
|
||||
echo "SPLIT: $SPLIT"
|
||||
|
||||
# Default to NOT use Hint
|
||||
if [ -z "$USE_HINT_TEXT" ]; then
|
||||
@@ -59,7 +74,9 @@ COMMAND="poetry run python evaluation/swe_bench/run_infer.py \
|
||||
--max-iterations $MAX_ITER \
|
||||
--max-chars 10000000 \
|
||||
--eval-num-workers $NUM_WORKERS \
|
||||
--eval-note $EVAL_NOTE"
|
||||
--eval-note $EVAL_NOTE \
|
||||
--dataset $DATASET \
|
||||
--split $SPLIT"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
|
||||
@@ -37,7 +37,7 @@ poetry run python evaluation/webarena/get_success_rate.py evaluation/evaluation_
|
||||
|
||||
## Submit your evaluation results
|
||||
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenDevin/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenHands/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
|
||||
|
||||
## BrowsingAgent V1.0 result
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nextui-org/react": "^2.4.6",
|
||||
@@ -17,7 +17,7 @@
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jose": "^5.8.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"vite": "^5.4.2",
|
||||
"vite": "^5.4.4",
|
||||
"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.5.1",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -59,13 +59,13 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"husky": "^9.1.5",
|
||||
"husky": "^9.1.6",
|
||||
"jsdom": "^25.0.0",
|
||||
"lint-staged": "^15.2.9",
|
||||
"postcss": "^8.4.41",
|
||||
"lint-staged": "^15.2.10",
|
||||
"postcss": "^8.4.45",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.4",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.6.2",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
@@ -4857,9 +4857,9 @@
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz",
|
||||
"integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==",
|
||||
"version": "22.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@@ -4871,9 +4871,9 @@
|
||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz",
|
||||
"integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==",
|
||||
"version": "18.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
|
||||
"integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -8118,9 +8118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.5",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz",
|
||||
"integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==",
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
|
||||
"integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
@@ -8133,9 +8133,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "23.14.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.14.0.tgz",
|
||||
"integrity": "sha512-Y5GL4OdA8IU2geRrt2+Uc1iIhsjICdHZzT9tNwQ3TVqdNzgxHToGCKf/TPRP80vTCAP6svg2WbbJL+Gx5MFQVA==",
|
||||
"version": "23.15.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.1.tgz",
|
||||
"integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -9020,9 +9020,9 @@
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
|
||||
},
|
||||
"node_modules/lint-staged": {
|
||||
"version": "15.2.9",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.9.tgz",
|
||||
"integrity": "sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==",
|
||||
"version": "15.2.10",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz",
|
||||
"integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "~5.3.0",
|
||||
@@ -9031,7 +9031,7 @@
|
||||
"execa": "~8.0.1",
|
||||
"lilconfig": "~3.1.2",
|
||||
"listr2": "~8.2.4",
|
||||
"micromatch": "~4.0.7",
|
||||
"micromatch": "~4.0.8",
|
||||
"pidtree": "~0.6.0",
|
||||
"string-argv": "~0.3.2",
|
||||
"yaml": "~2.5.0"
|
||||
@@ -10812,9 +10812,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"version": "8.4.45",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
|
||||
"integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -12423,9 +12423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
||||
"version": "3.4.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
|
||||
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -12800,9 +12800,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -13109,12 +13109,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
||||
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.4.tgz",
|
||||
"integrity": "sha512-RHFCkULitycHVTtelJ6jQLd+KSAAzOgEYorV32R2q++M6COBjKJR6BxqClwp5sf0XaBDjVMuJ9wnNfyAJwjMkA==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -16,7 +16,7 @@
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jose": "^5.8.0",
|
||||
@@ -32,7 +32,7 @@
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"vite": "^5.4.2",
|
||||
"vite": "^5.4.4",
|
||||
"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.5.1",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -82,13 +82,13 @@
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"husky": "^9.1.5",
|
||||
"husky": "^9.1.6",
|
||||
"jsdom": "^25.0.0",
|
||||
"lint-staged": "^15.2.9",
|
||||
"postcss": "^8.4.41",
|
||||
"lint-staged": "^15.2.10",
|
||||
"postcss": "^8.4.45",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.4",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.6.2",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 754 B After Width: | Height: | Size: 803 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,472 +1,32 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M5195 8749 c-122 -19 -282 -101 -383 -197 -66 -61 -59 -81 52 -160
|
||||
49 -34 99 -65 111 -68 16 -4 44 9 94 41 93 60 169 94 256 112 l70 14 82 -42
|
||||
c46 -23 83 -44 83 -48 -1 -3 -34 -21 -74 -40 -217 -98 -499 -312 -667 -505
|
||||
-138 -157 -309 -430 -309 -493 0 -38 66 -100 149 -140 42 -20 77 -38 79 -39 2
|
||||
-1 -17 -32 -43 -70 -185 -277 -278 -669 -246 -1041 20 -245 61 -392 167 -612
|
||||
35 -74 63 -135 62 -135 -2 -1 -30 -10 -62 -21 -38 -13 -67 -30 -79 -47 -19
|
||||
-26 -19 -29 -3 -71 23 -61 101 -184 166 -262 99 -119 265 -247 425 -327 149
|
||||
-75 397 -152 602 -188 l72 -13 -63 -67 c-139 -147 -205 -389 -139 -503 11 -18
|
||||
40 -43 70 -58 50 -26 51 -28 33 -44 -11 -10 -40 -58 -66 -109 -77 -149 -235
|
||||
-389 -267 -407 -17 -10 -269 -65 -352 -78 -38 -6 -115 -15 -169 -20 -74 -8
|
||||
-107 -15 -128 -31 -21 -16 -47 -21 -110 -24 -61 -3 -121 -15 -223 -46 -77 -23
|
||||
-153 -49 -168 -57 -16 -8 -52 -51 -84 -101 -31 -48 -69 -103 -85 -122 -54 -66
|
||||
-31 -122 68 -159 47 -18 48 -19 18 -25 -49 -10 -352 -44 -694 -76 -314 -30
|
||||
-326 -32 -347 -55 -19 -22 -31 -25 -85 -25 -91 0 -104 -12 -112 -103 -8 -94 3
|
||||
-121 58 -142 23 -8 174 -56 336 -106 162 -50 445 -137 628 -195 183 -57 482
|
||||
-149 665 -205 182 -55 367 -111 410 -125 43 -13 102 -24 130 -24 97 0 1501
|
||||
180 1564 201 46 15 77 70 77 135 0 82 -43 130 -126 141 -24 4 -59 16 -78 29
|
||||
-22 14 -129 46 -280 83 -135 34 -255 64 -268 67 -20 6 -16 10 30 31 29 13 63
|
||||
34 75 45 22 21 24 21 122 5 124 -21 158 -21 217 -1 46 15 48 15 108 -13 64
|
||||
-29 144 -37 231 -21 22 4 29 4 16 0 -24 -7 -23 -8 15 -19 68 -20 155 -35 169
|
||||
-29 8 3 15 0 17 -6 3 -7 10 -8 17 -4 11 7 11 10 0 17 -10 6 -8 9 9 9 14 0 21
|
||||
-4 16 -11 -4 -7 0 -9 14 -5 12 4 128 -13 259 -38 131 -25 362 -67 513 -95 815
|
||||
-146 759 -135 707 -143 -27 -3 -51 -9 -54 -11 -2 -3 38 -12 89 -21 51 -10 125
|
||||
-23 163 -31 98 -19 336 -53 420 -60 68 -5 69 -5 30 8 -22 7 -73 19 -114 27
|
||||
-41 8 -70 15 -64 17 11 4 120 -12 607 -92 154 -25 281 -43 284 -40 8 7 -16 13
|
||||
-153 39 -407 77 -958 186 -963 190 -8 8 15 135 48 268 46 182 103 350 221 643
|
||||
144 360 219 605 288 936 69 337 104 670 99 964 -3 232 -6 241 -89 282 -50 25
|
||||
-234 64 -414 88 -66 9 -134 18 -151 21 l-31 4 57 60 c233 242 426 579 505 884
|
||||
14 55 30 105 35 112 23 27 71 180 78 247 4 40 3 110 -1 157 -11 105 -11 95 -2
|
||||
210 5 73 3 111 -9 164 -31 131 -107 254 -204 328 -26 20 -42 40 -42 54 0 12
|
||||
-27 74 -59 137 -204 399 -520 697 -963 912 -375 181 -795 278 -1268 292 -338
|
||||
10 -691 -35 -972 -123 l-77 -25 -233 109 c-295 138 -320 146 -413 131z m107
|
||||
-89 c20 -6 65 -24 100 -40 l63 -30 -65 6 c-68 6 -173 -10 -260 -41 -52 -18
|
||||
-72 -32 -87 -59 -10 -18 -14 -14 -37 32 l-26 52 47 23 c68 34 154 60 213 66 8
|
||||
0 32 -4 52 -9z m1843 -110 c448 -52 824 -167 1126 -343 307 -179 534 -393 691
|
||||
-652 63 -102 101 -175 96 -181 -2 -2 -26 12 -52 31 -27 19 -64 39 -82 45 -19
|
||||
7 -34 15 -34 18 -1 31 -32 148 -48 178 -32 63 -65 88 -105 80 -29 -5 -35 -3
|
||||
-48 20 -8 15 -44 52 -81 83 -178 150 -544 337 -968 495 -129 48 -139 50 -235
|
||||
50 -130 -1 -217 -23 -378 -96 -86 -40 -93 -59 -44 -126 41 -55 141 -152 157
|
||||
-152 6 0 34 12 63 26 100 51 255 94 338 94 51 0 160 -42 324 -126 173 -88 320
|
||||
-176 394 -236 l61 -49 -35 -30 c-19 -17 -38 -42 -41 -54 -3 -14 -16 -26 -32
|
||||
-30 -63 -16 -56 -59 31 -192 l73 -110 -39 -44 c-53 -61 -138 -191 -180 -274
|
||||
-64 -130 -120 -339 -134 -502 l-6 -72 -71 -11 c-39 -6 -87 -13 -107 -16 l-36
|
||||
-5 -13 73 c-56 317 -249 568 -557 726 -254 130 -574 200 -1108 244 -339 27
|
||||
-1062 15 -1363 -23 -46 -6 -85 -9 -88 -7 -6 7 63 131 124 220 365 540 1013
|
||||
885 1797 958 120 11 484 5 610 -10z m-2169 -15 c20 -30 11 -81 -15 -88 -18 -4
|
||||
-81 28 -81 41 0 6 70 70 77 72 2 0 10 -11 19 -25z m2662 -299 c67 -27 122 -52
|
||||
122 -57 0 -12 -22 -7 -35 7 -5 6 -26 14 -45 17 -19 3 -44 9 -54 13 -14 5 -17
|
||||
4 -12 -4 4 -7 2 -12 -3 -12 -6 0 -11 5 -11 11 0 6 -7 9 -15 6 -8 -4 -17 -2
|
||||
-20 3 -3 5 -12 7 -20 4 -8 -4 -29 -1 -46 5 -24 8 -36 8 -45 0 -11 -9 -14 -8
|
||||
-14 2 0 11 -2 11 -9 0 -6 -10 -30 -16 -68 -17 -32 -2 -58 -4 -58 -4 0 -1 -8
|
||||
-3 -17 -5 -9 -2 -19 4 -22 12 -9 21 -29 4 -22 -19 2 -12 -1 -27 -9 -34 -18
|
||||
-18 -25 -18 -25 1 0 9 -7 30 -16 46 -8 17 -13 34 -10 37 21 20 149 42 236 39
|
||||
87 -2 105 -6 218 -51z m-478 -25 c17 -32 12 -61 -10 -61 -10 0 -23 -8 -29 -17
|
||||
-9 -16 -12 -15 -40 11 -17 17 -31 33 -31 37 0 7 77 47 92 49 4 0 12 -9 18 -19z
|
||||
m710 -76 c10 6 102 -34 112 -49 4 -6 8 -8 8 -4 0 4 13 2 30 -3 16 -6 28 -14
|
||||
25 -19 -2 -4 8 -13 23 -20 22 -11 24 -14 9 -18 -21 -4 -47 14 -47 34 0 14 -73
|
||||
14 -101 0 -8 -4 -5 -1 5 8 17 14 17 16 3 22 -9 3 -15 10 -12 14 3 5 -5 5 -17
|
||||
2 -19 -5 -20 -7 -5 -10 20 -4 22 -17 5 -28 -7 -5 -8 -3 -3 6 6 10 4 12 -8 7
|
||||
-9 -3 -23 1 -32 9 -8 8 -20 12 -26 8 -8 -4 -9 -3 -5 5 4 6 16 9 27 6 13 -4 19
|
||||
-2 19 9 0 9 -5 15 -11 14 -6 -2 -13 3 -14 10 -2 7 -1 8 2 2 2 -5 8 -8 13 -5z
|
||||
m384 -187 c-3 -4 -9 -8 -15 -8 -5 0 -9 4 -9 8 0 5 7 9 15 9 8 0 12 -4 9 -9z
|
||||
m241 -144 c39 -28 83 -65 99 -82 l29 -31 -25 -6 c-40 -10 -61 -2 -79 30 -9 17
|
||||
-37 51 -63 77 -25 26 -46 53 -46 58 0 6 -5 8 -12 4 -7 -4 -8 -3 -4 5 9 13 11
|
||||
12 101 -55z m-61 -152 c7 -12 -101 -68 -111 -59 -11 12 58 66 84 67 12 0 24
|
||||
-4 27 -8z m348 -64 l18 -48 -73 0 c-85 0 -150 -16 -246 -60 -68 -32 -95 -33
|
||||
-84 -5 12 33 204 102 306 112 37 3 64 9 60 14 -10 9 -157 -14 -225 -37 -29 -9
|
||||
-82 -32 -117 -50 -74 -39 -96 -41 -131 -13 l-25 21 36 -5 c25 -4 54 3 95 20
|
||||
126 53 162 65 244 83 116 26 121 24 142 -32z m-24 -113 c-2 -3 -29 -9 -59 -15
|
||||
-30 -6 -97 -31 -149 -56 -93 -45 -94 -45 -66 -15 32 34 111 68 182 80 59 10
|
||||
99 13 92 6z m317 -219 c113 -53 219 -169 265 -292 18 -51 19 -59 6 -85 -8 -17
|
||||
-10 -31 -5 -35 5 -3 13 5 18 18 8 19 9 16 9 -19 0 -22 3 -46 6 -52 4 -6 5 -11
|
||||
3 -11 -2 0 -16 17 -30 39 -20 30 -23 41 -14 51 9 10 9 11 -3 8 -9 -3 -35 14
|
||||
-63 39 -46 43 -156 103 -188 103 -9 0 -19 10 -23 23 -8 32 -60 144 -91 195
|
||||
l-26 42 43 0 c24 0 65 -11 93 -24z m-250 -76 c41 -67 79 -166 70 -181 -4 -5
|
||||
-12 -9 -18 -9 -31 0 -158 -72 -208 -118 -43 -39 -68 -74 -103 -144 -95 -192
|
||||
-101 -405 -16 -586 18 -38 27 -69 22 -74 -16 -16 -88 -11 -134 10 -86 37 -172
|
||||
152 -184 245 -4 26 -11 47 -17 47 -17 0 1 160 28 256 51 176 158 351 286 468
|
||||
62 56 201 146 226 146 6 0 28 -27 48 -60z m-295 2 c0 -5 -34 -43 -75 -86 -41
|
||||
-42 -96 -107 -122 -144 -39 -54 -49 -63 -51 -45 -10 67 166 283 230 283 10 0
|
||||
18 -4 18 -8z m-3625 -38 c-42 -65 -90 -153 -129 -237 -35 -76 -41 -97 -34
|
||||
-122 13 -48 0 -84 -38 -107 -19 -11 -34 -21 -34 -22 0 -1 -9 -42 -19 -91 -36
|
||||
-162 -47 -318 -41 -525 3 -69 5 -55 11 75 11 230 41 363 78 341 5 -3 12 2 15
|
||||
11 6 14 19 15 109 9 91 -6 142 -11 142 -16 0 -1 10 -3 22 -6 18 -3 27 4 44 37
|
||||
11 23 22 39 24 37 2 -2 26 -69 53 -148 28 -80 73 -201 101 -270 62 -148 61
|
||||
-145 67 -240 7 -93 -16 -250 -45 -310 -13 -26 -40 -59 -67 -79 -74 -56 -104
|
||||
-68 -156 -64 -27 2 -48 -1 -48 -6 0 -5 -8 -11 -17 -13 -16 -3 -15 -7 8 -29 14
|
||||
-14 32 -29 40 -32 12 -5 12 -7 -1 -16 -10 -7 -17 -6 -22 2 -6 9 -8 9 -8 -1 0
|
||||
-10 -5 -10 -22 -1 -20 11 -22 11 -10 -3 10 -13 10 -18 1 -24 -8 -4 -9 -3 -5 4
|
||||
6 11 2 13 -19 12 -3 0 -26 11 -50 26 -27 16 -54 24 -66 21 -18 -5 -28 10 -79
|
||||
116 -68 142 -111 268 -142 411 -18 85 -22 134 -22 311 0 218 13 336 54 466 11
|
||||
37 15 75 12 112 -5 67 14 107 52 107 20 0 31 13 62 73 42 81 75 136 112 185
|
||||
18 23 32 32 54 32 l30 0 -17 -26z m1046 16 c27 -15 24 -24 -18 -71 -49 -54
|
||||
-89 -78 -198 -119 -195 -72 -303 -179 -415 -411 -22 -46 -40 -78 -40 -72 0 9
|
||||
-82 51 -115 58 -29 6 -46 19 -41 31 3 7 0 16 -6 20 -7 4 -8 3 -4 -4 7 -12 -6
|
||||
-17 -17 -6 -18 18 122 230 222 336 113 121 235 201 352 232 63 17 251 21 280
|
||||
6z m3039 -130 c0 -5 -8 -10 -17 -10 -15 0 -16 2 -3 10 19 12 20 12 20 0z
|
||||
m-823 -221 c-35 -74 -69 -150 -77 -169 -21 -55 -30 -58 -30 -9 0 100 54 249
|
||||
113 312 25 27 30 28 43 16 13 -13 7 -31 -49 -150z m952 138 c24 -12 59 -90 76
|
||||
-166 l7 -34 -38 31 c-54 44 -114 62 -200 62 l-74 0 0 49 c0 48 1 49 38 59 53
|
||||
14 163 14 191 -1z m-63 -219 c51 -45 74 -99 74 -175 0 -36 -4 -63 -10 -63 -5
|
||||
0 -10 -9 -10 -19 0 -10 -6 -21 -14 -24 -11 -4 -13 -13 -7 -34 8 -28 -5 -73
|
||||
-21 -73 -4 0 -7 -6 -5 -12 1 -7 -3 -12 -9 -11 -5 1 -16 -7 -23 -19 -10 -16
|
||||
-10 -19 1 -12 10 6 10 4 -1 -9 -7 -9 -17 -14 -22 -12 -4 3 -15 -5 -24 -19 -29
|
||||
-44 -108 -65 -185 -50 -70 14 -121 159 -99 280 29 159 148 283 271 284 39 0
|
||||
54 -5 84 -32z m-961 -224 c0 -22 -5 -29 -20 -29 -24 0 -34 43 -13 56 21 14 33
|
||||
4 33 -27z m-1195 -115 c22 -7 40 -16 40 -21 0 -4 6 -8 14 -8 8 0 33 -16 55
|
||||
-34 71 -59 157 -204 180 -301 14 -60 19 -272 8 -335 -13 -71 -13 -72 -36 -125
|
||||
-36 -80 -44 -95 -53 -95 -4 0 -8 -6 -8 -14 0 -8 -5 -18 -12 -22 -7 -5 -9 -2
|
||||
-4 7 4 8 -1 4 -11 -8 -21 -25 -138 -85 -147 -76 -3 4 -6 1 -6 -5 0 -6 -7 -9
|
||||
-15 -6 -8 4 -15 1 -15 -6 0 -6 -4 -9 -9 -6 -5 3 -26 8 -47 11 -137 20 -247 93
|
||||
-274 180 -7 22 -19 53 -27 68 -8 16 -16 36 -18 45 -32 137 -36 169 -36 302 0
|
||||
80 2 152 4 160 12 59 44 140 65 168 34 47 79 92 92 92 6 0 22 8 37 17 47 31
|
||||
142 36 223 12z m1060 -98 c0 -38 9 -121 20 -184 35 -210 110 -369 229 -488
|
||||
145 -144 299 -168 529 -83 116 43 296 135 334 172 46 43 43 18 -7 -70 -216
|
||||
-375 -485 -627 -883 -827 -522 -262 -1255 -395 -1988 -360 -514 24 -883 122
|
||||
-1173 312 -133 88 -278 228 -334 324 -4 6 39 -12 95 -41 275 -139 616 -208
|
||||
953 -191 403 19 1003 133 1286 244 238 93 447 251 567 428 129 191 177 368
|
||||
170 636 l-3 144 90 25 c50 14 96 26 103 27 8 1 12 -19 12 -68z m128 37 c14
|
||||
-14 16 -94 3 -102 -30 -19 -56 28 -45 83 7 32 22 39 42 19z m1114 -185 c-6
|
||||
-26 -14 -57 -18 -68 -6 -16 1 -13 36 14 45 33 45 33 -7 -17 -95 -91 -226 -131
|
||||
-331 -103 l-43 12 22 37 c12 20 24 39 26 41 2 3 36 8 75 12 84 9 159 43 207
|
||||
93 19 20 37 34 39 32 2 -3 -1 -26 -6 -53z m32 -158 c-4 -8 -10 -15 -16 -15 -5
|
||||
0 -6 -6 -2 -12 4 -7 4 -10 -1 -6 -4 4 -16 -3 -26 -15 -16 -19 -18 -19 -12 -3
|
||||
5 15 3 17 -8 11 -19 -11 -2 6 35 33 34 26 37 27 30 7z m-149 -88 c-11 -7 -35
|
||||
-28 -54 -47 l-35 -35 20 34 c10 18 33 40 49 47 37 16 47 17 20 1z m-297 -84
|
||||
c-60 -88 -154 -190 -165 -180 -4 4 14 29 39 55 25 26 71 82 102 125 73 100 93
|
||||
100 24 0z m372 54 c0 -2 -19 -21 -42 -42 l-43 -40 40 43 c36 39 45 47 45 39z
|
||||
m-215 -146 c3 -6 -4 -15 -15 -21 -20 -11 -21 -10 -10 9 12 23 16 25 25 12z
|
||||
m-165 -41 c-11 -16 -26 -33 -33 -37 -12 -7 0 11 32 50 23 28 24 20 1 -13z
|
||||
m-1221 -46 c9 -8 -102 -149 -183 -230 -206 -208 -417 -302 -876 -394 -340 -68
|
||||
-617 -95 -885 -87 -217 6 -371 31 -539 87 -186 63 -464 202 -425 214 8 2 36
|
||||
11 63 20 67 21 74 21 126 -15 120 -81 319 -161 504 -201 90 -20 129 -23 366
|
||||
-22 297 0 442 15 747 79 484 101 758 245 945 496 l50 68 51 -6 c29 -3 54 -7
|
||||
56 -9z m1341 -640 c104 -15 247 -40 285 -51 13 -4 -3 -12 -50 -25 -91 -25 -99
|
||||
-25 -170 5 -33 14 -103 38 -155 52 -176 49 -154 53 90 19z m462 -132 c17 -65
|
||||
0 -429 -33 -692 -57 -461 -173 -905 -341 -1309 -24 -58 -48 -106 -55 -108 -15
|
||||
-5 -80 53 -98 88 -14 26 -12 37 35 170 28 78 49 146 48 151 -2 5 -25 -44 -52
|
||||
-109 -27 -65 -93 -217 -146 -338 -54 -121 -103 -237 -111 -257 -14 -42 -17
|
||||
-44 -43 -25 -17 13 -12 27 82 228 160 343 224 502 316 784 123 378 198 842
|
||||
182 1120 -6 111 -28 229 -46 251 -12 13 17 27 90 43 113 26 166 27 172 3z
|
||||
m-1252 -392 c63 -5 215 -10 336 -10 l222 0 7 -27 c3 -16 9 -131 12 -256 6
|
||||
-245 -6 -401 -35 -449 -21 -36 -69 -58 -126 -58 -40 0 -46 3 -52 25 -3 14 -3
|
||||
36 0 51 4 14 7 123 7 242 1 282 -18 394 -70 421 -10 6 -90 15 -177 21 -150 10
|
||||
-240 27 -274 50 -12 8 -9 9 10 5 14 -2 77 -9 140 -15z m-275 -35 c241 -62 374
|
||||
-242 375 -505 0 -115 -19 -144 -133 -199 -101 -50 -171 -69 -306 -82 -113 -11
|
||||
-271 1 -340 26 -170 60 -231 160 -201 333 33 197 175 352 364 398 39 9 85 23
|
||||
101 30 40 17 69 17 140 -1z m-1874 -81 l36 -6 -14 -41 c-9 -27 -12 -57 -8 -85
|
||||
5 -33 3 -47 -9 -55 -8 -7 -24 -41 -36 -77 -12 -36 -36 -90 -54 -120 -18 -30
|
||||
-41 -76 -51 -102 -16 -42 -21 -48 -47 -48 -45 0 -97 29 -115 63 -29 57 9 210
|
||||
78 315 38 58 150 162 174 162 5 0 26 -3 46 -6z m126 -131 c10 -12 12 -13 8 -3
|
||||
-4 8 1 6 10 -5 9 -11 19 -19 23 -18 4 2 15 0 25 -3 13 -4 15 -9 8 -18 -14 -17
|
||||
-2 -29 15 -15 10 8 11 5 7 -12 -4 -15 -3 -20 5 -15 7 4 12 3 12 -2 0 -5 10
|
||||
-12 22 -15 12 -4 20 -9 17 -12 -9 -8 -88 30 -105 51 -9 10 -13 13 -9 7 10 -22
|
||||
-23 -3 -38 20 -18 28 -28 57 -20 57 3 0 12 -8 20 -17z m180 -74 c21 -8 24 -12
|
||||
14 -19 -17 -10 -38 -3 -45 16 -6 17 -5 17 31 3z m724 -40 c-6 -24 -11 -78 -11
|
||||
-120 l0 -76 -46 -36 c-180 -140 -693 -157 -854 -29 -53 43 -72 87 -66 149 6
|
||||
53 25 106 35 101 3 -2 44 -22 91 -45 114 -57 238 -86 365 -86 160 -1 284 42
|
||||
435 151 30 22 56 39 58 37 1 -1 -2 -22 -7 -46z m-711 -9 c-20 -13 -33 -13 -25
|
||||
0 3 6 14 10 23 10 15 0 15 -2 2 -10z m-309 -254 c73 -120 247 -173 536 -163
|
||||
222 8 391 54 495 137 28 23 38 27 46 17 14 -17 47 -143 63 -241 14 -87 38
|
||||
-131 135 -242 29 -34 50 -64 47 -68 -3 -3 -1 -6 6 -6 6 0 28 -15 47 -32 33
|
||||
-30 36 -37 31 -73 -7 -53 -15 -89 -33 -155 -25 -89 -24 -87 -34 -70 -6 8 -10
|
||||
11 -10 5 0 -5 -12 4 -26 20 -14 17 -22 24 -19 17 4 -7 -31 14 -78 47 -161 116
|
||||
-495 262 -714 312 -109 25 -337 36 -411 20 -75 -16 -140 -49 -159 -82 -10 -16
|
||||
-21 -29 -25 -29 -28 0 -46 210 -29 330 18 118 77 301 98 300 4 0 20 -20 34
|
||||
-44z m2441 -248 c74 -8 112 -8 159 1 33 7 64 10 67 6 16 -16 -161 -611 -192
|
||||
-642 -28 -30 -107 -6 -121 37 -3 11 16 97 43 192 65 224 94 358 78 358 -6 0
|
||||
-42 5 -79 10 -62 9 -93 24 -150 71 -5 3 16 0 45 -7 29 -8 97 -19 150 -26z
|
||||
m-233 -47 c24 -18 23 -18 -120 -24 -86 -3 -202 -15 -289 -30 -156 -28 -310
|
||||
-46 -310 -38 0 3 21 27 47 53 l47 48 91 0 c133 0 262 16 346 43 l74 23 45 -28
|
||||
c25 -15 56 -36 69 -47z m-2433 51 c7 -5 8 -32 3 -82 -10 -104 4 -236 32 -300
|
||||
18 -42 38 -65 96 -111 91 -72 88 -78 -3 -9 -113 87 -224 129 -336 130 l-47 0
|
||||
74 110 c41 60 92 146 112 190 37 78 46 88 69 72z m2448 -162 l59 0 -6 -94
|
||||
c-12 -175 -77 -427 -156 -600 l-31 -69 -28 53 c-63 125 -217 236 -362 262 -59
|
||||
10 -186 0 -235 -18 -10 -4 -8 11 8 58 17 51 21 89 21 200 l1 138 45 5 c25 3
|
||||
126 17 225 32 226 32 320 42 365 37 19 -2 62 -4 94 -4z m-1904 -334 c0 -88 24
|
||||
-218 59 -316 l29 -83 -81 6 c-78 5 -84 7 -110 39 -60 71 -98 153 -104 227 -9
|
||||
112 18 152 122 177 87 21 85 22 85 -50z m225 34 c141 -29 340 -116 488 -213
|
||||
49 -33 73 -54 65 -58 -38 -21 -228 -50 -385 -60 -164 -9 -211 -16 -254 -34
|
||||
-14 -5 -21 6 -37 57 -36 109 -42 141 -48 239 l-7 96 59 -7 c33 -4 86 -13 119
|
||||
-20z m-969 -20 c42 -21 84 -70 103 -123 23 -61 2 -238 -31 -258 -48 -30 -558
|
||||
-89 -558 -65 0 4 5 16 10 27 48 89 14 270 -62 323 -12 9 -20 17 -17 19 2 2 49
|
||||
11 104 21 55 9 150 30 210 46 134 34 185 37 241 10z m-730 -164 c28 -9 63 -25
|
||||
78 -37 24 -19 26 -27 26 -92 0 -40 -5 -89 -10 -109 -30 -106 -187 -190 -249
|
||||
-134 -12 11 -21 25 -21 33 0 7 31 46 69 85 l69 73 -42 2 c-22 1 -65 5 -93 8
|
||||
l-53 6 0 49 c0 68 -24 94 -84 95 -25 0 -46 1 -46 2 0 5 48 27 75 34 46 12 223
|
||||
2 281 -15z m204 -18 c15 -24 20 -49 19 -93 l-2 -60 -7 67 c-3 36 -14 78 -23
|
||||
92 -10 14 -15 26 -12 26 3 0 14 -15 25 -32z m1049 -164 c17 -28 31 -54 31 -57
|
||||
0 -3 -15 -8 -32 -12 -18 -4 -68 -22 -110 -41 -43 -18 -115 -43 -160 -54 -45
|
||||
-11 -92 -29 -103 -40 -12 -11 -31 -20 -43 -20 -24 0 -303 48 -308 53 -2 2 36
|
||||
9 84 15 193 26 356 84 439 156 23 20 61 66 84 102 l43 65 21 -58 c12 -32 36
|
||||
-81 54 -109z m-1776 111 c-12 -8 -51 -57 -87 -108 -87 -122 -95 -132 -102
|
||||
-124 -4 4 11 30 32 59 22 29 55 77 73 106 18 29 45 59 60 67 35 19 51 19 24 0z
|
||||
m245 3 c17 -17 15 -42 -7 -69 -16 -20 -28 -23 -79 -24 -43 0 -65 5 -79 18 -11
|
||||
9 -25 17 -33 17 -7 0 -1 -9 14 -21 14 -11 26 -26 26 -33 0 -7 -14 -41 -31 -75
|
||||
-34 -68 -68 -92 -131 -90 -26 0 -29 2 -14 8 14 5 16 10 7 15 -6 4 -17 3 -24
|
||||
-3 -7 -5 -18 -6 -25 -2 -10 6 5 36 57 113 38 57 77 111 86 118 40 33 208 53
|
||||
233 28z m2962 -43 c66 -23 125 -77 159 -147 26 -52 31 -73 31 -135 0 -104 -26
|
||||
-202 -57 -214 -74 -28 -500 -69 -716 -69 -91 0 -99 2 -92 18 4 9 12 42 17 72
|
||||
17 102 -20 226 -93 305 -48 52 -49 51 71 60 112 8 299 46 435 88 123 39 183
|
||||
44 245 22z m265 -52 c26 -25 36 -60 25 -88 -10 -25 -30 -35 -30 -14 0 7 -13
|
||||
34 -30 61 -16 26 -30 50 -30 53 0 12 50 3 65 -12z m538 -21 c58 -4 59 -5 53
|
||||
-32 -3 -15 -29 -80 -58 -143 l-53 -116 -48 -1 c-27 0 -46 3 -44 8 73 124 127
|
||||
223 127 231 0 6 -18 11 -40 11 -41 0 -48 9 -34 45 4 10 12 14 23 9 9 -4 42 -9
|
||||
74 -12z m-3726 -149 c-9 -28 -7 -38 9 -61 60 -81 178 -81 289 -1 l45 32 117
|
||||
-27 c65 -15 214 -46 330 -69 l213 -42 -61 -49 c-57 -46 -61 -52 -56 -83 5 -28
|
||||
3 -33 -14 -33 -28 0 -299 62 -534 122 -110 28 -294 72 -410 98 l-210 47 60 31
|
||||
c33 16 77 43 98 59 36 27 42 29 86 19 l48 -9 -10 -34z m1868 -9 l116 -18 -49
|
||||
-18 c-80 -30 -87 -43 -88 -172 l-1 -111 -66 -3 -65 -3 10 35 c8 26 8 42 -2 66
|
||||
-18 43 -49 61 -109 62 -45 0 -52 3 -64 26 -9 19 -22 28 -47 32 -26 4 -35 11
|
||||
-40 30 -4 19 -13 26 -38 28 -42 4 -40 18 6 38 88 37 221 40 437 8z m319 -68
|
||||
c22 -80 29 -129 17 -121 -6 3 -11 18 -11 33 0 32 -22 107 -45 153 l-16 34 21
|
||||
-24 c11 -13 26 -46 34 -75z m-839 6 c-6 -5 -28 -14 -51 -20 -63 -18 -104 -51
|
||||
-165 -132 -31 -41 -70 -83 -85 -92 -32 -20 -69 -23 -79 -8 -7 11 173 187 214
|
||||
209 20 12 82 30 171 50 2 1 0 -3 -5 -7z m76 -36 c0 -5 -24 -17 -53 -26 -82
|
||||
-26 -114 -46 -212 -134 -50 -45 -100 -85 -111 -89 -16 -6 -15 -3 5 13 14 11
|
||||
27 20 30 20 3 0 34 36 69 80 67 83 106 109 199 135 52 15 73 15 73 1z m605
|
||||
-27 c14 -16 18 -34 17 -70 -3 -74 -12 -84 -72 -83 -28 0 -53 3 -56 6 -3 3 -6
|
||||
34 -6 69 -1 54 2 66 21 81 30 24 73 23 96 -3z m-2335 -21 l52 -12 -66 -12
|
||||
c-57 -11 -72 -10 -124 6 -47 15 -53 19 -33 23 49 9 119 7 171 -5z m1819 -6 c0
|
||||
-4 -23 -16 -52 -26 -35 -12 -72 -37 -114 -78 -34 -33 -65 -59 -68 -58 -4 1
|
||||
-31 -11 -61 -26 -53 -25 -105 -28 -105 -4 0 16 163 142 216 167 29 13 78 27
|
||||
110 31 59 8 74 7 74 -6z m-2055 -19 l40 -8 -40 -5 c-50 -6 -105 -1 -105 11 0
|
||||
11 50 11 105 2z m-190 -28 c29 -14 26 -15 -48 -11 -43 1 -83 8 -90 15 -17 17
|
||||
101 14 138 -4z m623 3 l47 -12 -55 -11 c-59 -12 -116 -5 -171 19 -34 14 -33
|
||||
14 49 15 46 1 104 -4 130 -11z m-903 -18 c27 -7 27 -8 5 -8 -14 0 -36 4 -50 8
|
||||
-20 6 -21 8 -5 8 11 0 34 -4 50 -8z m2689 -12 c7 -12 7 -26 0 -45 -9 -27 -12
|
||||
-28 -82 -28 -62 0 -72 -2 -72 -17 0 -23 -67 -67 -113 -75 -45 -7 -87 3 -87 21
|
||||
0 19 123 134 162 151 47 21 177 16 192 -7z m-2802 -5 l68 -17 -60 -13 c-55
|
||||
-11 -67 -10 -138 12 l-77 23 45 4 c25 3 56 5 70 6 14 1 55 -6 92 -15z m727 -5
|
||||
c36 -6 62 -13 56 -14 -5 -2 -44 2 -85 10 -41 8 -66 14 -56 15 11 1 49 -4 85
|
||||
-11z m441 -7 l55 -18 -50 -7 c-31 -4 -78 -2 -120 5 l-70 13 50 12 c66 16 71
|
||||
16 135 -5z m-625 -2 c13 -6 0 -7 -40 -3 -33 4 -62 8 -64 10 -9 8 85 2 104 -7z
|
||||
m-242 -6 c-13 -2 -35 -2 -50 0 -16 2 -5 4 22 4 28 0 40 -2 28 -4z m4772 -13
|
||||
c25 -28 8 -45 -31 -30 -20 8 -115 9 -316 4 -384 -9 -384 8 0 28 158 8 296 16
|
||||
308 17 11 0 29 -8 39 -19z m-4229 1 c-3 -5 -15 -7 -26 -4 -28 7 -25 13 6 13
|
||||
14 0 23 -4 20 -9z m47 -8 c-7 -2 -19 -2 -25 0 -7 3 -2 5 12 5 14 0 19 -2 13
|
||||
-5z m452 -9 l40 -7 -41 -18 c-41 -17 -78 -15 -164 7 -22 6 -19 8 25 19 49 13
|
||||
61 12 140 -1z m-640 -3 c14 -6 5 -8 -30 -9 -27 -1 -57 3 -65 8 -19 12 65 13
|
||||
95 1z m232 3 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-622 -47
|
||||
c122 -32 331 -89 455 -124 l55 -15 -34 -14 c-42 -18 -66 -14 -251 41 -80 24
|
||||
-219 64 -310 89 -222 60 -237 68 -130 62 52 -3 136 -18 215 -39z m160 33 c19
|
||||
-5 6 -6 -35 -3 -36 3 -67 7 -69 9 -8 7 73 2 104 -6z m383 -41 c-13 -9 -88 -3
|
||||
-118 8 -11 4 -8 8 10 12 33 9 126 -9 108 -20z m194 10 c-15 -5 -36 -6 -55 0
|
||||
-29 8 -27 9 23 9 44 1 50 -1 32 -9z m508 -10 l55 -13 -64 -10 c-51 -7 -78 -6
|
||||
-140 10 -73 17 -75 18 -41 25 44 9 118 4 190 -12z m-925 1 c27 -7 28 -8 5 -8
|
||||
-14 0 -45 3 -70 7 l-45 8 40 1 c22 0 54 -4 70 -8z m1742 -15 c-15 -14 -36 -25
|
||||
-45 -25 -10 0 0 11 23 24 52 31 57 31 22 1z m-1098 -25 c21 -13 8 -12 -69 5
|
||||
l-65 15 59 -5 c32 -3 66 -10 75 -15z m-474 -15 c6 -3 -16 -3 -48 -2 -32 2 -62
|
||||
8 -68 14 -10 10 92 -1 116 -12z m175 15 c-12 -8 -12 -10 -1 -10 8 0 12 -2 9
|
||||
-5 -3 -3 -25 -3 -49 1 -34 6 -39 9 -24 15 11 4 34 8 50 8 23 1 26 -2 15 -9z
|
||||
m800 -14 l45 -13 -61 -7 c-62 -8 -184 9 -184 25 0 22 122 19 200 -5z m-722 7
|
||||
c-10 -2 -28 -2 -40 0 -13 2 -5 4 17 4 22 1 32 -1 23 -4z m357 -18 l40 -14 -58
|
||||
3 c-32 1 -63 8 -69 14 -17 17 35 15 87 -3z m-1473 -16 c44 -12 77 -23 75 -26
|
||||
-4 -4 -158 36 -166 43 -9 8 14 4 91 -17z m1263 0 c26 0 27 -2 10 -9 -25 -11
|
||||
-102 -12 -135 -3 -23 7 -21 8 15 14 22 4 49 5 60 3 11 -2 34 -4 50 -5z m1210
|
||||
1 c4 -7 -3 -8 -22 -4 -38 9 -42 14 -10 14 14 0 29 -5 32 -10z m1393 3 c-10 -2
|
||||
-26 -2 -35 0 -10 3 -2 5 17 5 19 0 27 -2 18 -5z m-1841 -20 c52 -9 90 -20 85
|
||||
-25 -20 -20 -113 -21 -198 -2 l-85 19 48 11 c26 6 50 12 52 13 3 0 47 -7 98
|
||||
-16z m-1027 -3 l25 -8 -25 0 c-14 -1 -43 3 -65 7 l-40 9 40 0 c22 0 51 -4 65
|
||||
-8z m745 -21 l40 -13 -42 -9 c-52 -12 -226 22 -182 35 32 10 134 2 184 -13z
|
||||
m-1085 -152 c578 -168 1031 -304 1038 -312 18 -18 -78 6 -332 81 -160 47 -356
|
||||
104 -435 125 -225 63 -901 279 -826 265 11 -2 261 -74 555 -159z m769 144 c25
|
||||
-16 -6 -22 -76 -15 -47 5 -61 10 -48 15 27 10 107 11 124 0z m-259 -11 c19 -6
|
||||
12 -8 -30 -8 -30 0 -64 3 -75 8 -26 11 70 11 105 0z m1148 -28 c97 -34 108
|
||||
-42 56 -42 -30 0 -36 3 -24 11 11 8 -1 9 -44 5 -35 -3 -91 1 -135 10 -104 21
|
||||
-114 27 -61 34 88 13 128 10 208 -18z m2355 -9 c-2 -10 -11 -27 -18 -37 -14
|
||||
-18 -17 -18 -97 -2 -139 27 -154 35 -73 34 39 0 81 4 95 10 14 6 41 11 62 11
|
||||
30 1 35 -2 31 -16z m-2978 -22 c44 -11 113 -25 154 -32 l74 -11 -32 -14 c-19
|
||||
-8 -45 -14 -60 -14 -24 0 -290 69 -351 91 -47 17 132 0 215 -20z m245 22 c73
|
||||
-5 74 -5 46 -20 -23 -12 -39 -13 -99 -3 -133 22 -105 35 53 23z m-815 -31 c64
|
||||
-21 61 -27 -22 -37 -42 -4 -71 0 -140 22 l-88 28 50 6 c69 9 130 4 200 -19z
|
||||
m225 19 l25 -7 -30 -6 c-16 -4 -57 0 -90 7 l-60 13 65 0 c36 0 76 -3 90 -7z
|
||||
m765 -28 c31 -5 35 -8 20 -14 -30 -12 -86 -9 -125 6 l-35 13 50 1 c28 0 68 -2
|
||||
90 -6z m508 -24 c26 -6 47 -14 47 -17 0 -15 -139 -11 -210 7 l-75 18 95 1 c52
|
||||
1 116 -3 143 -9z m107 -19 c-36 -11 -52 -11 -45 0 3 6 21 10 38 9 31 -1 31 -1
|
||||
7 -9z m-359 -14 l34 -14 -28 -7 c-29 -8 -172 1 -217 13 -24 6 -23 7 10 13 65
|
||||
12 167 9 201 -5z m-986 -10 c28 -7 66 -20 85 -28 l35 -15 -83 -6 c-75 -6 -89
|
||||
-4 -160 22 -68 26 -74 29 -47 33 61 10 121 7 170 -6z m1544 -14 l75 -17 -78
|
||||
-14 c-74 -12 -83 -11 -160 11 l-81 23 65 6 c36 3 74 6 85 7 11 1 53 -7 94 -16z
|
||||
m-819 -2 c0 -5 -6 -10 -12 -11 -7 0 -4 -4 7 -9 40 -17 -59 -10 -125 10 l-65
|
||||
19 98 0 c58 1 97 -3 97 -9z m-181 -38 c20 -7 11 -10 -52 -19 -65 -9 -86 -8
|
||||
-144 7 -65 17 -66 18 -33 23 49 7 200 0 229 -11z m743 -1 c11 -12 10 -13 -3
|
||||
-8 -8 3 -38 1 -65 -5 -56 -10 -211 10 -177 24 32 13 231 5 245 -11z m-265 -30
|
||||
c15 -9 13 -10 -12 -11 -17 0 -45 -4 -62 -9 -24 -7 -49 -5 -95 9 l-63 18 40 5
|
||||
c57 7 172 0 192 -12z m-730 -15 l37 -13 -46 -6 c-47 -6 -143 14 -163 35 -13
|
||||
14 123 1 172 -16z m488 -16 c14 -5 -1 -11 -55 -20 -60 -10 -87 -10 -135 0 -63
|
||||
12 -115 34 -60 24 17 -2 57 0 90 5 59 9 121 6 160 -9z m1154 -44 c-2 -2 -13 8
|
||||
-23 22 l-19 27 24 -22 c12 -12 21 -24 18 -27z m-1420 -6 c12 -7 8 -10 -19 -11
|
||||
-53 -3 -80 1 -114 17 -29 13 -27 14 43 9 40 -3 81 -10 90 -15z m1351 17 c0 -2
|
||||
-9 -12 -20 -22 -24 -21 -27 -14 -4 9 15 15 24 20 24 13z m35 -17 c3 -6 -1 -7
|
||||
-9 -4 -18 7 -21 14 -7 14 6 0 13 -4 16 -10z m2322 -26 c-3 -3 -12 -4 -19 -1
|
||||
-8 3 -5 6 6 6 11 1 17 -2 13 -5z m-2383 -57 c7 -6 6 -7 -4 -4 -8 2 -16 14 -17
|
||||
28 -1 19 -1 20 4 4 3 -11 10 -24 17 -28z"/>
|
||||
<path d="M5283 8633 c9 -2 25 -2 35 0 9 3 1 5 -18 5 -19 0 -27 -2 -17 -5z"/>
|
||||
<path d="M5218 8623 c6 -2 18 -2 25 0 6 3 1 5 -13 5 -14 0 -19 -2 -12 -5z"/>
|
||||
<path d="M5113 8592 c-23 -10 -48 -23 -55 -30 -11 -11 97 33 111 44 10 9 -17
|
||||
2 -56 -14z"/>
|
||||
<path d="M7275 8230 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0
|
||||
-7 -4 -4 -10z"/>
|
||||
<path d="M9280 6916 c0 -2 7 -9 15 -16 9 -7 15 -8 15 -2 0 5 -7 12 -15 16 -8
|
||||
3 -15 4 -15 2z"/>
|
||||
<path d="M8555 7085 c-33 -30 -55 -54 -49 -55 6 0 17 6 23 13 6 8 28 22 49 31
|
||||
29 13 38 23 38 42 0 13 0 24 0 24 -1 0 -28 -25 -61 -55z"/>
|
||||
<path d="M8865 7010 c-16 -7 -17 -9 -3 -9 9 -1 20 4 23 9 7 11 7 11 -20 0z"/>
|
||||
<path d="M8760 6979 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M4910 6295 c0 -8 4 -15 8 -15 8 0 32 -43 32 -57 0 -3 -8 1 -17 8 -16
|
||||
13 -17 12 -4 -3 7 -10 17 -15 22 -13 12 8 36 -32 59 -100 22 -64 25 -77 10
|
||||
-55 -8 12 -10 11 -10 -7 0 -15 6 -23 18 -24 10 0 12 -3 5 -6 -9 -3 -13 -31
|
||||
-12 -91 0 -58 3 -80 9 -67 7 15 9 16 9 4 1 -9 -6 -22 -14 -29 -17 -14 -20 -40
|
||||
-5 -40 6 0 6 -9 -1 -26 -5 -15 -9 -45 -7 -66 2 -39 2 -39 18 -17 39 51 55 120
|
||||
55 239 0 93 -5 128 -23 184 -25 75 -67 143 -108 175 -32 25 -44 26 -44 6z"/>
|
||||
<path d="M4622 5910 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
|
||||
<path d="M4632 5840 c0 -14 2 -19 5 -12 2 6 2 18 0 25 -3 6 -5 1 -5 -13z"/>
|
||||
<path d="M4760 5454 c0 -5 7 -18 15 -28 8 -11 15 -16 15 -10 0 5 -7 18 -15 28
|
||||
-8 11 -15 16 -15 10z"/>
|
||||
<path d="M7960 4356 c24 -26 60 -52 60 -42 0 2 -19 19 -42 37 -40 30 -41 31
|
||||
-18 5z"/>
|
||||
<path d="M8027 4286 c-3 -8 1 -13 11 -12 9 0 16 -5 14 -12 -1 -8 2 -11 8 -7 8
|
||||
5 6 13 -6 26 -20 23 -20 23 -27 5z"/>
|
||||
<path d="M8143 3935 c0 -27 2 -38 4 -22 2 15 2 37 0 50 -2 12 -4 0 -4 -28z"/>
|
||||
<path d="M8131 3834 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
|
||||
<path d="M4667 2598 c-22 -14 46 -12 71 1 8 4 -3 7 -23 7 -20 -1 -42 -4 -48
|
||||
-8z"/>
|
||||
<path d="M6554 2467 c-12 -13 4 -87 20 -87 5 0 1 -7 -10 -15 -10 -8 -14 -15
|
||||
-7 -15 36 -1 56 67 33 111 -12 22 -20 23 -36 6z"/>
|
||||
<path d="M6520 2441 c0 -12 5 -21 10 -21 6 0 10 6 10 14 0 8 -4 18 -10 21 -5
|
||||
3 -10 -3 -10 -14z"/>
|
||||
<path d="M6098 2353 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
|
||||
<path d="M1455 5297 c-20 -17 -28 -32 -27 -55 0 -17 41 -128 91 -247 90 -217
|
||||
90 -217 70 -234 -21 -17 -21 -22 -14 -209 7 -173 10 -195 28 -219 20 -25 23
|
||||
-26 108 -20 62 4 90 2 97 -6 5 -7 71 -156 146 -332 89 -207 133 -321 125 -324
|
||||
-15 -5 -45 -35 -51 -51 -2 -7 -83 -20 -238 -36 -441 -45 -636 -92 -873 -210
|
||||
-204 -103 -362 -274 -412 -448 -69 -241 88 -466 431 -615 119 -51 269 -94 489
|
||||
-138 145 -29 185 -41 206 -60 21 -20 491 -223 643 -279 48 -17 222 -18 600 -3
|
||||
290 11 307 15 327 65 6 18 5 34 -3 52 -14 29 -107 72 -433 202 -88 35 -165 68
|
||||
-172 74 -7 6 -17 41 -23 77 -30 195 76 616 194 763 l23 30 79 -17 c148 -31
|
||||
889 -188 1033 -218 160 -35 178 -33 206 21 20 39 19 42 -95 315 -98 234 -363
|
||||
899 -605 1515 -201 514 -202 516 -233 536 -28 19 -28 19 -1052 63 -256 11
|
||||
-504 23 -551 26 -81 6 -88 5 -114 -18z m792 -83 c-3 -3 -12 -4 -19 -1 -8 3 -5
|
||||
6 6 6 11 1 17 -2 13 -5z m578 -25 c73 -5 19 -6 -160 -3 -148 3 -301 9 -340 14
|
||||
-86 10 341 1 500 -11z m-947 -135 c23 -4 32 -10 32 -24 0 -24 -2 -24 -105 -8
|
||||
-76 11 -94 20 -95 44 0 9 38 7 168 -12z m132 -67 c0 -32 0 -32 -45 -29 -42 4
|
||||
-45 6 -45 33 0 28 2 29 45 29 44 0 45 -1 45 -33z m90 4 c0 -17 -4 -33 -10 -36
|
||||
-6 -4 -10 8 -10 29 0 20 5 36 10 36 6 0 10 -13 10 -29z m190 4 c0 -11 -5 -12
|
||||
-23 -6 -14 6 -30 6 -38 0 -19 -12 -32 -11 -24 1 3 6 -5 10 -19 10 -15 0 -26
|
||||
-7 -29 -17 -4 -14 -5 -12 -6 5 -1 21 3 22 69 22 55 0 70 -3 70 -15z m-421 -1
|
||||
c21 -4 31 -12 31 -25 0 -17 -3 -17 -45 -2 -68 23 -59 41 14 27z m718 -20 c18
|
||||
-5 21 -13 21 -48 l-1 -42 -56 8 c-130 19 -121 14 -121 60 l0 41 68 -7 c37 -4
|
||||
77 -9 89 -12z m-217 -25 c0 -17 -4 -28 -10 -24 -5 3 -10 17 -10 31 0 13 5 24
|
||||
10 24 6 0 10 -14 10 -31z m308 -16 c2 -8 -5 -13 -17 -13 -12 0 -21 6 -21 16 0
|
||||
18 31 15 38 -3z m-816 -9 c53 -6 56 -8 61 -38 2 -17 3 -32 2 -34 -2 -2 -46 5
|
||||
-99 14 -96 17 -96 17 -96 47 0 29 1 29 38 24 20 -4 62 -9 94 -13z m1101 5 c3
|
||||
-2 8 -21 11 -43 l7 -39 -63 7 c-140 16 -138 15 -138 54 l0 34 90 -4 c49 -2 91
|
||||
-6 93 -9z m-1253 -96 c0 -5 -5 -15 -10 -23 -8 -12 -10 -11 -10 8 0 12 5 22 10
|
||||
22 6 0 10 -3 10 -7z m1039 -14 c83 -10 102 -15 106 -30 4 -11 12 -19 19 -19 8
|
||||
0 19 -7 26 -15 16 -19 9 -19 -145 0 -66 8 -123 15 -127 15 -5 0 -8 14 -8 30 0
|
||||
35 -6 34 129 19z m-611 -40 c116 -12 157 -29 52 -21 l-60 4 6 -78 c3 -44 7
|
||||
-92 9 -109 4 -28 4 -29 -10 -11 -31 40 -76 86 -85 86 -15 0 -23 49 -11 64 9
|
||||
10 4 19 -24 40 -40 31 -45 48 -12 42 12 -3 73 -10 135 -17z m850 -30 c4 -53
|
||||
-13 -42 -21 14 -4 27 -3 37 6 34 7 -2 13 -23 15 -48z m-1020 10 c-3 -8 -7 -3
|
||||
-11 10 -4 17 -3 21 5 13 5 -5 8 -16 6 -23z m952 21 c0 -5 -2 -10 -4 -10 -3 0
|
||||
-8 5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m477 -210 c30 -69 79 -183 108
|
||||
-255 29 -71 110 -269 179 -439 69 -170 124 -311 122 -313 -2 -2 -9 11 -16 29
|
||||
-7 18 -47 107 -87 198 -117 260 -414 977 -413 997 1 14 50 -85 107 -217z
|
||||
m-1462 196 c18 -13 18 -14 -8 -9 -15 3 -27 9 -27 14 0 13 13 11 35 -5z m24
|
||||
-24 c54 -7 54 -7 58 -44 4 -38 4 -38 -34 -38 -49 1 -122 15 -133 26 -5 5 -12
|
||||
21 -15 37 -7 26 -6 27 31 26 22 -1 63 -4 93 -7z m567 -12 l61 0 7 -47 c3 -25
|
||||
4 -48 1 -51 -2 -2 -16 -2 -30 2 -21 5 -25 12 -25 42 0 19 -4 33 -9 30 -12 -8
|
||||
-124 14 -136 27 -7 6 4 8 30 3 22 -3 68 -6 101 -6z m-336 -21 c0 -11 -5 -17
|
||||
-10 -14 -6 4 -8 10 -5 15 3 4 -1 11 -7 13 -8 3 -6 6 5 6 11 1 17 -6 17 -20z
|
||||
m53 14 c-7 -2 -21 -2 -30 0 -10 3 -4 5 12 5 17 0 24 -2 18 -5z m497 -18 c10
|
||||
-6 50 -78 50 -90 0 -7 -85 -6 -104 1 -12 5 -16 20 -16 59 l0 54 30 -9 c16 -5
|
||||
34 -11 40 -15z m-1026 -165 l65 -155 -61 -8 c-34 -4 -64 -4 -68 0 -11 12 -22
|
||||
340 -11 328 5 -6 39 -80 75 -165z m713 123 c-3 -10 -5 -2 -5 17 0 19 2 27 5
|
||||
18 2 -10 2 -26 0 -35z m-17 13 c0 -9 -8 -12 -25 -8 -37 7 -41 22 -6 22 20 0
|
||||
31 -5 31 -14z m-535 -46 c3 -5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10
|
||||
3 0 8 -4 11 -10z m82 -22 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12 1 -19z
|
||||
m51 4 c2 -7 -2 -10 -12 -6 -9 3 -16 11 -16 16 0 13 23 5 28 -10z m-19 -52 c71
|
||||
-18 91 -30 91 -57 0 -16 1 -16 -97 4 -73 15 -83 20 -83 38 0 17 8 36 14 35 1
|
||||
0 34 -9 75 -20z m98 -111 c57 -10 61 -13 65 -40 3 -16 3 -29 0 -29 -25 0 -190
|
||||
35 -199 43 -7 5 -13 18 -13 29 0 22 3 21 147 -3z m661 -33 c3 -33 1 -36 -22
|
||||
-36 -21 0 -26 6 -32 34 -8 44 0 57 29 47 16 -6 23 -18 25 -45z m248 -6 c1 0 4
|
||||
-14 6 -31 l5 -31 -65 6 c-36 4 -73 11 -83 17 -17 9 -26 48 -14 60 3 4 126 -13
|
||||
151 -21z m-770 10 c21 0 24 -5 24 -35 0 -33 -1 -35 -27 -29 -36 8 -40 13 -49
|
||||
48 -6 25 -5 28 10 23 10 -4 29 -7 42 -7z m124 -36 c0 -19 -4 -34 -10 -34 -5 0
|
||||
-10 18 -10 41 0 24 4 38 10 34 6 -3 10 -22 10 -41z m176 6 c20 0 24 -5 24 -29
|
||||
0 -17 5 -33 10 -36 15 -9 12 -35 -4 -35 -8 0 -16 4 -18 9 -1 5 -26 11 -54 15
|
||||
-72 8 -117 27 -50 20 27 -2 60 -7 73 -10 19 -5 23 -2 23 16 0 19 -4 22 -26 17
|
||||
-14 -2 -31 1 -37 7 -7 6 -23 17 -37 24 -23 11 -20 12 24 7 27 -3 59 -5 72 -5z
|
||||
m-599 -6 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m59 -61 c-2
|
||||
-3 -15 5 -28 17 -16 16 -18 21 -6 17 10 -3 19 1 24 11 6 13 9 11 12 -11 2 -16
|
||||
1 -31 -2 -34z m150 25 c54 -10 64 -15 69 -36 3 -13 4 -26 1 -29 -2 -2 -40 1
|
||||
-85 8 -79 12 -81 13 -81 41 0 32 5 33 96 16z m489 2 c3 -5 -1 -10 -10 -10 -9
|
||||
0 -13 5 -10 10 3 6 8 10 10 10 2 0 7 -4 10 -10z m-183 -75 c4 -16 1 -25 -8
|
||||
-25 -14 0 -23 19 -16 38 7 20 19 14 24 -13z m88 -5 c0 -5 -4 -10 -9 -10 -6 0
|
||||
-13 5 -16 10 -3 6 1 10 9 10 9 0 16 -4 16 -10z m140 -41 c0 -16 -4 -29 -10
|
||||
-29 -5 0 -10 16 -10 36 0 21 4 33 10 29 6 -3 10 -19 10 -36z m-614 23 c6 -4
|
||||
14 -16 17 -27 7 -18 6 -18 -10 2 -12 14 -32 23 -58 26 l-40 4 40 1 c22 1 45
|
||||
-2 51 -6z m222 -27 c6 -2 12 -16 12 -30 0 -28 -12 -31 -70 -16 -19 5 -52 12
|
||||
-72 16 -34 5 -38 9 -38 34 l0 29 78 -15 c42 -7 83 -16 90 -18z m-238 5 c0 -5
|
||||
-4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m877 -12
|
||||
c53 -8 62 -13 68 -35 3 -14 3 -28 0 -31 -5 -5 -213 32 -220 40 -2 2 -2 14 2
|
||||
27 6 24 -2 24 150 -1z m-567 -34 c0 -16 -26 -19 -35 -4 -3 6 -4 14 0 20 8 13
|
||||
35 1 35 -16z m173 9 c13 -4 17 -16 17 -49 0 -50 -12 -55 -47 -22 -29 27 -34
|
||||
95 -5 83 9 -4 25 -10 35 -12z m-405 -71 c2 -32 0 -42 -12 -42 -9 0 -18 12 -22
|
||||
28 -3 15 -7 32 -9 39 -5 15 6 24 25 21 10 -2 16 -17 18 -46z m512 8 c0 -19 -5
|
||||
-30 -14 -30 -8 0 -16 13 -18 30 -4 24 -1 30 14 30 13 0 18 -8 18 -30z m-325
|
||||
-13 c57 -11 60 -14 63 -43 l3 -31 -93 18 c-51 10 -94 19 -95 19 -2 0 -3 14 -3
|
||||
30 0 34 -12 34 125 7z m485 10 c50 -11 55 -15 58 -40 3 -27 4 -28 18 -10 12
|
||||
16 15 16 24 3 19 -30 10 -33 -77 -27 -130 10 -148 17 -148 55 -1 30 1 32 35
|
||||
31 19 0 60 -6 90 -12z m-248 -88 c5 -33 6 -33 -59 -18 -46 11 -49 14 -45 39 4
|
||||
26 4 27 52 17 43 -8 49 -13 52 -38z m88 -19 c0 -11 -2 -20 -4 -20 -2 0 -6 9
|
||||
-9 20 -3 11 -1 20 4 20 5 0 9 -9 9 -20z m198 -7 c5 -34 -34 -31 -41 3 -4 21
|
||||
-1 25 17 22 13 -2 22 -12 24 -25z m-144 -8 c3 -8 1 -15 -4 -15 -6 0 -10 7 -10
|
||||
15 0 8 2 15 4 15 2 0 6 -7 10 -15z m-279 -24 c75 -15 80 -18 83 -43 4 -33 10
|
||||
-32 -101 -13 -119 21 -137 29 -137 59 0 28 -1 28 155 -3z m623 1 c8 -11 12
|
||||
-12 12 -3 0 8 11 11 32 9 24 -2 34 -9 37 -23 5 -29 40 -50 37 -22 0 6 3 13 9
|
||||
14 5 2 10 -5 10 -15 0 -13 11 -18 52 -22 45 -5 87 -20 54 -20 -7 0 -9 -4 -6
|
||||
-10 4 -6 -21 -10 -68 -10 -107 0 -137 8 -151 40 -6 15 -16 25 -23 23 -6 -2
|
||||
-29 1 -51 6 -34 10 -39 14 -35 35 5 22 9 24 42 19 20 -3 42 -12 49 -21z m-168
|
||||
-16 c0 -3 -4 -8 -10 -11 -5 -3 -10 -1 -10 4 0 6 5 11 10 11 6 0 10 -2 10 -4z
|
||||
m-632 -37 c1 -8 -4 -23 -12 -34 -15 -19 -15 -18 -16 18 0 27 4 37 13 35 6 -3
|
||||
13 -11 15 -19z m212 -73 c56 -10 65 -15 68 -35 4 -23 2 -23 -35 -17 -21 4 -57
|
||||
10 -80 13 -24 3 -43 9 -43 13 0 5 -3 15 -6 24 -7 19 6 20 96 2z m778 -13 c2
|
||||
-16 4 -37 5 -47 1 -10 -4 -21 -10 -23 -9 -3 -13 5 -13 24 0 15 -3 38 -6 51 -4
|
||||
15 -2 22 8 22 8 0 15 -12 16 -27z m69 21 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6
|
||||
11 1 17 -2 13 -5z m-1080 -46 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12 1
|
||||
-19z m93 -8 c0 -5 -7 -10 -15 -10 -8 0 -15 5 -15 10 0 6 7 10 15 10 8 0 15 -4
|
||||
15 -10z m400 -51 c0 -28 -4 -39 -12 -37 -18 6 -19 78 -2 78 10 0 14 -13 14
|
||||
-41z m-221 25 c18 -4 31 -13 31 -21 0 -12 -8 -14 -37 -8 -44 8 -53 13 -53 26
|
||||
0 11 15 11 59 3z m-295 -10 c20 -8 21 -48 2 -63 -12 -10 -17 -8 -25 8 -13 24
|
||||
-14 61 -2 61 5 0 16 -3 25 -6z m416 -19 c0 -23 -3 -25 -27 -19 -38 9 -43 13
|
||||
-43 30 0 9 11 14 35 14 31 0 35 -3 35 -25z m317 -25 c3 -44 -2 -50 -37 -39
|
||||
-24 7 -24 7 -2 8 19 1 22 6 22 43 0 51 13 42 17 -12z m48 10 c3 -5 2 -10 -4
|
||||
-10 -5 0 -13 5 -16 10 -3 6 -2 10 4 10 5 0 13 -4 16 -10z m-665 -20 c7 0 -49
|
||||
-38 -85 -57 -29 -15 -30 -15 -14 3 9 11 19 31 23 46 6 25 8 26 38 17 18 -5 35
|
||||
-9 38 -9z m817 -106 c-14 -14 -27 11 -27 50 0 62 14 66 24 7 5 -28 6 -54 3
|
||||
-57z m298 31 c119 -23 115 -20 115 -70 l0 -43 -72 14 c-122 23 -118 21 -118
|
||||
57 0 18 -5 38 -12 45 -15 15 -9 15 87 -3z m-155 -19 c0 -2 -7 -7 -16 -10 -8
|
||||
-3 -12 -2 -9 4 6 10 25 14 25 6z m312 -68 c-9 -9 -12 -7 -12 12 0 19 3 21 12
|
||||
12 9 -9 9 -15 0 -24z m-1505 -78 c15 -12 41 -20 62 -20 35 0 37 -2 59 -58 12
|
||||
-32 22 -61 22 -64 0 -4 -20 -36 -45 -71 -81 -117 -123 -227 -179 -478 -45
|
||||
-202 -52 -358 -21 -482 6 -23 4 -24 -127 -46 -73 -13 -154 -30 -179 -38 -44
|
||||
-14 -55 -13 -220 21 -381 78 -596 167 -739 307 -82 80 -115 153 -115 250 1
|
||||
110 48 204 159 315 123 123 270 201 506 269 144 41 264 61 555 90 127 12 232
|
||||
23 233 24 2 0 15 -8 29 -19z m311 -216 c2 -22 -25 -85 -104 -242 -58 -117
|
||||
-110 -212 -115 -212 -16 0 -10 94 11 176 24 97 77 203 131 267 47 54 72 58 77
|
||||
11z m26 -181 c-31 -74 -85 -226 -104 -296 -23 -87 -26 -55 -4 50 18 88 44 157
|
||||
86 231 35 61 45 68 22 15z m-268 -940 c65 -31 148 -67 184 -80 52 -19 57 -22
|
||||
25 -17 -46 7 -279 96 -312 119 -20 14 -31 35 -19 35 1 0 57 -25 122 -57z"/>
|
||||
<path d="M7090 1674 c-52 -7 -131 -25 -175 -39 -92 -29 -208 -103 -242 -154
|
||||
-26 -39 -30 -93 -8 -111 11 -9 13 -16 5 -24 -38 -38 23 -52 265 -61 250 -10
|
||||
695 25 783 61 41 17 52 42 26 63 -14 12 -19 29 -19 61 0 93 -49 126 -278 185
|
||||
-152 39 -203 41 -357 19z m270 -74 c11 -7 6 -10 -21 -10 -20 0 -72 -3 -115 -6
|
||||
-43 -4 -76 -3 -72 1 20 21 181 32 208 15z m-427 -166 c56 -36 48 -48 -25 -40
|
||||
-34 3 -66 10 -70 14 -5 5 -4 21 2 36 12 32 31 30 93 -10z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1365 1365" width="1365" height="1365">
|
||||
<title>safari-pinned-tab-svg</title>
|
||||
<defs>
|
||||
<clipPath clipPathUnits="userSpaceOnUse" id="cp1">
|
||||
<path d="m655.2 313.1v822.23h-622.69v-822.23z"/>
|
||||
</clipPath>
|
||||
<clipPath clipPathUnits="userSpaceOnUse" id="cp2">
|
||||
<path d="m1308.84 304v828.5h-617.24v-828.5z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<style>
|
||||
.s0 { fill: none }
|
||||
.s1 { fill: #000000 }
|
||||
</style>
|
||||
<g id="surface1">
|
||||
<path class="s0" d="m1258.6 499.2c-40.8-26.1-68 13.8-64.7 68.1l-0.3 0.4c0.1-56.6-7.3-119.2-31.7-169.8-8.7-17.9-26.2-47.4-61-33.5-15.2 6.1-29 24.4-21.9 71.7 0 0 8 49.7 6.5 112.2v0.8c-9.9-172.2-47.3-224.7-100.7-221.3-17.1 3.1-40.5 10.8-32.6 63.8 0 0 8.5 55.2 11.3 99.2l0.2 2.2h-0.2c-25.2-94.9-59-96.2-83.5-92.5-22.3 3.3-46.6 27.3-34.3 74.7 38.6 148.4 31 327.2 28.2 352.9-7.9-17.6-10.3-31.4-21.3-50.7-43.9-77.1-64.8-82.8-90.4-84-25.5-1.1-53 15.2-51.2 46.3 1.9 31.1 17.1 36.3 38.7 79.6 16.9 33.8 21.7 78 55.7 158.4 28.1 66.5 101.6 139.5 235.6 130.8 108.5-3.7 270.6-43.2 242.4-302.5-7-45-1.7-82.7 1.9-121.4 5.8-60 14.1-159.4-26.7-185.5z"/>
|
||||
<path class="s0" d="m580.9 695.3c-25.7 1.7-46.4 7.7-89 85.7-10.6 19.4-12.7 33.3-20.3 50.9-3.3-25.5-14-204.2 21.9-353.4 11.4-47.5-13.3-71-35.6-73.9-24.6-3.3-58.5-1.3-81.9 94.6h-0.3l0.4-2.8c1.9-44 9.5-99.4 9.5-99.4 6.9-53.1-16.6-60.3-33.7-63.2-53.4-2.3-89.7 50.4-96.7 221.1h-0.2c-2.4-61.8 4.6-111 4.6-111 6.3-47.5-7.9-65.5-23.2-71.3-35-13.3-52 16.6-60.3 34.7-23.6 51-29.9 113.7-28.7 170.3l-0.4-0.4c2.4-54.2-25.6-93.7-65.9-66.9-40.3 26.9-30.1 126.2-23.4 186 4.5 38.6 10.4 76.2 4.1 121.4-23.5 259.7 139.3 296.1 247.8 297.8 134.1 6.1 206.3-68.3 233.3-135.4 32.5-80.9 36.6-125.3 52.8-159.3 20.8-43.8 36-49.2 37.3-80.3 1.3-31.1-26.5-46.9-52-45.3z"/>
|
||||
<g id="Clip-Path" clip-path="url(#cp1)">
|
||||
<g>
|
||||
<path fill-rule="evenodd" class="s1" d="m634.4 695.7c11.9 12 17.8 27.9 17.1 45.7-1 24.6-9.5 37.8-19.4 53.2-5.8 9-12.3 19.2-19.8 34.9-6.6 13.8-11.2 30.4-17 51.5-7.6 27.3-17 61.2-35.3 106.7-14.1 35.1-71.4 146.1-231.3 147.6q-9.8 0.1-20-0.3c-93.6-1.5-164.2-28.5-209.3-80.6-46.9-54-65.8-134.2-56.4-238.4l0.1-0.9c5.2-37.6 1.5-69.5-2.6-103.3-0.5-4.4-1-8.7-1.5-13.1-9.4-82.5-15.4-173.1 31.8-204.6 27.9-18.5 49.3-11.1 59.6-4.9 1 0.6 1.9 1.2 2.9 1.9 4.5-32.3 12.6-63.9 25.6-92.2 11.2-24.2 37.1-62.2 83.8-44.5 7.6 3 17.3 8.8 24.8 20.2 6.7-14.7 14.5-26.6 23.5-35.8 16.6-17.2 37.4-25.4 61.6-24.3l2.1 0.2c39.2 6.5 55.9 35 49.4 84.9 0 0 0 0.3 0 0.6 20-17 40.4-16.9 56.1-14.8 17.2 2.2 32.8 12.1 42.8 27.2 8.6 13 17.1 35.8 8.7 70.7-22.3 92.9-26.1 198.5-25.2 268.8 36.3-61.5 59.9-74.1 93.2-76.2 20.8-1.3 41.3 6 54.8 19.8zm-316.9-329.4c-35.1 36.2-49.7 148.5-43.4 333.7 19.5-3 39.5-5.2 59.6-6.5 4.7-91.2 13.3-153.7 23.7-197q0-0.5 0-0.9c2-44.4 9.3-98.9 9.7-101.2 4.7-36.3-5.7-39.2-17-41.1-13.2-0.3-23.5 3.8-32.5 13zm-124.6 49.5c-50 108.3-16.2 277.2-8.5 303.8 16.1-4.8 33.8-9.2 52.4-13-2.1-57.7-2.3-108.1-0.6-151.9-2.3-62.3 4.4-111.4 4.7-113.5 1.8-13 4.2-44.5-11.1-50.3-10.9-4.1-22.8-5.7-36.9 24.8zm407.9 357.3c8.9-13.8 12.6-19.5 13.2-33.3 0.2-6.7-1.6-12-5.9-16.3-5.9-6.1-16-9.4-26.1-8.8-17.3 1.2-33.6 2.2-73.8 75.9-5.6 10.4-8.5 18.9-11.8 28.6-2.1 6.3-4.4 13.2-7.7 20.8-0.1 0.3-0.2 0.6-0.4 0.8-1.4 3.3-3 6.8-4.8 10.4-17.6 34.2-46.2 53.4-76.5 51.6-10.4-0.7-18.2-9.9-17.6-20.6 0.6-10.7 9.6-18.7 19.8-18.2 18 1.1 33.1-15.3 41.2-31.1 0.7-1.4 1.3-2.8 2-4.2-4-41.1-11.8-210.7 22.9-354.8 3.9-16.5 2.8-30.1-3.3-39.3-5.6-8.4-13.4-10.3-16.5-10.7-11.2-1.5-19.3-0.9-27.8 6.4-20.5 17.8-46.8 77.8-56.4 261.8 5.8 0 11.6 0 17.3 0.2 10.3 0.3 18.5 9.2 18.2 19.9-0.3 10.7-8.8 19.1-19.2 18.9-95.8-2.8-204.4 22.8-250.2 47.9-2.8 1.5-5.7 2.3-8.6 2.3-6.8 0.1-13.4-3.7-16.8-10.3-4.8-9.5-1.4-21.2 7.8-26.3 8.1-4.4 17.9-8.9 28.9-13.1-6.7-22.4-18.6-82-20.1-150.6-0.3-1.5-0.5-3-0.4-4.6 1.2-29.4-7.6-48.4-16.4-53.6-5.2-3.1-12.1-1.8-20.6 3.9-31.6 21-19.4 127.4-14.9 167.4 0.5 4.3 1 8.6 1.5 12.9 4.1 34.7 8.4 70.6 2.6 113-8.3 92.7 7.5 162.9 47 208.5 38.4 44.2 98 66.3 182.4 67.6 151.5 7.1 203.3-92.6 215.7-123.3 17.4-43.4 26.5-76.2 33.8-102.5 6.4-22.9 11.4-41 19.5-58 8.5-17.9 16.1-29.7 22.2-39.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Clip-Path" clip-path="url(#cp2)">
|
||||
<g>
|
||||
<path fill-rule="evenodd" class="s1" d="m1301.9 802.9l0.1 0.9c11.3 104-6.3 184.6-52.2 239.5-44.1 52.8-114.2 81.3-208.3 84.5q-10.2 0.7-19.9 0.8c-159.5 1.6-218.8-108.4-233.5-143.2-19.1-45.1-29.1-78.9-37.2-106-6.2-21-11.1-37.5-18-51.2-7.7-15.5-14.4-25.6-20.4-34.5-10.1-15.1-18.8-28.2-20.3-52.8-1.1-17.7 4.5-33.6 16.2-45.9 13.3-14 33.8-21.8 54.5-20.9 33.3 1.5 57.1 13.7 94.5 74.5-0.3-70.4-6-175.9-30-268.4-9-34.8-0.9-57.7 7.5-70.8 9.7-15.3 25.1-25.5 42.2-28.1 15.7-2.3 36.2-2.9 56.4 13.8 0-0.2 0-0.4 0-0.4-7.4-49.9 8.8-78.8 47.9-86l2.1-0.3c24.1-1.6 45 6.3 62 23.2 9.1 9 17.1 20.7 24.1 35.3 7.4-11.6 16.9-17.6 24.5-20.6 46.4-18.7 72.9 18.9 84.5 42.9 13.5 28 22.1 59.5 27.3 91.6 0.9-0.6 1.8-1.3 2.8-1.9 10.2-6.3 31.5-14.3 59.7 3.8 47.8 30.6 43.4 121.3 35.5 203.9-0.4 4.4-0.8 8.7-1.3 13.1-3.4 33.9-6.6 65.9-0.8 103.3zm-197.6-256.5c2.5 43.7 3.2 94.2 2.1 151.9 18.7 3.5 36.4 7.5 52.7 12 7.2-26.7 37.9-196.3-14-303.7-14.6-30.1-26.5-28.4-37.4-24.1-15.1 6.1-12.2 37.5-10.2 50.7 0.4 1.9 8 50.9 6.8 113.2zm-117.5-199.2c-11.4 2.2-21.6 5.2-16.3 41.6 0.4 2.1 8.8 56.4 11.5 100.8q0 0.5 0 0.9c11.2 43.2 21 105.4 27.2 196.6 20.2 0.9 40.2 2.7 59.7 5.3 3-185.3-13.5-297.2-49.3-332.8-9.1-9.1-19.5-13-32.7-12.4zm278.5 348.5c0.5-4.3 0.9-8.6 1.3-13 3.9-40.1 14.1-146.6-17.9-167-8.6-5.5-15.5-6.7-20.6-3.5-8.7 5.4-17.2 24.5-15.4 53.9 0.1 1.5 0 3.1-0.3 4.6-0.4 68.5-11.2 128.4-17.5 150.9 11.1 4.1 20.9 8.3 29.1 12.6 9.3 4.9 13 16.6 8.3 26.2-3.3 6.7-9.8 10.6-16.6 10.6-2.9 0-5.9-0.6-8.6-2.1-46.2-24.3-155.4-47.7-251-43.2-10.5 0.3-19.2-7.7-19.6-18.5-0.5-10.7 7.5-19.8 17.9-20.2 5.6-0.3 11.4-0.5 17.2-0.6-12.8-183.8-40.2-243.3-61-260.7-8.6-7.1-16.8-7.6-27.9-5.9-3.2 0.5-10.9 2.5-16.4 11.1-5.9 9.3-6.8 22.9-2.5 39.3 37.3 143.5 32.4 313.2 29.2 354.3 0.7 1.4 1.3 2.7 2.1 4.2 8.4 15.6 23.7 31.7 41.7 30.3 10.3-0.7 19.3 7.2 20.1 17.9 0.8 10.6-6.9 20-17.2 20.8-30.3 2.5-59.3-16.3-77.4-50.1-2-3.6-3.6-7-5.1-10.3-0.1-0.2-0.2-0.5-0.3-0.7-3.5-7.5-5.9-14.5-8.2-20.8-3.5-9.6-6.4-18-12.3-28.3-41.6-72.9-57.9-73.7-75.1-74.5-10.2-0.5-20.2 3.1-26.1 9.3-4.1 4.3-5.9 9.7-5.5 16.4 0.8 13.8 4.6 19.4 13.7 33.1 6.3 9.4 14.1 21 23 38.7 8.3 16.9 13.7 34.8 20.5 57.7 7.7 26.2 17.4 58.7 35.6 101.8 12.9 30.5 66.7 129.1 217.3 119.2 84.9-2.9 144.2-26.2 181.7-71.1 38.7-46.3 53.2-116.8 43.3-209.4-6.6-42.3-3-78.2 0.5-113z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="s1" d="m739.8 434.2c-3 0-6.1-0.6-9-2-10.5-5-15.2-18-10.3-28.9 15.7-35.6 38.8-68.4 66.6-94.9 8.5-8.1 21.9-7.6 29.7 1.3 7.9 8.9 7.4 22.7-1.2 30.8-23.7 22.7-43.4 50.6-56.8 81-3.6 7.9-11.1 12.6-19 12.7z"/>
|
||||
<path class="s1" d="m668.8 421.6c-10.9 0.1-20.3-8.5-21.2-20-4-51.4-4.2-103.5-0.4-154.8 0.9-12 11.1-21 22.6-20.1 11.6 0.9 20.3 11.3 19.4 23.3-3.6 49.1-3.4 98.9 0.4 148 1 12-7.7 22.5-19.3 23.5-0.5 0-1 0-1.5 0z"/>
|
||||
<path class="s1" d="m596.2 435.1c-9.4 0.1-18.2-6.5-20.6-16.4-8.9-36.3-25.9-70.8-48.9-99.7-7.4-9.3-6.1-23 2.8-30.7 9-7.6 22.3-6.3 29.7 3 26.9 33.8 46.7 74.2 57.2 116.7 2.9 11.6-4 23.5-15.2 26.5-1.8 0.4-3.4 0.6-5.1 0.7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 6.9 KiB |
@@ -4,18 +4,17 @@ import TreeNode from "./TreeNode";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ExplorerTreeProps {
|
||||
files: string[];
|
||||
files: string[] | null;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
function ExplorerTree({ files, defaultOpen = false }: ExplorerTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<div className="text-sm text-gray-400 pt-4">
|
||||
{t(I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE)}
|
||||
</div>
|
||||
);
|
||||
if (!files?.length) {
|
||||
const message = !files
|
||||
? I18nKey.EXPLORER$LOADING_WORKSPACE_MESSAGE
|
||||
: I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE;
|
||||
return <div className="text-sm text-gray-400 pt-4">{t(message)}</div>;
|
||||
}
|
||||
return (
|
||||
<div className="w-full h-full pt-[4px]">
|
||||
|
||||
@@ -90,7 +90,7 @@ function ExplorerActions({
|
||||
function FileExplorer() {
|
||||
const [isHidden, setIsHidden] = React.useState(false);
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
const [files, setFiles] = React.useState<string[]>([]);
|
||||
const [files, setFiles] = React.useState<string[] | null>(null);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -79,12 +79,10 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
|
||||
setIsOpen((prev) => !prev);
|
||||
} else {
|
||||
let newFileState = fileStates.find((f) => f.path === path);
|
||||
if (!newFileState) {
|
||||
const code = await selectFile(path);
|
||||
newFileState = { path, savedContent: code, unsavedContent: code };
|
||||
}
|
||||
const code = await selectFile(path);
|
||||
newFileState = { path, savedContent: code, unsavedContent: code };
|
||||
dispatch(addOrUpdateFileState(newFileState));
|
||||
dispatch(setCode(newFileState.unsavedContent));
|
||||
dispatch(setCode(code));
|
||||
dispatch(setActiveFilepath(path));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@ function BaseModal({
|
||||
data-testid={testID}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={title}
|
||||
isDismissable={isDismissable}
|
||||
backdrop="blur"
|
||||
hideCloseButton
|
||||
@@ -51,14 +50,14 @@ function BaseModal({
|
||||
<>
|
||||
{title && (
|
||||
<ModalHeader className="flex flex-col p-0">
|
||||
<HeaderContent title={title} subtitle={subtitle} />
|
||||
<HeaderContent maintitle={title} subtitle={subtitle} />
|
||||
</ModalHeader>
|
||||
)}
|
||||
|
||||
<ModalBody className={bodyClassName}>{children}</ModalBody>
|
||||
|
||||
{actions && actions.length > 0 && (
|
||||
<ModalFooter className="flex-col flex justify-start p-0">
|
||||
<ModalFooter className="flex-row flex justify-start p-0">
|
||||
<FooterContent actions={actions} closeModal={closeModal} />
|
||||
</ModalFooter>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
interface HeaderContentProps {
|
||||
title: string;
|
||||
maintitle: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export function HeaderContent({
|
||||
title,
|
||||
maintitle,
|
||||
subtitle = undefined,
|
||||
}: HeaderContentProps) {
|
||||
return (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<h3>{maintitle}</h3>
|
||||
{subtitle && (
|
||||
<span className="text-neutral-400 text-sm font-light">{subtitle}</span>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Feedback, sendFeedback } from "#/services/feedbackService";
|
||||
import toast from "#/utils/toast";
|
||||
import { getToken } from "#/services/auth";
|
||||
import Session from "#/services/session";
|
||||
import { removeApiKey } from "#/utils/utils";
|
||||
import { removeApiKey, removeUnwantedKeys } from "#/utils/utils";
|
||||
|
||||
const isEmailValid = (email: string) => {
|
||||
// Regular expression to validate email format
|
||||
@@ -95,7 +95,7 @@ function FeedbackModal({
|
||||
email,
|
||||
permissions,
|
||||
token: getToken(),
|
||||
trajectory: removeApiKey(Session._history),
|
||||
trajectory: removeApiKey(removeUnwantedKeys(Session._history)),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("ModelSelector", () => {
|
||||
const onModelChange = vi.fn();
|
||||
render(<ModelSelector models={models} onModelChange={onModelChange} />);
|
||||
|
||||
const selector = screen.getByLabelText("Provider");
|
||||
const selector = screen.getByLabelText("LLM Provider");
|
||||
expect(selector).toBeInTheDocument();
|
||||
|
||||
await user.click(selector);
|
||||
@@ -45,10 +45,10 @@ describe("ModelSelector", () => {
|
||||
const onModelChange = vi.fn();
|
||||
render(<ModelSelector models={models} onModelChange={onModelChange} />);
|
||||
|
||||
const modelSelector = screen.getByLabelText("Model");
|
||||
const modelSelector = screen.getByLabelText("LLM Model");
|
||||
expect(modelSelector).toBeDisabled();
|
||||
|
||||
const providerSelector = screen.getByLabelText("Provider");
|
||||
const providerSelector = screen.getByLabelText("LLM Provider");
|
||||
await user.click(providerSelector);
|
||||
|
||||
const vertexAI = screen.getByText("VertexAI");
|
||||
@@ -62,13 +62,13 @@ describe("ModelSelector", () => {
|
||||
const onModelChange = vi.fn();
|
||||
render(<ModelSelector models={models} onModelChange={onModelChange} />);
|
||||
|
||||
const providerSelector = screen.getByLabelText("Provider");
|
||||
const providerSelector = screen.getByLabelText("LLM Provider");
|
||||
await user.click(providerSelector);
|
||||
|
||||
const azureProvider = screen.getByText("Azure");
|
||||
await user.click(azureProvider);
|
||||
|
||||
const modelSelector = screen.getByLabelText("Model");
|
||||
const modelSelector = screen.getByLabelText("LLM Model");
|
||||
await user.click(modelSelector);
|
||||
|
||||
expect(screen.getByText("ada")).toBeInTheDocument();
|
||||
@@ -84,42 +84,13 @@ describe("ModelSelector", () => {
|
||||
expect(screen.getByText("chat-bison-32k")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the actual litellm model ID as the user is making the selections", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onModelChange = vi.fn();
|
||||
render(<ModelSelector models={models} onModelChange={onModelChange} />);
|
||||
|
||||
const id = screen.getByTestId("model-id");
|
||||
const providerSelector = screen.getByLabelText("Provider");
|
||||
const modelSelector = screen.getByLabelText("Model");
|
||||
|
||||
expect(id).toHaveTextContent("No model selected");
|
||||
|
||||
await user.click(providerSelector);
|
||||
await user.click(screen.getByText("Azure"));
|
||||
|
||||
expect(id).toHaveTextContent("azure/");
|
||||
|
||||
await user.click(modelSelector);
|
||||
await user.click(screen.getByText("ada"));
|
||||
expect(id).toHaveTextContent("azure/ada");
|
||||
|
||||
await user.click(providerSelector);
|
||||
await user.click(screen.getByText("cohere"));
|
||||
expect(id).toHaveTextContent("cohere.");
|
||||
|
||||
await user.click(modelSelector);
|
||||
await user.click(screen.getByText("command-r-v1:0"));
|
||||
expect(id).toHaveTextContent("cohere.command-r-v1:0");
|
||||
});
|
||||
|
||||
it("should call onModelChange when the model is changed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onModelChange = vi.fn();
|
||||
render(<ModelSelector models={models} onModelChange={onModelChange} />);
|
||||
|
||||
const providerSelector = screen.getByLabelText("Provider");
|
||||
const modelSelector = screen.getByLabelText("Model");
|
||||
const providerSelector = screen.getByLabelText("LLM Provider");
|
||||
const modelSelector = screen.getByLabelText("LLM Model");
|
||||
|
||||
await user.click(providerSelector);
|
||||
await user.click(screen.getByText("Azure"));
|
||||
@@ -146,29 +117,6 @@ describe("ModelSelector", () => {
|
||||
expect(onModelChange).toHaveBeenCalledWith("cohere.command-r-v1:0");
|
||||
});
|
||||
|
||||
it("should clear the model ID when the provider is cleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onModelChange = vi.fn();
|
||||
render(<ModelSelector models={models} onModelChange={onModelChange} />);
|
||||
|
||||
const providerSelector = screen.getByLabelText("Provider");
|
||||
const modelSelector = screen.getByLabelText("Model");
|
||||
|
||||
await user.click(providerSelector);
|
||||
await user.click(screen.getByText("Azure"));
|
||||
|
||||
await user.click(modelSelector);
|
||||
await user.click(screen.getByText("ada"));
|
||||
|
||||
expect(screen.getByTestId("model-id")).toHaveTextContent("azure/ada");
|
||||
|
||||
await user.clear(providerSelector);
|
||||
|
||||
expect(screen.getByTestId("model-id")).toHaveTextContent(
|
||||
"No model selected",
|
||||
);
|
||||
});
|
||||
|
||||
it("should have a default value if passed", async () => {
|
||||
const onModelChange = vi.fn();
|
||||
render(
|
||||
@@ -179,9 +127,8 @@ describe("ModelSelector", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("model-id")).toHaveTextContent("azure/ada");
|
||||
expect(screen.getByLabelText("Provider")).toHaveValue("Azure");
|
||||
expect(screen.getByLabelText("Model")).toHaveValue("ada");
|
||||
expect(screen.getByLabelText("LLM Provider")).toHaveValue("Azure");
|
||||
expect(screen.getByLabelText("LLM Model")).toHaveValue("ada");
|
||||
});
|
||||
|
||||
it.todo("should disable provider if isDisabled is true");
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ModelSelector({
|
||||
onModelChange,
|
||||
defaultModel,
|
||||
}: ModelSelectorProps) {
|
||||
const [litellmId, setLitellmId] = React.useState<string | null>(null);
|
||||
const [, setLitellmId] = React.useState<string | null>(null);
|
||||
const [selectedProvider, setSelectedProvider] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -48,7 +48,11 @@ export function ModelSelector({
|
||||
|
||||
const handleChangeModel = (model: string) => {
|
||||
const separator = models[selectedProvider || ""]?.separator || "";
|
||||
const fullModel = selectedProvider + separator + model;
|
||||
let fullModel = selectedProvider + separator + model;
|
||||
if (selectedProvider === "openai") {
|
||||
// LiteLLM lists OpenAI models without the openai/ prefix
|
||||
fullModel = model;
|
||||
}
|
||||
setLitellmId(fullModel);
|
||||
onModelChange(fullModel);
|
||||
setSelectedModel(model);
|
||||
@@ -61,14 +65,10 @@ export function ModelSelector({
|
||||
|
||||
return (
|
||||
<div data-testid="model-selector" className="flex flex-col gap-2">
|
||||
<span className="text-center italic text-gray-500" data-testid="model-id">
|
||||
{litellmId?.replace("other", "") || "No model selected"}
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-row gap-3">
|
||||
<Autocomplete
|
||||
isDisabled={isDisabled}
|
||||
label="Provider"
|
||||
label="LLM Provider"
|
||||
placeholder="Select a provider"
|
||||
isClearable={false}
|
||||
onSelectionChange={(e) => {
|
||||
@@ -99,7 +99,7 @@ export function ModelSelector({
|
||||
</Autocomplete>
|
||||
|
||||
<Autocomplete
|
||||
label="Model"
|
||||
label="LLM Model"
|
||||
placeholder="Select a model"
|
||||
onSelectionChange={(e) => {
|
||||
if (e?.toString()) handleChangeModel(e.toString());
|
||||
|
||||
@@ -6,8 +6,7 @@ import { Settings } from "#/services/settings";
|
||||
import SettingsForm from "./SettingsForm";
|
||||
|
||||
const onModelChangeMock = vi.fn();
|
||||
const onCustomModelChangeMock = vi.fn();
|
||||
const onModelTypeChangeMock = vi.fn();
|
||||
const onBaseURLChangeMock = vi.fn();
|
||||
const onAgentChangeMock = vi.fn();
|
||||
const onLanguageChangeMock = vi.fn();
|
||||
const onAPIKeyChangeMock = vi.fn();
|
||||
@@ -21,21 +20,19 @@ const renderSettingsForm = (settings?: Settings) => {
|
||||
settings={
|
||||
settings || {
|
||||
LLM_MODEL: "gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
AGENT: "agent1",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "analyzer1",
|
||||
LLM_BASE_URL: "",
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
}
|
||||
}
|
||||
models={["gpt-4o", "gpt-3.5-turbo", "azure/ada"]}
|
||||
agents={["agent1", "agent2", "agent3"]}
|
||||
securityAnalyzers={["analyzer1", "analyzer2", "analyzer3"]}
|
||||
onModelChange={onModelChangeMock}
|
||||
onCustomModelChange={onCustomModelChangeMock}
|
||||
onModelTypeChange={onModelTypeChangeMock}
|
||||
onBaseURLChange={onBaseURLChangeMock}
|
||||
onAgentChange={onAgentChangeMock}
|
||||
onLanguageChange={onLanguageChangeMock}
|
||||
onAPIKeyChange={onAPIKeyChangeMock}
|
||||
@@ -49,50 +46,90 @@ describe("SettingsForm", () => {
|
||||
it("should display the first values in the array by default", () => {
|
||||
renderSettingsForm();
|
||||
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
const agentInput = screen.getByRole("combobox", { name: "agent" });
|
||||
const providerInput = screen.getByRole("combobox", {
|
||||
name: "LLM Provider",
|
||||
});
|
||||
const modelInput = screen.getByRole("combobox", { name: "LLM Model" });
|
||||
const languageInput = screen.getByRole("combobox", { name: "language" });
|
||||
const apiKeyInput = screen.getByTestId("apikey");
|
||||
const confirmationModeInput = screen.getByTestId("confirmationmode");
|
||||
const securityAnalyzerInput = screen.getByRole("combobox", {
|
||||
name: "securityanalyzer",
|
||||
});
|
||||
|
||||
expect(providerInput).toHaveValue("OpenAI");
|
||||
expect(modelInput).toHaveValue("gpt-4o");
|
||||
expect(agentInput).toHaveValue("agent1");
|
||||
expect(languageInput).toHaveValue("English");
|
||||
expect(apiKeyInput).toHaveValue("sk-...");
|
||||
expect(confirmationModeInput).toHaveAttribute("data-selected", "true");
|
||||
expect(securityAnalyzerInput).toHaveValue("analyzer1");
|
||||
});
|
||||
|
||||
it("should display the existing values if they are present", () => {
|
||||
renderSettingsForm({
|
||||
LLM_MODEL: "gpt-3.5-turbo",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
AGENT: "agent2",
|
||||
LANGUAGE: "es",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "analyzer2",
|
||||
LLM_BASE_URL: "",
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
});
|
||||
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
const agentInput = screen.getByRole("combobox", { name: "agent" });
|
||||
const languageInput = screen.getByRole("combobox", { name: "language" });
|
||||
const securityAnalyzerInput = screen.getByRole("combobox", {
|
||||
name: "securityanalyzer",
|
||||
const providerInput = screen.getByRole("combobox", {
|
||||
name: "LLM Provider",
|
||||
});
|
||||
const modelInput = screen.getByRole("combobox", { name: "LLM Model" });
|
||||
const languageInput = screen.getByRole("combobox", { name: "language" });
|
||||
|
||||
expect(providerInput).toHaveValue("OpenAI");
|
||||
expect(modelInput).toHaveValue("gpt-3.5-turbo");
|
||||
expect(agentInput).toHaveValue("agent2");
|
||||
expect(languageInput).toHaveValue("Español");
|
||||
expect(securityAnalyzerInput).toHaveValue("analyzer2");
|
||||
});
|
||||
|
||||
it("should show advanced settings by default if advanced settings are in use", () => {
|
||||
renderSettingsForm({
|
||||
LLM_MODEL: "gpt-3.5-turbo",
|
||||
AGENT: "agent2",
|
||||
LANGUAGE: "es",
|
||||
LLM_API_KEY: "sk-...",
|
||||
LLM_BASE_URL: "",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "",
|
||||
});
|
||||
|
||||
const customModelInput = screen.getByTestId("custom-model-input");
|
||||
expect(customModelInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show advanced settings if using a custom model", () => {
|
||||
renderSettingsForm({
|
||||
LLM_MODEL: "bagel",
|
||||
AGENT: "agent2",
|
||||
LANGUAGE: "es",
|
||||
LLM_API_KEY: "sk-...",
|
||||
LLM_BASE_URL: "",
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
});
|
||||
|
||||
const customModelInput = screen.getByTestId("custom-model-input");
|
||||
expect(customModelInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show advanced settings if button is clicked", async () => {
|
||||
renderSettingsForm({
|
||||
LLM_MODEL: "gpt-3.5-turbo",
|
||||
AGENT: "agent2",
|
||||
LANGUAGE: "es",
|
||||
LLM_API_KEY: "sk-...",
|
||||
LLM_BASE_URL: "",
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
});
|
||||
|
||||
let customModelInput = screen.queryByTestId("custom-model-input");
|
||||
expect(customModelInput).not.toBeInTheDocument();
|
||||
|
||||
const advancedToggle = screen.getByTestId("advanced-options-toggle");
|
||||
await userEvent.click(advancedToggle);
|
||||
|
||||
customModelInput = screen.getByTestId("custom-model-input");
|
||||
expect(customModelInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable settings when disabled is true", () => {
|
||||
@@ -100,21 +137,19 @@ describe("SettingsForm", () => {
|
||||
<SettingsForm
|
||||
settings={{
|
||||
LLM_MODEL: "gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
AGENT: "agent1",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "analyzer1",
|
||||
LLM_BASE_URL: "",
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
}}
|
||||
models={["gpt-4o", "gpt-3.5-turbo", "azure/ada"]}
|
||||
agents={["agent1", "agent2", "agent3"]}
|
||||
securityAnalyzers={["analyzer1", "analyzer2", "analyzer3"]}
|
||||
disabled
|
||||
onModelChange={onModelChangeMock}
|
||||
onCustomModelChange={onCustomModelChangeMock}
|
||||
onModelTypeChange={onModelTypeChangeMock}
|
||||
onBaseURLChange={onBaseURLChangeMock}
|
||||
onAgentChange={onAgentChangeMock}
|
||||
onLanguageChange={onLanguageChangeMock}
|
||||
onAPIKeyChange={onAPIKeyChangeMock}
|
||||
@@ -123,21 +158,15 @@ describe("SettingsForm", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
const agentInput = screen.getByRole("combobox", { name: "agent" });
|
||||
const languageInput = screen.getByRole("combobox", { name: "language" });
|
||||
const confirmationModeInput = screen.getByTestId("confirmationmode");
|
||||
const securityAnalyzerInput = screen.getByRole("combobox", {
|
||||
name: "securityanalyzer",
|
||||
const providerInput = screen.getByRole("combobox", {
|
||||
name: "LLM Provider",
|
||||
});
|
||||
const modelInput = screen.getByRole("combobox", { name: "LLM Model" });
|
||||
const languageInput = screen.getByRole("combobox", { name: "language" });
|
||||
|
||||
expect(providerInput).toBeDisabled();
|
||||
expect(modelInput).toBeDisabled();
|
||||
expect(agentInput).toBeDisabled();
|
||||
expect(languageInput).toBeDisabled();
|
||||
expect(confirmationModeInput).toHaveAttribute("data-disabled", "true");
|
||||
expect(securityAnalyzerInput).toBeDisabled();
|
||||
});
|
||||
|
||||
describe("onChange handlers", () => {
|
||||
@@ -146,7 +175,7 @@ describe("SettingsForm", () => {
|
||||
renderSettingsForm();
|
||||
|
||||
// We need to enable the agent select
|
||||
const agentSwitch = screen.getByTestId("enableagentselect");
|
||||
const agentSwitch = screen.getByTestId("advanced-options-toggle");
|
||||
await user.click(agentSwitch);
|
||||
|
||||
const agentInput = screen.getByRole("combobox", { name: "agent" });
|
||||
@@ -201,8 +230,8 @@ describe("SettingsForm", () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsForm();
|
||||
|
||||
const customModelToggle = screen.getByTestId("custom-model-toggle");
|
||||
await user.click(customModelToggle);
|
||||
const advancedToggle = screen.getByTestId("advanced-options-toggle");
|
||||
await user.click(advancedToggle);
|
||||
|
||||
const modelSelector = screen.queryByTestId("model-selector");
|
||||
expect(modelSelector).not.toBeInTheDocument();
|
||||
@@ -215,23 +244,22 @@ describe("SettingsForm", () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsForm();
|
||||
|
||||
const customModelToggle = screen.getByTestId("custom-model-toggle");
|
||||
await user.click(customModelToggle);
|
||||
const advancedToggle = screen.getByTestId("advanced-options-toggle");
|
||||
await user.click(advancedToggle);
|
||||
|
||||
const customModelInput = screen.getByTestId("custom-model-input");
|
||||
await userEvent.clear(customModelInput);
|
||||
await userEvent.type(customModelInput, "my/custom-model");
|
||||
|
||||
expect(onCustomModelChangeMock).toHaveBeenCalledWith("my/custom-model");
|
||||
expect(onModelTypeChangeMock).toHaveBeenCalledWith("custom");
|
||||
expect(onModelChangeMock).toHaveBeenCalledWith("my/custom-model");
|
||||
});
|
||||
|
||||
it("should have custom model switched if using custom model", () => {
|
||||
it("should have advanced options switched if using advanced options", () => {
|
||||
renderWithProviders(
|
||||
<SettingsForm
|
||||
settings={{
|
||||
LLM_MODEL: "gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "CUSTOM_MODEL",
|
||||
USING_CUSTOM_MODEL: true,
|
||||
LLM_BASE_URL: "base_url",
|
||||
AGENT: "agent1",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "sk-...",
|
||||
@@ -243,8 +271,7 @@ describe("SettingsForm", () => {
|
||||
securityAnalyzers={["analyzer1", "analyzer2", "analyzer3"]}
|
||||
disabled
|
||||
onModelChange={onModelChangeMock}
|
||||
onCustomModelChange={onCustomModelChangeMock}
|
||||
onModelTypeChange={onModelTypeChangeMock}
|
||||
onBaseURLChange={onBaseURLChangeMock}
|
||||
onAgentChange={onAgentChangeMock}
|
||||
onLanguageChange={onLanguageChangeMock}
|
||||
onAPIKeyChange={onAPIKeyChangeMock}
|
||||
@@ -253,8 +280,8 @@ describe("SettingsForm", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const customModelToggle = screen.getByTestId("custom-model-toggle");
|
||||
expect(customModelToggle).toHaveAttribute("aria-checked", "true");
|
||||
const advancedToggle = screen.getByTestId("advanced-options-toggle");
|
||||
expect(advancedToggle).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,8 +17,7 @@ interface SettingsFormProps {
|
||||
disabled: boolean;
|
||||
|
||||
onModelChange: (model: string) => void;
|
||||
onCustomModelChange: (model: string) => void;
|
||||
onModelTypeChange: (type: "custom" | "default") => void;
|
||||
onBaseURLChange: (baseURL: string) => void;
|
||||
onAPIKeyChange: (apiKey: string) => void;
|
||||
onAgentChange: (agent: string) => void;
|
||||
onLanguageChange: (language: string) => void;
|
||||
@@ -33,8 +32,7 @@ function SettingsForm({
|
||||
securityAnalyzers,
|
||||
disabled,
|
||||
onModelChange,
|
||||
onCustomModelChange,
|
||||
onModelTypeChange,
|
||||
onBaseURLChange,
|
||||
onAPIKeyChange,
|
||||
onAgentChange,
|
||||
onLanguageChange,
|
||||
@@ -43,40 +41,44 @@ function SettingsForm({
|
||||
}: SettingsFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen: isVisible, onOpenChange: onVisibleChange } = useDisclosure();
|
||||
const [isAgentSelectEnabled, setIsAgentSelectEnabled] = React.useState(false);
|
||||
const [usingCustomModel, setUsingCustomModel] = React.useState(
|
||||
settings.USING_CUSTOM_MODEL,
|
||||
const advancedAlreadyInUse = React.useMemo(
|
||||
() =>
|
||||
!!settings.SECURITY_ANALYZER ||
|
||||
!!settings.CONFIRMATION_MODE ||
|
||||
!!settings.LLM_BASE_URL ||
|
||||
(!!settings.LLM_MODEL && !models.includes(settings.LLM_MODEL)),
|
||||
[],
|
||||
);
|
||||
|
||||
const changeModelType = (type: "custom" | "default") => {
|
||||
if (type === "custom") {
|
||||
setUsingCustomModel(true);
|
||||
onModelTypeChange("custom");
|
||||
} else {
|
||||
setUsingCustomModel(false);
|
||||
onModelTypeChange("default");
|
||||
}
|
||||
};
|
||||
const [enableAdvanced, setEnableAdvanced] =
|
||||
React.useState(advancedAlreadyInUse);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
data-testid="custom-model-toggle"
|
||||
aria-checked={usingCustomModel}
|
||||
isSelected={usingCustomModel}
|
||||
onValueChange={(value) => changeModelType(value ? "custom" : "default")}
|
||||
data-testid="advanced-options-toggle"
|
||||
aria-checked={enableAdvanced}
|
||||
isSelected={enableAdvanced}
|
||||
onValueChange={(value) => setEnableAdvanced(value)}
|
||||
>
|
||||
Use custom model
|
||||
Advanced Options
|
||||
</Switch>
|
||||
{usingCustomModel && (
|
||||
<Input
|
||||
data-testid="custom-model-input"
|
||||
label="Custom Model"
|
||||
onValueChange={onCustomModelChange}
|
||||
defaultValue={settings.CUSTOM_LLM_MODEL}
|
||||
/>
|
||||
{enableAdvanced && (
|
||||
<>
|
||||
<Input
|
||||
data-testid="custom-model-input"
|
||||
label="Custom Model"
|
||||
onValueChange={onModelChange}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
/>
|
||||
<Input
|
||||
data-testid="base-url-input"
|
||||
label="Base URL"
|
||||
onValueChange={onBaseURLChange}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!usingCustomModel && (
|
||||
{!enableAdvanced && (
|
||||
<ModelSelector
|
||||
isDisabled={disabled}
|
||||
models={organizeModelsAndProviders(models)}
|
||||
@@ -117,52 +119,48 @@ function SettingsForm({
|
||||
tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="agent"
|
||||
items={agents.map((agent) => ({ value: agent, label: agent }))}
|
||||
defaultKey={settings.AGENT}
|
||||
onChange={onAgentChange}
|
||||
tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
|
||||
disabled={disabled || !isAgentSelectEnabled}
|
||||
/>
|
||||
<Switch
|
||||
defaultSelected={false}
|
||||
isSelected={isAgentSelectEnabled}
|
||||
onValueChange={setIsAgentSelectEnabled}
|
||||
aria-label="enableagentselect"
|
||||
data-testid="enableagentselect"
|
||||
>
|
||||
{t(I18nKey.SETTINGS$AGENT_SELECT_ENABLED)}
|
||||
</Switch>
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="securityanalyzer"
|
||||
items={securityAnalyzers.map((securityAnalyzer) => ({
|
||||
value: securityAnalyzer,
|
||||
label: securityAnalyzer,
|
||||
}))}
|
||||
defaultKey={settings.SECURITY_ANALYZER}
|
||||
onChange={onSecurityAnalyzerChange}
|
||||
tooltip={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Switch
|
||||
aria-label="confirmationmode"
|
||||
data-testid="confirmationmode"
|
||||
defaultSelected={
|
||||
settings.CONFIRMATION_MODE || !!settings.SECURITY_ANALYZER
|
||||
}
|
||||
onValueChange={onConfirmationModeChange}
|
||||
isDisabled={disabled || !!settings.SECURITY_ANALYZER}
|
||||
isSelected={settings.CONFIRMATION_MODE}
|
||||
>
|
||||
<Tooltip
|
||||
content={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
|
||||
closeDelay={100}
|
||||
delay={500}
|
||||
{enableAdvanced && (
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="agent"
|
||||
items={agents.map((agent) => ({ value: agent, label: agent }))}
|
||||
defaultKey={settings.AGENT}
|
||||
onChange={onAgentChange}
|
||||
tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
|
||||
/>
|
||||
)}
|
||||
{enableAdvanced && (
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="securityanalyzer"
|
||||
items={securityAnalyzers.map((securityAnalyzer) => ({
|
||||
value: securityAnalyzer,
|
||||
label: securityAnalyzer,
|
||||
}))}
|
||||
defaultKey={settings.SECURITY_ANALYZER}
|
||||
onChange={onSecurityAnalyzerChange}
|
||||
tooltip={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{enableAdvanced && (
|
||||
<Switch
|
||||
aria-label="confirmationmode"
|
||||
data-testid="confirmationmode"
|
||||
defaultSelected={
|
||||
settings.CONFIRMATION_MODE || !!settings.SECURITY_ANALYZER
|
||||
}
|
||||
onValueChange={onConfirmationModeChange}
|
||||
isDisabled={disabled || !!settings.SECURITY_ANALYZER}
|
||||
isSelected={settings.CONFIRMATION_MODE}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
<Tooltip
|
||||
content={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
|
||||
closeDelay={100}
|
||||
delay={500}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import i18next from "i18next";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { Mock } from "vitest";
|
||||
import toast from "#/utils/toast";
|
||||
import {
|
||||
Settings,
|
||||
getSettings,
|
||||
@@ -15,7 +14,6 @@ import Session from "#/services/session";
|
||||
import { fetchAgents, fetchModels } from "#/services/options";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
|
||||
const toastSpy = vi.spyOn(toast, "settingsChanged");
|
||||
const i18nSpy = vi.spyOn(i18next, "changeLanguage");
|
||||
const startNewSessionSpy = vi.spyOn(Session, "startNewSession");
|
||||
vi.spyOn(Session, "isConnected").mockImplementation(() => true);
|
||||
@@ -24,18 +22,14 @@ vi.mock("#/services/settings", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("#/services/settings")>()),
|
||||
getSettings: vi.fn().mockReturnValue({
|
||||
LLM_MODEL: "gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "invariant",
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
}),
|
||||
getDefaultSettings: vi.fn().mockReturnValue({
|
||||
LLM_MODEL: "gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "",
|
||||
@@ -85,7 +79,9 @@ describe("SettingsModal", () => {
|
||||
it("should close the modal when the close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenChange = vi.fn();
|
||||
renderWithProviders(<SettingsModal isOpen onOpenChange={onOpenChange} />);
|
||||
await act(async () =>
|
||||
renderWithProviders(<SettingsModal isOpen onOpenChange={onOpenChange} />),
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole("button", {
|
||||
name: /MODAL_CLOSE_BUTTON_LABEL/i, // i18n key
|
||||
@@ -98,8 +94,7 @@ describe("SettingsModal", () => {
|
||||
it("should disabled the save button if the settings contain a missing value", async () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
(getSettings as Mock).mockReturnValueOnce({
|
||||
LLM_MODEL: "gpt-4o",
|
||||
AGENT: "",
|
||||
LLM_MODEL: "",
|
||||
});
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
@@ -113,15 +108,13 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
describe("onHandleSave", () => {
|
||||
const initialSettings: Settings = {
|
||||
const initialSettings: Partial<Settings> = {
|
||||
LLM_MODEL: "gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "invariant",
|
||||
SECURITY_ANALYZER: "",
|
||||
CONFIRMATION_MODE: false,
|
||||
};
|
||||
|
||||
it("should save the settings", async () => {
|
||||
@@ -135,8 +128,10 @@ describe("SettingsModal", () => {
|
||||
await assertModelsAndAgentsFetched();
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
const providerInput = screen.getByRole("combobox", {
|
||||
name: "LLM Provider",
|
||||
});
|
||||
const modelInput = screen.getByRole("combobox", { name: "LLM Model" });
|
||||
|
||||
await user.click(providerInput);
|
||||
const azure = screen.getByText("Azure");
|
||||
@@ -164,8 +159,10 @@ describe("SettingsModal", () => {
|
||||
);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
const providerInput = screen.getByRole("combobox", {
|
||||
name: "LLM Provider",
|
||||
});
|
||||
const modelInput = screen.getByRole("combobox", { name: "LLM Model" });
|
||||
|
||||
await user.click(providerInput);
|
||||
const openai = screen.getByText("OpenAI");
|
||||
@@ -180,32 +177,6 @@ describe("SettingsModal", () => {
|
||||
expect(startNewSessionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display a toast for every change", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenChangeMock = vi.fn();
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
|
||||
),
|
||||
);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
|
||||
await user.click(providerInput);
|
||||
const cohere = screen.getByText("cohere");
|
||||
await user.click(cohere);
|
||||
|
||||
await user.click(modelInput);
|
||||
const model3 = screen.getByText("command-r-v1:0");
|
||||
await user.click(model3);
|
||||
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(toastSpy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("should change the language", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenChangeMock = vi.fn();
|
||||
@@ -230,6 +201,10 @@ describe("SettingsModal", () => {
|
||||
it("should close the modal", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenChangeMock = vi.fn();
|
||||
(getSettings as Mock).mockReturnValueOnce({
|
||||
LLM_MODEL: "gpt-4o",
|
||||
LLM_API_KEY: "sk-...",
|
||||
});
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
|
||||
@@ -241,8 +216,10 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
const providerInput = screen.getByRole("combobox", { name: "Provider" });
|
||||
const modelInput = screen.getByRole("combobox", { name: "Model" });
|
||||
const providerInput = screen.getByRole("combobox", {
|
||||
name: "LLM Provider",
|
||||
});
|
||||
const modelInput = screen.getByRole("combobox", { name: "LLM Model" });
|
||||
|
||||
await user.click(providerInput);
|
||||
const cohere = screen.getByText("cohere");
|
||||
@@ -252,6 +229,7 @@ describe("SettingsModal", () => {
|
||||
const model3 = screen.getByText("command-r-v1:0");
|
||||
await user.click(model3);
|
||||
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||
@@ -261,16 +239,16 @@ describe("SettingsModal", () => {
|
||||
it("should reset settings to defaults when the 'reset to defaults' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenChangeMock = vi.fn();
|
||||
(getSettings as Mock).mockReturnValueOnce({
|
||||
LLM_MODEL: "gpt-4o",
|
||||
SECURITY_ANALYZER: "fakeanalyzer",
|
||||
});
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
|
||||
),
|
||||
);
|
||||
|
||||
// We need to enable the agent select first
|
||||
const agentSwitch = screen.getByTestId("enableagentselect");
|
||||
await user.click(agentSwitch);
|
||||
|
||||
const resetButton = screen.getByRole("button", {
|
||||
name: /MODAL_RESET_BUTTON_LABEL/i,
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
Settings,
|
||||
getSettings,
|
||||
getDefaultSettings,
|
||||
getSettingsDifference,
|
||||
settingsAreUpToDate,
|
||||
maybeMigrateSettings,
|
||||
saveSettings,
|
||||
@@ -31,7 +30,7 @@ interface SettingsProps {
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
const REQUIRED_SETTINGS = ["LLM_MODEL", "AGENT"];
|
||||
const REQUIRED_SETTINGS = ["LLM_MODEL"];
|
||||
|
||||
function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -83,17 +82,10 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCustomModelChange = (model: string) => {
|
||||
const handleBaseURLChange = (baseURL: string) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
CUSTOM_LLM_MODEL: model,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleModelTypeChange = (type: "custom" | "default") => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
USING_CUSTOM_MODEL: type === "custom",
|
||||
LLM_BASE_URL: baseURL,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -131,28 +123,17 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
};
|
||||
|
||||
const handleSaveSettings = () => {
|
||||
const updatedSettings = getSettingsDifference(settings);
|
||||
saveSettings(settings);
|
||||
i18next.changeLanguage(settings.LANGUAGE);
|
||||
Session.startNewSession();
|
||||
|
||||
const sensitiveKeys = ["LLM_API_KEY"];
|
||||
|
||||
Object.entries(updatedSettings).forEach(([key, value]) => {
|
||||
if (!sensitiveKeys.includes(key)) {
|
||||
toast.settingsChanged(`${key} set to "${value}"`);
|
||||
} else {
|
||||
toast.settingsChanged(`${key} has been updated securely.`);
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
`API_KEY_${settings.LLM_MODEL || models[0]}`,
|
||||
settings.LLM_API_KEY,
|
||||
);
|
||||
};
|
||||
|
||||
let subtitle = t(I18nKey.CONFIGURATION$MODAL_SUB_TITLE);
|
||||
let subtitle = "";
|
||||
if (loading) {
|
||||
subtitle = t(I18nKey.CONFIGURATION$AGENT_LOADING);
|
||||
} else if (agentIsRunning) {
|
||||
@@ -171,30 +152,34 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
title={t(I18nKey.CONFIGURATION$MODAL_TITLE)}
|
||||
isDismissable={settingsAreUpToDate()}
|
||||
subtitle={subtitle}
|
||||
actions={[
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
|
||||
action: handleSaveSettings,
|
||||
isDisabled: saveIsDisabled,
|
||||
closeAfterAction: true,
|
||||
className: "bg-primary rounded-lg",
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_RESET_BUTTON_LABEL),
|
||||
action: handleResetSettings,
|
||||
closeAfterAction: false,
|
||||
className: "bg-neutral-500 rounded-lg",
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL),
|
||||
action: () => {
|
||||
setSettings(getSettings()); // reset settings from any changes
|
||||
},
|
||||
isDisabled: !settingsAreUpToDate(),
|
||||
closeAfterAction: true,
|
||||
className: "bg-rose-600 rounded-lg",
|
||||
},
|
||||
]}
|
||||
actions={
|
||||
loading
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL),
|
||||
action: handleSaveSettings,
|
||||
isDisabled: saveIsDisabled,
|
||||
closeAfterAction: true,
|
||||
className: "bg-primary rounded-lg",
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_RESET_BUTTON_LABEL),
|
||||
action: handleResetSettings,
|
||||
closeAfterAction: false,
|
||||
className: "bg-neutral-500 rounded-lg",
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL),
|
||||
action: () => {
|
||||
setSettings(getSettings()); // reset settings from any changes
|
||||
},
|
||||
isDisabled: !settingsAreUpToDate(),
|
||||
closeAfterAction: true,
|
||||
className: "bg-rose-600 rounded-lg",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
{loading && <Spinner />}
|
||||
{!loading && (
|
||||
@@ -205,8 +190,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
|
||||
agents={agents}
|
||||
securityAnalyzers={securityAnalyzers}
|
||||
onModelChange={handleModelChange}
|
||||
onCustomModelChange={handleCustomModelChange}
|
||||
onModelTypeChange={handleModelTypeChange}
|
||||
onBaseURLChange={handleBaseURLChange}
|
||||
onAgentChange={handleAgentChange}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onAPIKeyChange={handleAPIKeyChange}
|
||||
|
||||
@@ -196,18 +196,18 @@
|
||||
"es": "Editor de código"
|
||||
},
|
||||
"WORKSPACE$BROWSER_TAB_LABEL": {
|
||||
"en": "Browser",
|
||||
"zh-CN": "浏览器",
|
||||
"de": "Browser",
|
||||
"ko-KR": "브라우저",
|
||||
"no": "Nettleser",
|
||||
"zh-TW": "瀏覽器",
|
||||
"it": "Browser",
|
||||
"pt": "Navegador",
|
||||
"es": "Navegador",
|
||||
"ar": "المتصفح",
|
||||
"fr": "Navigateur",
|
||||
"tr": "Tarayıcı"
|
||||
"en": "Browser (Experimental)",
|
||||
"zh-CN": "浏览器(实验性)",
|
||||
"de": "Browser (Experimentell)",
|
||||
"ko-KR": "브라우저 (실험적)",
|
||||
"no": "Nettleser (Eksperimentell)",
|
||||
"zh-TW": "瀏覽器 (實驗性)",
|
||||
"it": "Browser (Sperimentale)",
|
||||
"pt": "Navegador (Experimental)",
|
||||
"es": "Navegador (Experimental)",
|
||||
"ar": "المتصفح (تجريبي)",
|
||||
"fr": "Navigateur (Expérimental)",
|
||||
"tr": "Tarayıcı (Deneysel)"
|
||||
},
|
||||
"CONFIGURATION$OPENHANDS_WORKSPACE_DIRECTORY_INPUT_LABEL": {
|
||||
"en": "OpenHands Workspace directory",
|
||||
@@ -250,18 +250,6 @@
|
||||
"fr": "Configuration",
|
||||
"tr": "Konfigürasyon"
|
||||
},
|
||||
"CONFIGURATION$MODAL_SUB_TITLE": {
|
||||
"en": "Adjust settings to your liking",
|
||||
"zh-CN": "根据您的喜好调整设置",
|
||||
"de": "Passen Sie die Einstellungen nach Ihren Wünschen an ",
|
||||
"ko-KR": "원하는 대로 설정 조정",
|
||||
"no": "Juster innstillinger etter dine ønsker ",
|
||||
"zh-TW": "調整設定以符合您的喜好",
|
||||
"it": "Regola le impostazioni in base alle tue preferenze",
|
||||
"pt": "Ajuste as configurações de acordo com sua preferência",
|
||||
"es": "Ajusta la configuración a tu gusto",
|
||||
"tr": "Ayarları isteğinize göre ayarlayın"
|
||||
},
|
||||
"CONFIGURATION$MODEL_SELECT_LABEL": {
|
||||
"en": "Model",
|
||||
"zh-CN": "模型",
|
||||
@@ -453,6 +441,11 @@
|
||||
"zh-CN": "工作区没有文件",
|
||||
"de": "Keine Dateien im Arbeitsbereich"
|
||||
},
|
||||
"EXPLORER$LOADING_WORKSPACE_MESSAGE": {
|
||||
"en": "Loading workspace...",
|
||||
"zh-CN": "正在加载工作区...",
|
||||
"de": "Arbeitsbereich wird geladen..."
|
||||
},
|
||||
"EXPLORER$REFRESH_ERROR_MESSAGE": {
|
||||
"en": "Error refreshing workspace",
|
||||
"zh-CN": "工作区刷新错误",
|
||||
|
||||
@@ -19,8 +19,7 @@ describe("startNewSession", () => {
|
||||
it("Should start a new session with the current settings", () => {
|
||||
const settings: Settings = {
|
||||
LLM_MODEL: "llm_value",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
LLM_BASE_URL: "base_url",
|
||||
AGENT: "agent_value",
|
||||
LANGUAGE: "language_value",
|
||||
LLM_API_KEY: "sk-...",
|
||||
@@ -39,33 +38,4 @@ describe("startNewSession", () => {
|
||||
expect(setupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendSpy).toHaveBeenCalledWith(JSON.stringify(event));
|
||||
});
|
||||
|
||||
it("should start with the custom llm if set", () => {
|
||||
const settings: Settings = {
|
||||
LLM_MODEL: "llm_value",
|
||||
CUSTOM_LLM_MODEL: "custom_llm_value",
|
||||
USING_CUSTOM_MODEL: true,
|
||||
AGENT: "agent_value",
|
||||
LANGUAGE: "language_value",
|
||||
LLM_API_KEY: "sk-...",
|
||||
CONFIRMATION_MODE: true,
|
||||
SECURITY_ANALYZER: "analyzer",
|
||||
};
|
||||
|
||||
const event = {
|
||||
action: ActionType.INIT,
|
||||
args: settings,
|
||||
};
|
||||
|
||||
saveSettings(settings);
|
||||
Session.startNewSession();
|
||||
|
||||
expect(setupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(sendSpy).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
...event,
|
||||
args: { ...settings, LLM_MODEL: "custom_llm_value" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,9 +50,6 @@ class Session {
|
||||
action: ActionType.INIT,
|
||||
args: {
|
||||
...settings,
|
||||
LLM_MODEL: settings.USING_CUSTOM_MODEL
|
||||
? settings.CUSTOM_LLM_MODEL
|
||||
: settings.LLM_MODEL,
|
||||
},
|
||||
};
|
||||
const eventString = JSON.stringify(event);
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
DEFAULT_SETTINGS,
|
||||
Settings,
|
||||
getSettings,
|
||||
getSettingsDifference,
|
||||
saveSettings,
|
||||
} from "./settings";
|
||||
|
||||
@@ -18,8 +17,7 @@ describe("getSettings", () => {
|
||||
it("should get the stored settings", () => {
|
||||
(localStorage.getItem as Mock)
|
||||
.mockReturnValueOnce("llm_value")
|
||||
.mockReturnValueOnce("custom_llm_value")
|
||||
.mockReturnValueOnce("true")
|
||||
.mockReturnValueOnce("base_url")
|
||||
.mockReturnValueOnce("agent_value")
|
||||
.mockReturnValueOnce("language_value")
|
||||
.mockReturnValueOnce("api_key")
|
||||
@@ -30,8 +28,7 @@ describe("getSettings", () => {
|
||||
|
||||
expect(settings).toEqual({
|
||||
LLM_MODEL: "llm_value",
|
||||
CUSTOM_LLM_MODEL: "custom_llm_value",
|
||||
USING_CUSTOM_MODEL: true,
|
||||
LLM_BASE_URL: "base_url",
|
||||
AGENT: "agent_value",
|
||||
LANGUAGE: "language_value",
|
||||
LLM_API_KEY: "api_key",
|
||||
@@ -55,11 +52,10 @@ describe("getSettings", () => {
|
||||
|
||||
expect(settings).toEqual({
|
||||
LLM_MODEL: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: DEFAULT_SETTINGS.USING_CUSTOM_MODEL,
|
||||
AGENT: DEFAULT_SETTINGS.AGENT,
|
||||
LANGUAGE: DEFAULT_SETTINGS.LANGUAGE,
|
||||
LLM_API_KEY: "",
|
||||
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
});
|
||||
@@ -70,8 +66,7 @@ describe("saveSettings", () => {
|
||||
it("should save the settings", () => {
|
||||
const settings: Settings = {
|
||||
LLM_MODEL: "llm_value",
|
||||
CUSTOM_LLM_MODEL: "custom_llm_value",
|
||||
USING_CUSTOM_MODEL: true,
|
||||
LLM_BASE_URL: "base_url",
|
||||
AGENT: "agent_value",
|
||||
LANGUAGE: "language_value",
|
||||
LLM_API_KEY: "some_key",
|
||||
@@ -82,14 +77,6 @@ describe("saveSettings", () => {
|
||||
saveSettings(settings);
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("LLM_MODEL", "llm_value");
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"CUSTOM_LLM_MODEL",
|
||||
"custom_llm_value",
|
||||
);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"USING_CUSTOM_MODEL",
|
||||
"true",
|
||||
);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("AGENT", "agent_value");
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"LANGUAGE",
|
||||
@@ -110,7 +97,7 @@ describe("saveSettings", () => {
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledTimes(2);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("LLM_MODEL", "llm_value");
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("SETTINGS_VERSION", "1");
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("SETTINGS_VERSION", "2");
|
||||
});
|
||||
|
||||
it("should not save invalid settings", () => {
|
||||
@@ -135,47 +122,3 @@ describe("saveSettings", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSettingsDifference", () => {
|
||||
beforeEach(() => {
|
||||
(localStorage.getItem as Mock)
|
||||
.mockReturnValueOnce("llm_value")
|
||||
.mockReturnValueOnce("custom_llm_value")
|
||||
.mockReturnValueOnce("false")
|
||||
.mockReturnValueOnce("agent_value")
|
||||
.mockReturnValueOnce("language_value");
|
||||
});
|
||||
|
||||
it("should return updated settings", () => {
|
||||
const settings = {
|
||||
LLM_MODEL: "new_llm_value",
|
||||
CUSTOM_LLM_MODEL: "custom_llm_value",
|
||||
USING_CUSTOM_MODEL: true,
|
||||
AGENT: "new_agent_value",
|
||||
LANGUAGE: "language_value",
|
||||
};
|
||||
|
||||
const updatedSettings = getSettingsDifference(settings);
|
||||
|
||||
expect(updatedSettings).toEqual({
|
||||
USING_CUSTOM_MODEL: true,
|
||||
LLM_MODEL: "new_llm_value",
|
||||
AGENT: "new_agent_value",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not handle invalid settings", () => {
|
||||
const settings = {
|
||||
LLM_MODEL: "new_llm_value",
|
||||
AGENT: "new_agent_value",
|
||||
INVALID: "invalid_value",
|
||||
};
|
||||
|
||||
const updatedSettings = getSettingsDifference(settings);
|
||||
|
||||
expect(updatedSettings).toEqual({
|
||||
LLM_MODEL: "new_llm_value",
|
||||
AGENT: "new_agent_value",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
const LATEST_SETTINGS_VERSION = 1;
|
||||
const LATEST_SETTINGS_VERSION = 2;
|
||||
|
||||
export type Settings = {
|
||||
LLM_MODEL: string;
|
||||
CUSTOM_LLM_MODEL: string;
|
||||
USING_CUSTOM_MODEL: boolean;
|
||||
LLM_BASE_URL: string;
|
||||
AGENT: string;
|
||||
LANGUAGE: string;
|
||||
LLM_API_KEY: string;
|
||||
@@ -11,12 +10,9 @@ export type Settings = {
|
||||
SECURITY_ANALYZER: string;
|
||||
};
|
||||
|
||||
type SettingsInput = Settings[keyof Settings];
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_MODEL: "openai/gpt-4o",
|
||||
CUSTOM_LLM_MODEL: "",
|
||||
USING_CUSTOM_MODEL: false,
|
||||
LLM_MODEL: "gpt-4o",
|
||||
LLM_BASE_URL: "",
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY: "",
|
||||
@@ -46,6 +42,14 @@ export const maybeMigrateSettings = () => {
|
||||
if (currentVersion < 1) {
|
||||
localStorage.setItem("AGENT", DEFAULT_SETTINGS.AGENT);
|
||||
}
|
||||
if (currentVersion < 2) {
|
||||
const customModel = localStorage.getItem("CUSTOM_LLM_MODEL");
|
||||
if (customModel) {
|
||||
localStorage.setItem("LLM_MODEL", customModel);
|
||||
}
|
||||
localStorage.removeItem("CUSTOM_LLM_MODEL");
|
||||
localStorage.removeItem("USING_CUSTOM_MODEL");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -58,9 +62,7 @@ export const getDefaultSettings = (): Settings => DEFAULT_SETTINGS;
|
||||
*/
|
||||
export const getSettings = (): Settings => {
|
||||
const model = localStorage.getItem("LLM_MODEL");
|
||||
const customModel = localStorage.getItem("CUSTOM_LLM_MODEL");
|
||||
const usingCustomModel =
|
||||
localStorage.getItem("USING_CUSTOM_MODEL") === "true";
|
||||
const baseUrl = localStorage.getItem("LLM_BASE_URL");
|
||||
const agent = localStorage.getItem("AGENT");
|
||||
const language = localStorage.getItem("LANGUAGE");
|
||||
const apiKey = localStorage.getItem("LLM_API_KEY");
|
||||
@@ -69,8 +71,7 @@ export const getSettings = (): Settings => {
|
||||
|
||||
return {
|
||||
LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL,
|
||||
CUSTOM_LLM_MODEL: customModel || DEFAULT_SETTINGS.CUSTOM_LLM_MODEL,
|
||||
USING_CUSTOM_MODEL: usingCustomModel || DEFAULT_SETTINGS.USING_CUSTOM_MODEL,
|
||||
LLM_BASE_URL: baseUrl || DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
AGENT: agent || DEFAULT_SETTINGS.AGENT,
|
||||
LANGUAGE: language || DEFAULT_SETTINGS.LANGUAGE,
|
||||
LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY,
|
||||
@@ -93,34 +94,3 @@ export const saveSettings = (settings: Partial<Settings>) => {
|
||||
});
|
||||
localStorage.setItem("SETTINGS_VERSION", LATEST_SETTINGS_VERSION.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the difference between the current settings and the provided settings.
|
||||
* Useful for notifying the user of exact changes.
|
||||
*
|
||||
* @example
|
||||
* // Assuming the current settings are: { LLM_MODEL: "gpt-4o", AGENT: "CodeActAgent", LANGUAGE: "en" }
|
||||
* const updatedSettings = getSettingsDifference({ LLM_MODEL: "gpt-4o", AGENT: "OTHER_AGENT", LANGUAGE: "en" });
|
||||
* // updatedSettings = { AGENT: "OTHER_AGENT" }
|
||||
*
|
||||
* @param settings - the settings to compare
|
||||
* @returns the updated settings
|
||||
*/
|
||||
export const getSettingsDifference = (settings: Partial<Settings>) => {
|
||||
const currentSettings = getSettings();
|
||||
const updatedSettings: Partial<Settings> = {};
|
||||
|
||||
Object.keys(settings).forEach((key) => {
|
||||
const typedKey = key as keyof Settings;
|
||||
if (
|
||||
validKeys.includes(typedKey) &&
|
||||
settings[typedKey] !== currentSettings[typedKey]
|
||||
) {
|
||||
(updatedSettings[typedKey] as SettingsInput) = settings[
|
||||
typedKey
|
||||
] as SettingsInput;
|
||||
}
|
||||
});
|
||||
|
||||
return updatedSettings;
|
||||
};
|
||||
|
||||
@@ -58,5 +58,23 @@ describe("extractModelAndProvider", () => {
|
||||
model: "gpt-4o",
|
||||
separator: "/",
|
||||
});
|
||||
|
||||
expect(extractModelAndProvider("claude-3-5-sonnet-20240620")).toEqual({
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
separator: "/",
|
||||
});
|
||||
|
||||
expect(extractModelAndProvider("claude-3-haiku-20240307")).toEqual({
|
||||
provider: "anthropic",
|
||||
model: "claude-3-haiku-20240307",
|
||||
separator: "/",
|
||||
});
|
||||
|
||||
expect(extractModelAndProvider("claude-2.1")).toEqual({
|
||||
provider: "anthropic",
|
||||
model: "claude-2.1",
|
||||
separator: "/",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { isNumber } from "./isNumber";
|
||||
import { VERIFIED_OPENAI_MODELS } from "./verified-models";
|
||||
import {
|
||||
VERIFIED_ANTHROPIC_MODELS,
|
||||
VERIFIED_OPENAI_MODELS,
|
||||
} from "./verified-models";
|
||||
|
||||
/**
|
||||
* Checks if the split array is actually a version number.
|
||||
@@ -41,6 +44,9 @@ export const extractModelAndProvider = (model: string) => {
|
||||
if (VERIFIED_OPENAI_MODELS.includes(split[0])) {
|
||||
return { provider: "openai", model: split[0], separator: "/" };
|
||||
}
|
||||
if (VERIFIED_ANTHROPIC_MODELS.includes(split[0])) {
|
||||
return { provider: "anthropic", model: split[0], separator: "/" };
|
||||
}
|
||||
// return as model only
|
||||
return { provider: "", model, separator: "" };
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ test("organizeModelsAndProviders", () => {
|
||||
"gpt-4o",
|
||||
"together-ai-21.1b-41b",
|
||||
"gpt-3.5-turbo",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-2",
|
||||
"claude-2.1",
|
||||
"anthropic.unsafe-claude-2.1",
|
||||
];
|
||||
|
||||
const object = organizeModelsAndProviders(models);
|
||||
@@ -43,6 +48,15 @@ test("organizeModelsAndProviders", () => {
|
||||
separator: "/",
|
||||
models: ["gpt-4o", "gpt-3.5-turbo"],
|
||||
},
|
||||
anthropic: {
|
||||
separator: "/",
|
||||
models: [
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-2",
|
||||
"claude-2.1",
|
||||
],
|
||||
},
|
||||
other: {
|
||||
separator: "",
|
||||
models: ["together-ai-21.1b-41b"],
|
||||
|
||||
@@ -32,6 +32,13 @@ export const organizeModelsAndProviders = (models: string[]) => {
|
||||
provider,
|
||||
model: modelId,
|
||||
} = extractModelAndProvider(model);
|
||||
|
||||
// Ignore "anthropic" providers with a separator of "."
|
||||
// These are outdated and incompatible providers.
|
||||
if (provider === "anthropic" && separator === ".") {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = provider || "other";
|
||||
if (!object[key]) {
|
||||
object[key] = { separator, models: [] };
|
||||
|
||||
@@ -10,9 +10,50 @@ interface EventActionHistory {
|
||||
LLM_API_KEY?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
extras?: {
|
||||
open_page_urls: string[];
|
||||
active_page_index: number;
|
||||
dom_object: Record<string, unknown>;
|
||||
axtree_object: Record<string, unknown>;
|
||||
extra_element_properties: Record<string, unknown>;
|
||||
last_browser_action: string;
|
||||
last_browser_action_error: unknown;
|
||||
focused_element_bid: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const removeUnwantedKeys = (
|
||||
data: EventActionHistory[],
|
||||
): EventActionHistory[] => {
|
||||
const UNDESIRED_KEYS = [
|
||||
"open_page_urls",
|
||||
"active_page_index",
|
||||
"dom_object",
|
||||
"axtree_object",
|
||||
"extra_element_properties",
|
||||
"last_browser_action",
|
||||
"last_browser_action_error",
|
||||
"focused_element_bid",
|
||||
];
|
||||
|
||||
return data.map((item) => {
|
||||
// Create a shallow copy of item
|
||||
const newItem = { ...item };
|
||||
|
||||
// Check if extras exists and delete it from a new extras object
|
||||
if (newItem.extras) {
|
||||
const newExtras = { ...newItem.extras };
|
||||
UNDESIRED_KEYS.forEach((key) => {
|
||||
delete newExtras[key as keyof typeof newExtras];
|
||||
});
|
||||
newItem.extras = newExtras;
|
||||
}
|
||||
|
||||
return newItem;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeApiKey = (
|
||||
data: EventActionHistory[],
|
||||
): EventActionHistory[] =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Here are the list of verified models and providers that we know work well with OpenHands.
|
||||
export const VERIFIED_PROVIDERS = ["openai", "azure", "anthropic"];
|
||||
export const VERIFIED_MODELS = ["gpt-4o", "claude-3-5-sonnet-20240620-v1:0"];
|
||||
export const VERIFIED_MODELS = ["gpt-4o", "claude-3-5-sonnet-20240620"];
|
||||
|
||||
// LiteLLM does not return OpenAI models with the provider, so we list them here to set them ourselves for consistency
|
||||
// (e.g., they return `gpt-4o` instead of `openai/gpt-4o`)
|
||||
@@ -12,3 +12,16 @@ export const VERIFIED_OPENAI_MODELS = [
|
||||
"gpt-4-32k",
|
||||
"gpt-3.5-turbo",
|
||||
];
|
||||
|
||||
// LiteLLM does not return the compatible Anthropic models with the provider, so we list them here to set them ourselves
|
||||
// (e.g., they return `claude-3-5-sonnet-20240620` instead of `anthropic/claude-3-5-sonnet-20240620`)
|
||||
export const VERIFIED_ANTHROPIC_MODELS = [
|
||||
"claude-2",
|
||||
"claude-2.1",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-instant-1",
|
||||
"claude-instant-1.2",
|
||||
];
|
||||
|
||||
@@ -365,10 +365,14 @@ class AgentController:
|
||||
f'[Agent Controller {self.id}] Delegate state: {delegate_state}'
|
||||
)
|
||||
if delegate_state == AgentState.ERROR:
|
||||
# update iteration that shall be shared across agents
|
||||
self.state.iteration = self.delegate.state.iteration
|
||||
|
||||
# close the delegate upon error
|
||||
await self.delegate.close()
|
||||
self.delegate = None
|
||||
self.delegateAction = None
|
||||
|
||||
await self.report_error('Delegator agent encounters an error')
|
||||
return
|
||||
delegate_done = delegate_state in (AgentState.FINISHED, AgentState.REJECTED)
|
||||
|
||||
@@ -51,6 +51,8 @@ class LLMConfig:
|
||||
output_cost_per_token: The cost per output token. This will available in logs for the user to check.
|
||||
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.
|
||||
"""
|
||||
|
||||
model: str = 'gpt-4o'
|
||||
@@ -63,10 +65,10 @@ class LLMConfig:
|
||||
aws_access_key_id: str | None = None
|
||||
aws_secret_access_key: str | None = None
|
||||
aws_region_name: str | None = None
|
||||
num_retries: int = 10
|
||||
num_retries: int = 8
|
||||
retry_multiplier: float = 2
|
||||
retry_min_wait: int = 3
|
||||
retry_max_wait: int = 300
|
||||
retry_min_wait: int = 15
|
||||
retry_max_wait: int = 120
|
||||
timeout: int | None = None
|
||||
max_message_chars: int = 10_000 # maximum number of characters in an observation's content when sent to the llm
|
||||
temperature: float = 0
|
||||
@@ -78,6 +80,8 @@ class LLMConfig:
|
||||
output_cost_per_token: float | None = None
|
||||
ollama_base_url: str | None = None
|
||||
drop_params: bool | None = None
|
||||
disable_vision: bool | None = None
|
||||
caching_prompt: bool = False
|
||||
|
||||
def defaults_to_dict(self) -> dict:
|
||||
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
|
||||
@@ -619,7 +623,7 @@ def get_llm_config_arg(
|
||||
model = 'gpt-3.5-turbo'
|
||||
api_key = '...'
|
||||
temperature = 0.5
|
||||
num_retries = 10
|
||||
num_retries = 8
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ class LlmFileHandler(logging.FileHandler):
|
||||
self.message_counter += 1
|
||||
|
||||
|
||||
def _get_llm_file_handler(name, log_level=logging.INFO):
|
||||
def _get_llm_file_handler(name: str, log_level: int):
|
||||
# The 'delay' parameter, when set to True, postpones the opening of the log file
|
||||
# until the first log message is emitted.
|
||||
llm_file_handler = LlmFileHandler(name, delay=True)
|
||||
@@ -248,7 +248,7 @@ def _get_llm_file_handler(name, log_level=logging.INFO):
|
||||
return llm_file_handler
|
||||
|
||||
|
||||
def _setup_llm_logger(name, log_level=logging.INFO):
|
||||
def _setup_llm_logger(name: str, log_level: int):
|
||||
logger = logging.getLogger(name)
|
||||
logger.propagate = False
|
||||
logger.setLevel(log_level)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
from pydantic import BaseModel, Field, model_serializer
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class ContentType(Enum):
|
||||
TEXT = 'text'
|
||||
@@ -10,7 +13,7 @@ class ContentType(Enum):
|
||||
|
||||
|
||||
class Content(BaseModel):
|
||||
type: ContentType
|
||||
type: str
|
||||
cache_prompt: bool = False
|
||||
|
||||
@model_serializer
|
||||
@@ -19,13 +22,13 @@ class Content(BaseModel):
|
||||
|
||||
|
||||
class TextContent(Content):
|
||||
type: ContentType = ContentType.TEXT
|
||||
type: str = ContentType.TEXT.value
|
||||
text: str
|
||||
|
||||
@model_serializer
|
||||
def serialize_model(self):
|
||||
data: dict[str, str | dict[str, str]] = {
|
||||
'type': self.type.value,
|
||||
'type': self.type,
|
||||
'text': self.text,
|
||||
}
|
||||
if self.cache_prompt:
|
||||
@@ -34,14 +37,14 @@ class TextContent(Content):
|
||||
|
||||
|
||||
class ImageContent(Content):
|
||||
type: ContentType = ContentType.IMAGE_URL
|
||||
type: str = ContentType.IMAGE_URL.value
|
||||
image_urls: list[str]
|
||||
|
||||
@model_serializer
|
||||
def serialize_model(self):
|
||||
images: list[dict[str, str | dict[str, str]]] = []
|
||||
for url in self.image_urls:
|
||||
images.append({'type': self.type.value, 'image_url': {'url': url}})
|
||||
images.append({'type': self.type, 'image_url': {'url': url}})
|
||||
if self.cache_prompt and images:
|
||||
images[-1]['cache_control'] = {'type': 'ephemeral'}
|
||||
return images
|
||||
@@ -65,4 +68,52 @@ class Message(BaseModel):
|
||||
elif isinstance(item, ImageContent):
|
||||
content.extend(item.model_dump())
|
||||
|
||||
return {'role': self.role, 'content': content}
|
||||
return {'content': content, 'role': self.role}
|
||||
|
||||
|
||||
def format_messages(
|
||||
messages: Union[Message, list[Message]],
|
||||
with_images: bool,
|
||||
with_prompt_caching: bool,
|
||||
) -> list[dict]:
|
||||
if not isinstance(messages, list):
|
||||
messages = [messages]
|
||||
|
||||
if with_images or with_prompt_caching:
|
||||
return [message.model_dump() for message in messages]
|
||||
|
||||
converted_messages = []
|
||||
for message in messages:
|
||||
content_parts = []
|
||||
role = 'user'
|
||||
|
||||
if isinstance(message, str) and message:
|
||||
content_parts.append(message)
|
||||
elif isinstance(message, dict):
|
||||
role = message.get('role', 'user')
|
||||
if 'content' in message and message['content']:
|
||||
content_parts.append(message['content'])
|
||||
elif isinstance(message, Message):
|
||||
role = message.role
|
||||
for content in message.content:
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, TextContent) and item.text:
|
||||
content_parts.append(item.text)
|
||||
elif isinstance(content, TextContent) and content.text:
|
||||
content_parts.append(content.text)
|
||||
else:
|
||||
logger.error(
|
||||
f'>>> `message` is not a string, dict, or Message: {type(message)}'
|
||||
)
|
||||
|
||||
if content_parts:
|
||||
content_str = '\n'.join(content_parts)
|
||||
converted_messages.append(
|
||||
{
|
||||
'role': role,
|
||||
'content': content_str,
|
||||
}
|
||||
)
|
||||
|
||||
return converted_messages
|
||||
|
||||
@@ -3,45 +3,47 @@ from enum import Enum
|
||||
|
||||
class ConfigType(str, Enum):
|
||||
# For frontend
|
||||
LLM_CUSTOM_LLM_PROVIDER = 'LLM_CUSTOM_LLM_PROVIDER'
|
||||
LLM_DROP_PARAMS = 'LLM_DROP_PARAMS'
|
||||
LLM_MAX_INPUT_TOKENS = 'LLM_MAX_INPUT_TOKENS'
|
||||
LLM_MAX_OUTPUT_TOKENS = 'LLM_MAX_OUTPUT_TOKENS'
|
||||
LLM_TOP_P = 'LLM_TOP_P'
|
||||
LLM_TEMPERATURE = 'LLM_TEMPERATURE'
|
||||
LLM_TIMEOUT = 'LLM_TIMEOUT'
|
||||
LLM_API_KEY = 'LLM_API_KEY'
|
||||
LLM_BASE_URL = 'LLM_BASE_URL'
|
||||
AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID'
|
||||
AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY'
|
||||
AWS_REGION_NAME = 'AWS_REGION_NAME'
|
||||
WORKSPACE_BASE = 'WORKSPACE_BASE'
|
||||
WORKSPACE_MOUNT_PATH = 'WORKSPACE_MOUNT_PATH'
|
||||
WORKSPACE_MOUNT_REWRITE = 'WORKSPACE_MOUNT_REWRITE'
|
||||
WORKSPACE_MOUNT_PATH_IN_SANDBOX = 'WORKSPACE_MOUNT_PATH_IN_SANDBOX'
|
||||
CACHE_DIR = 'CACHE_DIR'
|
||||
LLM_MODEL = 'LLM_MODEL'
|
||||
CONFIRMATION_MODE = 'CONFIRMATION_MODE'
|
||||
BASE_CONTAINER_IMAGE = 'BASE_CONTAINER_IMAGE'
|
||||
RUN_AS_OPENHANDS = 'RUN_AS_OPENHANDS'
|
||||
LLM_EMBEDDING_MODEL = 'LLM_EMBEDDING_MODEL'
|
||||
LLM_EMBEDDING_BASE_URL = 'LLM_EMBEDDING_BASE_URL'
|
||||
LLM_EMBEDDING_DEPLOYMENT_NAME = 'LLM_EMBEDDING_DEPLOYMENT_NAME'
|
||||
LLM_API_VERSION = 'LLM_API_VERSION'
|
||||
LLM_NUM_RETRIES = 'LLM_NUM_RETRIES'
|
||||
LLM_RETRY_MIN_WAIT = 'LLM_RETRY_MIN_WAIT'
|
||||
LLM_RETRY_MAX_WAIT = 'LLM_RETRY_MAX_WAIT'
|
||||
AGENT_MEMORY_MAX_THREADS = 'AGENT_MEMORY_MAX_THREADS'
|
||||
AGENT_MEMORY_ENABLED = 'AGENT_MEMORY_ENABLED'
|
||||
MAX_ITERATIONS = 'MAX_ITERATIONS'
|
||||
AGENT = 'AGENT'
|
||||
E2B_API_KEY = 'E2B_API_KEY'
|
||||
SECURITY_ANALYZER = 'SECURITY_ANALYZER'
|
||||
SANDBOX_USER_ID = 'SANDBOX_USER_ID'
|
||||
SANDBOX_TIMEOUT = 'SANDBOX_TIMEOUT'
|
||||
USE_HOST_NETWORK = 'USE_HOST_NETWORK'
|
||||
DISABLE_COLOR = 'DISABLE_COLOR'
|
||||
AGENT_MEMORY_ENABLED = 'AGENT_MEMORY_ENABLED'
|
||||
AGENT_MEMORY_MAX_THREADS = 'AGENT_MEMORY_MAX_THREADS'
|
||||
AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID'
|
||||
AWS_REGION_NAME = 'AWS_REGION_NAME'
|
||||
AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY'
|
||||
BASE_CONTAINER_IMAGE = 'BASE_CONTAINER_IMAGE'
|
||||
CACHE_DIR = 'CACHE_DIR'
|
||||
CONFIRMATION_MODE = 'CONFIRMATION_MODE'
|
||||
DEBUG = 'DEBUG'
|
||||
DISABLE_COLOR = 'DISABLE_COLOR'
|
||||
E2B_API_KEY = 'E2B_API_KEY'
|
||||
FILE_UPLOADS_ALLOWED_EXTENSIONS = 'FILE_UPLOADS_ALLOWED_EXTENSIONS'
|
||||
FILE_UPLOADS_MAX_FILE_SIZE_MB = 'FILE_UPLOADS_MAX_FILE_SIZE_MB'
|
||||
FILE_UPLOADS_RESTRICT_FILE_TYPES = 'FILE_UPLOADS_RESTRICT_FILE_TYPES'
|
||||
FILE_UPLOADS_ALLOWED_EXTENSIONS = 'FILE_UPLOADS_ALLOWED_EXTENSIONS'
|
||||
LLM_API_KEY = 'LLM_API_KEY'
|
||||
LLM_API_VERSION = 'LLM_API_VERSION'
|
||||
LLM_BASE_URL = 'LLM_BASE_URL'
|
||||
LLM_CACHING_PROMPT = 'LLM_CACHING_PROMPT'
|
||||
LLM_CUSTOM_LLM_PROVIDER = 'LLM_CUSTOM_LLM_PROVIDER'
|
||||
LLM_DROP_PARAMS = 'LLM_DROP_PARAMS'
|
||||
LLM_EMBEDDING_BASE_URL = 'LLM_EMBEDDING_BASE_URL'
|
||||
LLM_EMBEDDING_DEPLOYMENT_NAME = 'LLM_EMBEDDING_DEPLOYMENT_NAME'
|
||||
LLM_EMBEDDING_MODEL = 'LLM_EMBEDDING_MODEL'
|
||||
LLM_MAX_INPUT_TOKENS = 'LLM_MAX_INPUT_TOKENS'
|
||||
LLM_MAX_OUTPUT_TOKENS = 'LLM_MAX_OUTPUT_TOKENS'
|
||||
LLM_MODEL = 'LLM_MODEL'
|
||||
LLM_NUM_RETRIES = 'LLM_NUM_RETRIES'
|
||||
LLM_RETRY_MAX_WAIT = 'LLM_RETRY_MAX_WAIT'
|
||||
LLM_RETRY_MIN_WAIT = 'LLM_RETRY_MIN_WAIT'
|
||||
LLM_TEMPERATURE = 'LLM_TEMPERATURE'
|
||||
LLM_TIMEOUT = 'LLM_TIMEOUT'
|
||||
LLM_TOP_P = 'LLM_TOP_P'
|
||||
LLM_DISABLE_VISION = 'LLM_DISABLE_VISION'
|
||||
MAX_ITERATIONS = 'MAX_ITERATIONS'
|
||||
RUN_AS_OPENHANDS = 'RUN_AS_OPENHANDS'
|
||||
SANDBOX_TIMEOUT = 'SANDBOX_TIMEOUT'
|
||||
SANDBOX_USER_ID = 'SANDBOX_USER_ID'
|
||||
SECURITY_ANALYZER = 'SECURITY_ANALYZER'
|
||||
USE_HOST_NETWORK = 'USE_HOST_NETWORK'
|
||||
WORKSPACE_BASE = 'WORKSPACE_BASE'
|
||||
WORKSPACE_MOUNT_PATH = 'WORKSPACE_MOUNT_PATH'
|
||||
WORKSPACE_MOUNT_PATH_IN_SANDBOX = 'WORKSPACE_MOUNT_PATH_IN_SANDBOX'
|
||||
WORKSPACE_MOUNT_REWRITE = 'WORKSPACE_MOUNT_REWRITE'
|
||||
|
||||
@@ -13,6 +13,10 @@ from openhands.events.action.action import (
|
||||
class CmdRunAction(Action):
|
||||
command: str
|
||||
thought: str = ''
|
||||
blocking: bool = False
|
||||
# If False, the command will be run in a non-blocking / interactive way
|
||||
# The partial command outputs will be returned as output observation.
|
||||
# If True, the command will be run for max .timeout seconds.
|
||||
keep_prompt: bool = True
|
||||
# if True, the command prompt will be kept in the command output observation
|
||||
# Example of command output:
|
||||
|
||||
@@ -49,3 +49,8 @@ class Event:
|
||||
@timeout.setter
|
||||
def timeout(self, value: int | None) -> None:
|
||||
self._timeout = value
|
||||
|
||||
# Check if .blocking is an attribute of the event
|
||||
if hasattr(self, 'blocking'):
|
||||
# .blocking needs to be set to True if .timeout is set
|
||||
self.blocking = True
|
||||
|
||||
@@ -6,7 +6,7 @@ from openhands.events.observation.observation import Observation
|
||||
|
||||
@dataclass
|
||||
class UserRejectObservation(Observation):
|
||||
"""This data class represents the result of a successful action."""
|
||||
"""This data class represents the result of a rejected action."""
|
||||
|
||||
observation: str = ObservationType.USER_REJECTED
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import copy
|
||||
import warnings
|
||||
from functools import partial
|
||||
from typing import Union
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
|
||||
@@ -14,6 +15,7 @@ from litellm.exceptions import (
|
||||
APIConnectionError,
|
||||
ContentPolicyViolationError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
OpenAIError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError,
|
||||
@@ -23,12 +25,13 @@ from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_random_exponential,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from openhands.core.exceptions import UserCancelledError
|
||||
from openhands.core.exceptions import LLMResponseError, 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, format_messages
|
||||
from openhands.core.metrics import Metrics
|
||||
|
||||
__all__ = ['LLM']
|
||||
@@ -60,12 +63,9 @@ class LLM:
|
||||
Args:
|
||||
config: The LLM configuration
|
||||
"""
|
||||
self.config = copy.deepcopy(config)
|
||||
self.metrics = metrics if metrics is not None else Metrics()
|
||||
self.cost_metric_supported = True
|
||||
self.supports_prompt_caching = (
|
||||
self.config.model in cache_prompting_supported_models
|
||||
)
|
||||
self.config = copy.deepcopy(config)
|
||||
|
||||
# Set up config attributes with default values to prevent AttributeError
|
||||
LLMConfig.set_missing_attributes(self.config)
|
||||
@@ -83,6 +83,15 @@ 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 (
|
||||
@@ -122,33 +131,58 @@ class LLM:
|
||||
top_p=self.config.top_p,
|
||||
)
|
||||
|
||||
if self.vision_is_active():
|
||||
logger.debug('LLM: model has vision enabled')
|
||||
|
||||
completion_unwrapped = self._completion
|
||||
|
||||
def attempt_on_error(retry_state):
|
||||
"""Custom attempt function for litellm completion."""
|
||||
logger.error(
|
||||
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.',
|
||||
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize retry values in the configuration.',
|
||||
exc_info=False,
|
||||
)
|
||||
return None
|
||||
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_attempt(self.config.num_retries),
|
||||
wait=wait_random_exponential(
|
||||
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=self.config.retry_min_wait,
|
||||
max=self.config.retry_max_wait,
|
||||
),
|
||||
retry=retry_if_exception_type(
|
||||
(
|
||||
RateLimitError,
|
||||
APIConnectionError,
|
||||
ServiceUnavailableError,
|
||||
InternalServerError,
|
||||
ContentPolicyViolationError,
|
||||
)
|
||||
),
|
||||
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(
|
||||
after=attempt_on_error,
|
||||
stop=stop_after_attempt(self.config.num_retries),
|
||||
reraise=True,
|
||||
retry=retry_if_exception_type(self.retry_exceptions),
|
||||
wait=custom_completion_wait,
|
||||
)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Wrapper for the litellm completion function. Logs the input and output of the completion function."""
|
||||
@@ -156,47 +190,33 @@ class LLM:
|
||||
if 'messages' in kwargs:
|
||||
messages = kwargs['messages']
|
||||
else:
|
||||
messages = args[1]
|
||||
messages = args[1] if len(args) > 1 else []
|
||||
|
||||
# log the prompt
|
||||
debug_message = ''
|
||||
for message in messages:
|
||||
content = message['content']
|
||||
# this serves to prevent empty messages and logging the messages
|
||||
debug_message = self._get_debug_message(messages)
|
||||
|
||||
if isinstance(content, list):
|
||||
for element in content:
|
||||
if isinstance(element, dict):
|
||||
if 'text' in element:
|
||||
content_str = element['text'].strip()
|
||||
elif (
|
||||
'image_url' in element and 'url' in element['image_url']
|
||||
):
|
||||
content_str = element['image_url']['url']
|
||||
else:
|
||||
content_str = str(element)
|
||||
else:
|
||||
content_str = str(element)
|
||||
|
||||
debug_message += message_separator + content_str
|
||||
else:
|
||||
content_str = str(content)
|
||||
debug_message += message_separator + content_str
|
||||
|
||||
llm_prompt_logger.debug(debug_message)
|
||||
if self.is_caching_prompt_active():
|
||||
# Anthropic-specific prompt caching
|
||||
if 'claude-3' in self.config.model:
|
||||
kwargs['extra_headers'] = {
|
||||
'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': ''}}]}
|
||||
|
||||
# log the response
|
||||
message_back = resp['choices'][0]['message']['content']
|
||||
if message_back:
|
||||
llm_response_logger.debug(message_back)
|
||||
|
||||
llm_response_logger.debug(message_back)
|
||||
|
||||
# post-process to log costs
|
||||
self._post_completion(resp)
|
||||
# post-process to log costs
|
||||
self._post_completion(resp)
|
||||
|
||||
return resp
|
||||
|
||||
@@ -220,23 +240,11 @@ class LLM:
|
||||
async_completion_unwrapped = self._async_completion
|
||||
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_attempt(self.config.num_retries),
|
||||
wait=wait_random_exponential(
|
||||
multiplier=self.config.retry_multiplier,
|
||||
min=self.config.retry_min_wait,
|
||||
max=self.config.retry_max_wait,
|
||||
),
|
||||
retry=retry_if_exception_type(
|
||||
(
|
||||
RateLimitError,
|
||||
APIConnectionError,
|
||||
ServiceUnavailableError,
|
||||
InternalServerError,
|
||||
ContentPolicyViolationError,
|
||||
)
|
||||
),
|
||||
after=attempt_on_error,
|
||||
stop=stop_after_attempt(self.config.num_retries),
|
||||
reraise=True,
|
||||
retry=retry_if_exception_type(self.retry_exceptions),
|
||||
wait=custom_completion_wait,
|
||||
)
|
||||
async def async_completion_wrapper(*args, **kwargs):
|
||||
"""Async wrapper for the litellm acompletion function."""
|
||||
@@ -244,34 +252,10 @@ class LLM:
|
||||
if 'messages' in kwargs:
|
||||
messages = kwargs['messages']
|
||||
else:
|
||||
messages = args[1]
|
||||
messages = args[1] if len(args) > 1 else []
|
||||
|
||||
# log the prompt
|
||||
debug_message = ''
|
||||
for message in messages:
|
||||
content = message['content']
|
||||
|
||||
if isinstance(content, list):
|
||||
for element in content:
|
||||
if isinstance(element, dict):
|
||||
if 'text' in element:
|
||||
content_str = element['text']
|
||||
elif (
|
||||
'image_url' in element and 'url' in element['image_url']
|
||||
):
|
||||
content_str = element['image_url']['url']
|
||||
else:
|
||||
content_str = str(element)
|
||||
else:
|
||||
content_str = str(element)
|
||||
|
||||
debug_message += message_separator + content_str
|
||||
else:
|
||||
content_str = str(content)
|
||||
|
||||
debug_message += message_separator + content_str
|
||||
|
||||
llm_prompt_logger.debug(debug_message)
|
||||
# this serves to prevent empty messages and logging the messages
|
||||
debug_message = self._get_debug_message(messages)
|
||||
|
||||
async def check_stopped():
|
||||
while True:
|
||||
@@ -287,7 +271,12 @@ class LLM:
|
||||
|
||||
try:
|
||||
# Directly call and await litellm_acompletion
|
||||
resp = await async_completion_unwrapped(*args, **kwargs)
|
||||
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:
|
||||
@@ -303,14 +292,14 @@ class LLM:
|
||||
except UserCancelledError:
|
||||
logger.info('LLM request cancelled by user.')
|
||||
raise
|
||||
except OpenAIError as e:
|
||||
logger.error(f'OpenAIError occurred:\n{e}')
|
||||
raise
|
||||
except (
|
||||
RateLimitError,
|
||||
APIConnectionError,
|
||||
ServiceUnavailableError,
|
||||
ContentPolicyViolationError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
OpenAIError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError,
|
||||
) as e:
|
||||
logger.error(f'Completion Error occurred:\n{e}')
|
||||
raise
|
||||
@@ -324,23 +313,11 @@ class LLM:
|
||||
pass
|
||||
|
||||
@retry(
|
||||
reraise=True,
|
||||
stop=stop_after_attempt(self.config.num_retries),
|
||||
wait=wait_random_exponential(
|
||||
multiplier=self.config.retry_multiplier,
|
||||
min=self.config.retry_min_wait,
|
||||
max=self.config.retry_max_wait,
|
||||
),
|
||||
retry=retry_if_exception_type(
|
||||
(
|
||||
RateLimitError,
|
||||
APIConnectionError,
|
||||
ServiceUnavailableError,
|
||||
InternalServerError,
|
||||
ContentPolicyViolationError,
|
||||
)
|
||||
),
|
||||
after=attempt_on_error,
|
||||
stop=stop_after_attempt(self.config.num_retries),
|
||||
reraise=True,
|
||||
retry=retry_if_exception_type(self.retry_exceptions),
|
||||
wait=custom_completion_wait,
|
||||
)
|
||||
async def async_acompletion_stream_wrapper(*args, **kwargs):
|
||||
"""Async wrapper for the litellm acompletion with streaming function."""
|
||||
@@ -348,7 +325,7 @@ class LLM:
|
||||
if 'messages' in kwargs:
|
||||
messages = kwargs['messages']
|
||||
else:
|
||||
messages = args[1]
|
||||
messages = args[1] if len(args) > 1 else []
|
||||
|
||||
# log the prompt
|
||||
debug_message = ''
|
||||
@@ -381,14 +358,14 @@ class LLM:
|
||||
except UserCancelledError:
|
||||
logger.info('LLM request cancelled by user.')
|
||||
raise
|
||||
except OpenAIError as e:
|
||||
logger.error(f'OpenAIError occurred:\n{e}')
|
||||
raise
|
||||
except (
|
||||
RateLimitError,
|
||||
APIConnectionError,
|
||||
ServiceUnavailableError,
|
||||
ContentPolicyViolationError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
OpenAIError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError,
|
||||
) as e:
|
||||
logger.error(f'Completion Error occurred:\n{e}')
|
||||
raise
|
||||
@@ -400,6 +377,38 @@ class LLM:
|
||||
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):
|
||||
return await litellm.acompletion(*args, **kwargs)
|
||||
|
||||
@@ -409,7 +418,10 @@ class LLM:
|
||||
|
||||
Check the complete documentation at https://litellm.vercel.app/docs/completion
|
||||
"""
|
||||
return self._completion
|
||||
try:
|
||||
return self._completion
|
||||
except Exception as e:
|
||||
raise LLMResponseError(e)
|
||||
|
||||
@property
|
||||
def async_completion(self):
|
||||
@@ -417,7 +429,10 @@ class LLM:
|
||||
|
||||
Check the complete documentation at https://litellm.vercel.app/docs/providers/ollama#example-usage---streaming--acompletion
|
||||
"""
|
||||
return self._async_completion
|
||||
try:
|
||||
return self._async_completion
|
||||
except Exception as e:
|
||||
raise LLMResponseError(e)
|
||||
|
||||
@property
|
||||
def async_streaming_completion(self):
|
||||
@@ -425,10 +440,34 @@ class LLM:
|
||||
|
||||
Check the complete documentation at https://litellm.vercel.app/docs/providers/ollama#example-usage---streaming--acompletion
|
||||
"""
|
||||
return self._async_streaming_completion
|
||||
try:
|
||||
return self._async_streaming_completion
|
||||
except Exception as e:
|
||||
raise LLMResponseError(e)
|
||||
|
||||
def supports_vision(self):
|
||||
return litellm.supports_vision(self.config.model)
|
||||
def vision_is_active(self):
|
||||
return not self.config.disable_vision and self._supports_vision()
|
||||
|
||||
def _supports_vision(self):
|
||||
"""Acquire from litellm if model is vision capable.
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def _post_completion(self, response) -> None:
|
||||
"""Post-process the completion response."""
|
||||
@@ -484,7 +523,11 @@ class LLM:
|
||||
Returns:
|
||||
int: The number of tokens.
|
||||
"""
|
||||
return litellm.token_counter(model=self.config.model, messages=messages)
|
||||
try:
|
||||
return litellm.token_counter(model=self.config.model, messages=messages)
|
||||
except Exception:
|
||||
# TODO: this is to limit logspam in case token count is not supported
|
||||
return 0
|
||||
|
||||
def is_local(self):
|
||||
"""Determines if the system is using a locally running LLM.
|
||||
@@ -550,3 +593,10 @@ class LLM:
|
||||
|
||||
def reset(self):
|
||||
self.metrics = Metrics()
|
||||
|
||||
def format_messages_for_llm(
|
||||
self, messages: Union[Message, list[Message]]
|
||||
) -> list[dict]:
|
||||
return format_messages(
|
||||
messages, self.vision_is_active(), self.is_caching_prompt_active()
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ if LLAMA_INDEX_AVAILABLE:
|
||||
|
||||
def attempt_on_error(retry_state):
|
||||
logger.error(
|
||||
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize these settings in the configuration.',
|
||||
f'{retry_state.outcome.exception()}. Attempt #{retry_state.attempt_number} | You can customize retry values in the configuration.',
|
||||
exc_info=False,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -17,8 +17,6 @@ from pathlib import Path
|
||||
import pexpect
|
||||
from fastapi import FastAPI, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from pathspec import PathSpec
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pydantic import BaseModel
|
||||
from uvicorn import run
|
||||
|
||||
@@ -60,6 +58,7 @@ ROOT_GID = 0
|
||||
INIT_COMMANDS = [
|
||||
'git config --global user.name "openhands" && git config --global user.email "openhands@all-hands.dev" && alias git="git --no-pager"',
|
||||
]
|
||||
SOFT_TIMEOUT_SECONDS = 5
|
||||
|
||||
|
||||
class RuntimeClient:
|
||||
@@ -114,6 +113,7 @@ class RuntimeClient:
|
||||
logger.info(f'AgentSkills initialized: {obs}')
|
||||
|
||||
await self._init_bash_commands()
|
||||
logger.info('Runtime client initialized.')
|
||||
|
||||
def _init_user(self, username: str, user_id: int) -> None:
|
||||
"""Create user if not exists."""
|
||||
@@ -212,6 +212,9 @@ class RuntimeClient:
|
||||
if ps1 == pexpect.EOF:
|
||||
logger.error(f'Bash shell EOF! {self.shell.after=}, {self.shell.before=}')
|
||||
raise RuntimeError('Bash shell EOF')
|
||||
if ps1 == pexpect.TIMEOUT:
|
||||
logger.warning('Bash shell timeout')
|
||||
return ''
|
||||
|
||||
# begin at the last occurrence of '[PEXPECT_BEGIN]'.
|
||||
# In multi-line bash commands, the prompt will be repeated
|
||||
@@ -243,39 +246,56 @@ class RuntimeClient:
|
||||
command: str,
|
||||
timeout: int | None,
|
||||
keep_prompt: bool = True,
|
||||
kill_on_timeout: bool = True,
|
||||
) -> tuple[str, int]:
|
||||
logger.debug(f'Executing command: {command}')
|
||||
self.shell.sendline(command)
|
||||
return self._continue_bash(
|
||||
timeout=timeout, keep_prompt=keep_prompt, kill_on_timeout=kill_on_timeout
|
||||
)
|
||||
|
||||
def _interrupt_bash(self, timeout: int | None = None) -> tuple[str, int]:
|
||||
self.shell.sendintr() # send SIGINT to the shell
|
||||
self.shell.expect(self.__bash_expect_regex, timeout=timeout)
|
||||
output = self.shell.before
|
||||
exit_code = 130 # SIGINT
|
||||
return output, exit_code
|
||||
|
||||
def _continue_bash(
|
||||
self,
|
||||
timeout: int | None,
|
||||
keep_prompt: bool = True,
|
||||
kill_on_timeout: bool = True,
|
||||
) -> tuple[str, int]:
|
||||
try:
|
||||
self.shell.sendline(command)
|
||||
self.shell.expect(self.__bash_expect_regex, timeout=timeout)
|
||||
|
||||
output = self.shell.before
|
||||
|
||||
# Get exit code
|
||||
self.shell.sendline('echo $?')
|
||||
logger.debug(f'Executing command for exit code: {command}')
|
||||
logger.debug('Requesting exit code...')
|
||||
self.shell.expect(self.__bash_expect_regex, timeout=timeout)
|
||||
_exit_code_output = self.shell.before
|
||||
logger.debug(f'Exit code Output: {_exit_code_output}')
|
||||
exit_code = int(_exit_code_output.strip().split()[0])
|
||||
|
||||
except pexpect.TIMEOUT as e:
|
||||
self.shell.sendintr() # send SIGINT to the shell
|
||||
self.shell.expect(self.__bash_expect_regex, timeout=timeout)
|
||||
output = self.shell.before
|
||||
output += (
|
||||
'\r\n\r\n'
|
||||
+ f'[Command timed out after {timeout} seconds. SIGINT was sent to interrupt it.]'
|
||||
)
|
||||
exit_code = 130 # SIGINT
|
||||
logger.error(f'Failed to execute command: {command}. Error: {e}')
|
||||
if kill_on_timeout:
|
||||
output, exit_code = self._interrupt_bash()
|
||||
output += (
|
||||
'\r\n\r\n'
|
||||
+ f'[Command timed out after {timeout} seconds. SIGINT was sent to interrupt it.]'
|
||||
)
|
||||
logger.error(f'Failed to execute command. Error: {e}')
|
||||
else:
|
||||
output = self.shell.before or ''
|
||||
exit_code = -1
|
||||
|
||||
finally:
|
||||
bash_prompt = self._get_bash_prompt_and_update_pwd()
|
||||
if keep_prompt:
|
||||
output += '\r\n' + bash_prompt
|
||||
logger.debug(f'Command output: {output}')
|
||||
|
||||
return output, exit_code
|
||||
|
||||
async def run_action(self, action) -> Observation:
|
||||
@@ -293,11 +313,25 @@ class RuntimeClient:
|
||||
commands = split_bash_commands(action.command)
|
||||
all_output = ''
|
||||
for command in commands:
|
||||
output, exit_code = self._execute_bash(
|
||||
command,
|
||||
timeout=action.timeout,
|
||||
keep_prompt=action.keep_prompt,
|
||||
)
|
||||
if command == '':
|
||||
output, exit_code = self._continue_bash(
|
||||
timeout=SOFT_TIMEOUT_SECONDS,
|
||||
keep_prompt=action.keep_prompt,
|
||||
kill_on_timeout=False,
|
||||
)
|
||||
elif command.lower() == 'ctrl+c':
|
||||
output, exit_code = self._interrupt_bash(
|
||||
timeout=SOFT_TIMEOUT_SECONDS
|
||||
)
|
||||
else:
|
||||
output, exit_code = self._execute_bash(
|
||||
command,
|
||||
timeout=SOFT_TIMEOUT_SECONDS
|
||||
if not action.blocking
|
||||
else action.timeout,
|
||||
keep_prompt=action.keep_prompt,
|
||||
kill_on_timeout=False if not action.blocking else True,
|
||||
)
|
||||
if all_output:
|
||||
# previous output already exists with prompt "user@hostname:working_dir #""
|
||||
# we need to add the command to the previous output,
|
||||
@@ -482,7 +516,6 @@ if __name__ == '__main__':
|
||||
browsergym_eval_env=args.browsergym_eval_env,
|
||||
)
|
||||
await client.ainit()
|
||||
logger.info('Runtime client initialized.')
|
||||
yield
|
||||
# Clean up & release the resources
|
||||
client.close()
|
||||
@@ -616,52 +649,12 @@ if __name__ == '__main__':
|
||||
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
||||
return []
|
||||
|
||||
# Check if .gitignore exists
|
||||
gitignore_path = os.path.join(full_path, '.gitignore')
|
||||
if os.path.exists(gitignore_path):
|
||||
# Use PathSpec to parse .gitignore
|
||||
with open(gitignore_path, 'r') as f:
|
||||
spec = PathSpec.from_lines(GitWildMatchPattern, f.readlines())
|
||||
else:
|
||||
# Fallback to default exclude list if .gitignore doesn't exist
|
||||
default_exclude = [
|
||||
'.git',
|
||||
'.DS_Store',
|
||||
'.svn',
|
||||
'.hg',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'.settings',
|
||||
'.pytest_cache',
|
||||
'__pycache__',
|
||||
'node_modules',
|
||||
'vendor',
|
||||
'build',
|
||||
'dist',
|
||||
'bin',
|
||||
'logs',
|
||||
'log',
|
||||
'tmp',
|
||||
'temp',
|
||||
'coverage',
|
||||
'venv',
|
||||
'env',
|
||||
]
|
||||
spec = PathSpec.from_lines(GitWildMatchPattern, default_exclude)
|
||||
|
||||
entries = os.listdir(full_path)
|
||||
|
||||
# Filter entries using PathSpec
|
||||
filtered_entries = [
|
||||
os.path.join(full_path, entry)
|
||||
for entry in entries
|
||||
if not spec.match_file(os.path.relpath(entry, str(full_path)))
|
||||
]
|
||||
|
||||
# Separate directories and files
|
||||
directories = []
|
||||
files = []
|
||||
for entry in filtered_entries:
|
||||
for entry in entries:
|
||||
# Remove leading slash and any parent directory components
|
||||
entry_relative = entry.lstrip('/').split('/')[-1]
|
||||
|
||||
@@ -689,6 +682,7 @@ if __name__ == '__main__':
|
||||
logger.error(f'Error listing files: {e}', exc_info=True)
|
||||
return []
|
||||
|
||||
logger.info('Runtime client initialized.')
|
||||
|
||||
logger.info(f'Starting action execution API on port {args.port}')
|
||||
print(f'Starting action execution API on port {args.port}')
|
||||
run(app, host='0.0.0.0', port=args.port)
|
||||
|
||||
@@ -13,6 +13,7 @@ from openhands.core.config import AppConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventStream
|
||||
from openhands.events.action import (
|
||||
ActionConfirmationStatus,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
@@ -25,6 +26,7 @@ from openhands.events.observation import (
|
||||
ErrorObservation,
|
||||
NullObservation,
|
||||
Observation,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.events.serialization import event_to_dict, observation_from_dict
|
||||
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
|
||||
@@ -45,6 +47,9 @@ class LogBuffer:
|
||||
"""
|
||||
|
||||
def __init__(self, container: docker.models.containers.Container):
|
||||
self.client_ready = False
|
||||
self.init_msg = 'Runtime client initialized.'
|
||||
|
||||
self.buffer: list[str] = []
|
||||
self.lock = threading.Lock()
|
||||
self.log_generator = container.logs(stream=True, follow=True)
|
||||
@@ -75,9 +80,12 @@ class LogBuffer:
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
if log_line:
|
||||
self.append(log_line.decode('utf-8').rstrip())
|
||||
decoded_line = log_line.decode('utf-8').rstrip()
|
||||
self.append(decoded_line)
|
||||
if self.init_msg in decoded_line:
|
||||
self.client_ready = True
|
||||
except Exception as e:
|
||||
logger.error(f'Error in stream_logs: {e}')
|
||||
logger.error(f'Error streaming docker logs: {e}')
|
||||
|
||||
def __del__(self):
|
||||
if self.log_stream_thread.is_alive():
|
||||
@@ -123,11 +131,10 @@ class EventStreamRuntime(Runtime):
|
||||
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
|
||||
|
||||
self.runtime_builder = DockerRuntimeBuilder(self.docker_client)
|
||||
logger.debug(f'EventStreamRuntime `{sid}` config:\n{self.config}')
|
||||
logger.debug(f'EventStreamRuntime `{sid}`')
|
||||
|
||||
# Buffer for container logs
|
||||
self.log_buffer: LogBuffer | None = None
|
||||
self.startup_done = False
|
||||
|
||||
if self.config.sandbox.runtime_extra_deps:
|
||||
logger.info(
|
||||
@@ -152,6 +159,8 @@ class EventStreamRuntime(Runtime):
|
||||
# will initialize both the event stream and the env vars
|
||||
super().__init__(config, event_stream, sid, plugins, env_vars)
|
||||
|
||||
self._wait_until_alive()
|
||||
|
||||
logger.info(
|
||||
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}'
|
||||
)
|
||||
@@ -163,7 +172,7 @@ class EventStreamRuntime(Runtime):
|
||||
return docker.from_env()
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
'Launch docker client failed. Please make sure you have installed docker and started the docker daemon.'
|
||||
'Launch docker client failed. Please make sure you have installed docker and started docker desktop/daemon.'
|
||||
)
|
||||
raise ex
|
||||
|
||||
@@ -244,9 +253,9 @@ class EventStreamRuntime(Runtime):
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_attempt(10),
|
||||
wait=tenacity.wait_exponential(multiplier=2, min=10, max=60),
|
||||
reraise=(ConnectionRefusedError,),
|
||||
)
|
||||
def _wait_until_alive(self):
|
||||
init_msg = 'Runtime client initialized.'
|
||||
logger.debug('Getting container logs...')
|
||||
|
||||
# Print and clear the log buffer
|
||||
@@ -254,26 +263,23 @@ class EventStreamRuntime(Runtime):
|
||||
self.log_buffer is not None
|
||||
), 'Log buffer is expected to be initialized when container is started'
|
||||
|
||||
# Always process logs, regardless of startup_done status
|
||||
# Always process logs, regardless of client_ready status
|
||||
logs = self.log_buffer.get_and_clear()
|
||||
if logs:
|
||||
formatted_logs = '\n'.join([f' |{log}' for log in logs])
|
||||
logger.info(
|
||||
'\n'
|
||||
+ '-' * 30
|
||||
+ '-' * 35
|
||||
+ 'Container logs:'
|
||||
+ '-' * 30
|
||||
+ '-' * 35
|
||||
+ f'\n{formatted_logs}'
|
||||
+ '\n'
|
||||
+ '-' * 90
|
||||
+ '-' * 80
|
||||
)
|
||||
# Check for initialization message even if startup_done is True
|
||||
if any(init_msg in log for log in logs):
|
||||
self.startup_done = True
|
||||
|
||||
if not self.startup_done:
|
||||
if not self.log_buffer.client_ready:
|
||||
attempts = 0
|
||||
while not self.startup_done and attempts < 10:
|
||||
while not self.log_buffer.client_ready and attempts < 5:
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
logs = self.log_buffer.get_and_clear()
|
||||
@@ -281,16 +287,13 @@ class EventStreamRuntime(Runtime):
|
||||
formatted_logs = '\n'.join([f' |{log}' for log in logs])
|
||||
logger.info(
|
||||
'\n'
|
||||
+ '-' * 30
|
||||
+ '-' * 35
|
||||
+ 'Container logs:'
|
||||
+ '-' * 30
|
||||
+ '-' * 35
|
||||
+ f'\n{formatted_logs}'
|
||||
+ '\n'
|
||||
+ '-' * 90
|
||||
+ '-' * 80
|
||||
)
|
||||
if any(init_msg in log for log in logs):
|
||||
self.startup_done = True
|
||||
break
|
||||
|
||||
response = self.session.get(f'{self.api_url}/alive')
|
||||
if response.status_code == 200:
|
||||
@@ -304,7 +307,15 @@ class EventStreamRuntime(Runtime):
|
||||
def sandbox_workspace_dir(self):
|
||||
return self.config.workspace_mount_path_in_sandbox
|
||||
|
||||
def close(self, close_client: bool = True):
|
||||
def close(self, close_client: bool = True, rm_all_containers: bool = True):
|
||||
"""
|
||||
Closes the EventStreamRuntime and associated objects
|
||||
|
||||
Parameters:
|
||||
- close_client (bool): Whether to close the DockerClient
|
||||
- rm_all_containers (bool): Whether to remove all containers with the 'openhands-sandbox-' prefix
|
||||
"""
|
||||
|
||||
if self.log_buffer:
|
||||
self.log_buffer.close()
|
||||
|
||||
@@ -314,7 +325,13 @@ class EventStreamRuntime(Runtime):
|
||||
containers = self.docker_client.containers.list(all=True)
|
||||
for container in containers:
|
||||
try:
|
||||
if container.name.startswith(self.container_name_prefix):
|
||||
# If the app doesn't shut down properly, it can leave runtime containers on the system. This ensures
|
||||
# that all 'openhands-sandbox-' containers are removed as well.
|
||||
if rm_all_containers and container.name.startswith(
|
||||
self.container_name_prefix
|
||||
):
|
||||
container.remove(force=True)
|
||||
elif container.name == self.container_name:
|
||||
logs = container.logs(tail=1000).decode('utf-8')
|
||||
logger.debug(
|
||||
f'==== Container logs ====\n{logs}\n==== End of container logs ===='
|
||||
@@ -322,6 +339,7 @@ class EventStreamRuntime(Runtime):
|
||||
container.remove(force=True)
|
||||
except docker.errors.NotFound:
|
||||
pass
|
||||
|
||||
if close_client:
|
||||
self.docker_client.close()
|
||||
|
||||
@@ -333,6 +351,12 @@ class EventStreamRuntime(Runtime):
|
||||
with self.action_semaphore:
|
||||
if not action.runnable:
|
||||
return NullObservation('')
|
||||
if (
|
||||
hasattr(action, 'is_confirmed')
|
||||
and action.is_confirmed
|
||||
== ActionConfirmationStatus.AWAITING_CONFIRMATION
|
||||
):
|
||||
return NullObservation('')
|
||||
action_type = action.action # type: ignore[attr-defined]
|
||||
if action_type not in ACTION_TYPE_TO_CLASS:
|
||||
return ErrorObservation(f'Action {action_type} does not exist.')
|
||||
@@ -340,6 +364,13 @@ class EventStreamRuntime(Runtime):
|
||||
return ErrorObservation(
|
||||
f'Action {action_type} is not supported in the current runtime.'
|
||||
)
|
||||
if (
|
||||
hasattr(action, 'is_confirmed')
|
||||
and action.is_confirmed == ActionConfirmationStatus.REJECTED
|
||||
):
|
||||
return UserRejectObservation(
|
||||
'Action has been rejected by the user! Waiting for further user input.'
|
||||
)
|
||||
|
||||
logger.info('Awaiting session')
|
||||
self._wait_until_alive()
|
||||
|
||||