mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
12 Commits
chore/refa
...
0.32.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a820e45ec4 | ||
|
|
588b9f34be | ||
|
|
fb02fefaca | ||
|
|
856d5ff976 | ||
|
|
eb4aeb3922 | ||
|
|
4b47e5215b | ||
|
|
0087082643 | ||
|
|
e698a393b2 | ||
|
|
d48e2a4cf1 | ||
|
|
749a903de5 | ||
|
|
0a6321246a | ||
|
|
ddda30d9b7 |
81
.github/workflows/ghcr-build.yml
vendored
81
.github/workflows/ghcr-build.yml
vendored
@@ -28,6 +28,28 @@ env:
|
||||
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
jobs:
|
||||
define-matrix:
|
||||
runs-on: blacksmith
|
||||
outputs:
|
||||
base_image: ${{ steps.define-base-images.outputs.base_image }}
|
||||
steps:
|
||||
- name: Define base images
|
||||
shell: bash
|
||||
id: define-base-images
|
||||
run: |
|
||||
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
|
||||
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
||||
json=$(jq -n -c '[
|
||||
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
|
||||
]')
|
||||
else
|
||||
json=$(jq -n -c '[
|
||||
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
|
||||
{ image: "ubuntu:24.04", tag: "ubuntu" }
|
||||
]')
|
||||
fi
|
||||
echo "base_image=$json" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Builds the OpenHands Docker images
|
||||
ghcr_build_app:
|
||||
name: Build App Image
|
||||
@@ -84,13 +106,10 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: define-matrix
|
||||
strategy:
|
||||
matrix:
|
||||
base_image:
|
||||
- image: 'nikolaik/python-nodejs:python3.12-nodejs22'
|
||||
tag: nikolaik
|
||||
- image: 'ubuntu:24.04'
|
||||
tag: ubuntu
|
||||
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -118,9 +137,9 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.cache/ms-playwright
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
# This is the one that saves the cache, the others set 'lookup-only: true'
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Install poetry via pipx
|
||||
@@ -132,14 +151,33 @@ jobs:
|
||||
- name: Lowercase Repository Owner
|
||||
run: |
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Short SHA
|
||||
run: |
|
||||
echo SHORT_SHA=$(git rev-parse --short "$RELEVANT_SHA") >> $GITHUB_ENV
|
||||
- name: Determine docker build params
|
||||
if: github.event.pull_request.head.repo.fork != true
|
||||
shell: bash
|
||||
run: |
|
||||
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} -t ${{ matrix.base_image.tag }} --dry
|
||||
|
||||
DOCKER_BUILD_JSON=$(jq -c . < docker-build-dry.json)
|
||||
echo "DOCKER_TAGS=$(echo "$DOCKER_BUILD_JSON" | jq -r '.tags | join(",")')" >> $GITHUB_ENV
|
||||
echo "DOCKER_PLATFORM=$(echo "$DOCKER_BUILD_JSON" | jq -r '.platform')" >> $GITHUB_ENV
|
||||
echo "DOCKER_BUILD_ARGS=$(echo "$DOCKER_BUILD_JSON" | jq -r '.build_args | join(",")')" >> $GITHUB_ENV
|
||||
- name: Build and push runtime image ${{ matrix.base_image.image }}
|
||||
if: github.event.pull_request.head.repo.fork != true
|
||||
run: |
|
||||
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} --push -t ${{ matrix.base_image.tag }}
|
||||
uses: useblacksmith/build-push-action@v1
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_TAGS }}
|
||||
platforms: ${{ env.DOCKER_PLATFORM }}
|
||||
build-args: ${{ env.DOCKER_BUILD_ARGS }}
|
||||
context: containers/runtime
|
||||
provenance: false
|
||||
# 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
|
||||
uses: docker/build-push-action@v6
|
||||
uses: useblacksmith/build-push-action@v1
|
||||
with:
|
||||
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
@@ -170,9 +208,9 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.cache/ms-playwright
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
lookup-only: true
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
@@ -209,12 +247,12 @@ jobs:
|
||||
# Run unit tests with the Docker runtime Docker images as root
|
||||
test_runtime_root:
|
||||
name: RT Unit Tests (Root)
|
||||
needs: [ghcr_build_runtime]
|
||||
needs: [ghcr_build_runtime, define-matrix]
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
base_image: ['nikolaik', 'ubuntu']
|
||||
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -225,20 +263,20 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image }}
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.cache/ms-playwright
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
lookup-only: true
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
@@ -260,7 +298,7 @@ jobs:
|
||||
# Install to be able to retry on failures for flaky tests
|
||||
poetry run pip install pytest-rerunfailures
|
||||
|
||||
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
|
||||
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
|
||||
# Setting RUN_AS_OPENHANDS to false means use root.
|
||||
# That should mean SANDBOX_USER_ID is ignored but some tests do not check for RUN_AS_OPENHANDS.
|
||||
@@ -280,10 +318,10 @@ jobs:
|
||||
test_runtime_oh:
|
||||
name: RT Unit Tests (openhands)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
needs: [ghcr_build_runtime]
|
||||
needs: [ghcr_build_runtime, define-matrix]
|
||||
strategy:
|
||||
matrix:
|
||||
base_image: [nikolaik, ubuntu]
|
||||
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -294,12 +332,12 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image }}
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
@@ -307,6 +345,7 @@ jobs:
|
||||
~/.cache/pypoetry
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
lookup-only: true
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
@@ -328,7 +367,7 @@ jobs:
|
||||
# Install to be able to retry on failures for flaky tests
|
||||
poetry run pip install pytest-rerunfailures
|
||||
|
||||
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
|
||||
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
|
||||
TEST_RUNTIME=docker \
|
||||
SANDBOX_USER_ID=$(id -u) \
|
||||
|
||||
@@ -118,7 +118,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.31-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.32-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -185,7 +185,7 @@ test:
|
||||
|
||||
build-frontend:
|
||||
@echo "$(YELLOW)Building frontend...$(RESET)"
|
||||
@cd frontend && npm run build
|
||||
@cd frontend && npm run prepare && npm run build
|
||||
|
||||
# Start backend
|
||||
start-backend:
|
||||
|
||||
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -7,15 +7,17 @@ org_name=""
|
||||
push=0
|
||||
load=0
|
||||
tag_suffix=""
|
||||
dry_run=0
|
||||
|
||||
# Function to display usage information
|
||||
usage() {
|
||||
echo "Usage: $0 -i <image_name> [-o <org_name>] [--push] [--load] [-t <tag_suffix>]"
|
||||
echo "Usage: $0 -i <image_name> [-o <org_name>] [--push] [--load] [-t <tag_suffix>] [--dry]"
|
||||
echo " -i: Image name (required)"
|
||||
echo " -o: Organization name"
|
||||
echo " --push: Push the image"
|
||||
echo " --load: Load the image"
|
||||
echo " -t: Tag suffix"
|
||||
echo " --dry: Don't build, only create build-args.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -27,6 +29,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--push) push=1; shift ;;
|
||||
--load) load=1; shift ;;
|
||||
-t) tag_suffix="$2"; shift 2 ;;
|
||||
--dry) dry_run=1; shift ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
@@ -113,10 +116,13 @@ echo "Repo: $DOCKER_REPOSITORY"
|
||||
echo "Base dir: $DOCKER_BASE_DIR"
|
||||
|
||||
args=""
|
||||
full_tags=()
|
||||
for tag in "${tags[@]}"; do
|
||||
args+=" -t $DOCKER_REPOSITORY:$tag"
|
||||
full_tags+=("$DOCKER_REPOSITORY:$tag")
|
||||
done
|
||||
|
||||
|
||||
if [[ $push -eq 1 ]]; then
|
||||
args+=" --push"
|
||||
args+=" --cache-to=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag,mode=max"
|
||||
@@ -136,6 +142,26 @@ else
|
||||
# For push or without load, build for multiple platforms
|
||||
platform="linux/amd64,linux/arm64"
|
||||
fi
|
||||
if [[ $dry_run -eq 1 ]]; then
|
||||
echo "Dry Run is enabled. Writing build config to docker-build-dry.json"
|
||||
jq -n \
|
||||
--argjson tags "$(printf '%s\n' "${full_tags[@]}" | jq -R . | jq -s .)" \
|
||||
--arg platform "$platform" \
|
||||
--arg openhands_build_version "$OPENHANDS_BUILD_VERSION" \
|
||||
--arg dockerfile "$dir/Dockerfile" \
|
||||
'{
|
||||
tags: $tags,
|
||||
platform: $platform,
|
||||
build_args: [
|
||||
"OPENHANDS_BUILD_VERSION=" + $openhands_build_version
|
||||
],
|
||||
dockerfile: $dockerfile
|
||||
}' > docker-build-dry.json
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo "Building for platform(s): $platform"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.31-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.32-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ 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) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,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) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -44,7 +44,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
@@ -82,5 +82,5 @@ docker network create openhands-network
|
||||
# 分離されたネットワークで OpenHands を実行
|
||||
docker run # ... \
|
||||
--network openhands-network \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "escreva um script bash que imprima oi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@
|
||||
A maneira mais fácil de executar o OpenHands é no Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
Você encontrará o OpenHands em execução em http://localhost:3000!
|
||||
|
||||
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ 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) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,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) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.31
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
52
evaluation/benchmarks/swe_bench/binary_patch_utils.py
Normal file
52
evaluation/benchmarks/swe_bench/binary_patch_utils.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Utilities for handling binary files and patch generation in SWE-bench evaluation.
|
||||
"""
|
||||
|
||||
|
||||
def remove_binary_diffs(patch_text):
|
||||
"""
|
||||
Remove binary file diffs from a git patch.
|
||||
|
||||
Args:
|
||||
patch_text (str): The git patch text
|
||||
|
||||
Returns:
|
||||
str: The cleaned patch text with binary diffs removed
|
||||
"""
|
||||
lines = patch_text.splitlines()
|
||||
cleaned_lines = []
|
||||
block = []
|
||||
is_binary_block = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith('diff --git '):
|
||||
if block and not is_binary_block:
|
||||
cleaned_lines.extend(block)
|
||||
block = [line]
|
||||
is_binary_block = False
|
||||
elif 'Binary files' in line:
|
||||
is_binary_block = True
|
||||
block.append(line)
|
||||
else:
|
||||
block.append(line)
|
||||
|
||||
if block and not is_binary_block:
|
||||
cleaned_lines.extend(block)
|
||||
return '\n'.join(cleaned_lines)
|
||||
|
||||
|
||||
def remove_binary_files_from_git():
|
||||
"""
|
||||
Generate a bash command to remove binary files from git staging.
|
||||
|
||||
Returns:
|
||||
str: A bash command that removes binary files from git staging
|
||||
"""
|
||||
return """
|
||||
for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do
|
||||
if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then
|
||||
git rm -f "$file" 2>/dev/null || rm -f "$file"
|
||||
echo "Removed: $file"
|
||||
fi
|
||||
done
|
||||
""".strip()
|
||||
@@ -28,7 +28,7 @@ def get_resource_mapping(dataset_name: str) -> dict[str, float]:
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
_global_resource_mapping[dataset_name] = json.load(f)
|
||||
logger.info(f'Loaded resource mapping for {dataset_name}')
|
||||
logger.debug(f'Loaded resource mapping for {dataset_name}')
|
||||
return _global_resource_mapping[dataset_name]
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ import toml
|
||||
from datasets import load_dataset
|
||||
|
||||
import openhands.agenthub
|
||||
from evaluation.benchmarks.swe_bench.binary_patch_utils import (
|
||||
remove_binary_diffs,
|
||||
remove_binary_files_from_git,
|
||||
)
|
||||
from evaluation.benchmarks.swe_bench.resource.mapping import (
|
||||
get_instance_resource_factor,
|
||||
)
|
||||
@@ -38,8 +42,12 @@ from openhands.core.config import (
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.critic import AgentFinishedCritic
|
||||
from openhands.events.action import CmdRunAction, MessageAction
|
||||
from openhands.events.observation import CmdOutputObservation, ErrorObservation
|
||||
from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
|
||||
from openhands.events.observation import (
|
||||
CmdOutputObservation,
|
||||
ErrorObservation,
|
||||
FileReadObservation,
|
||||
)
|
||||
from openhands.events.serialization.event import event_from_dict, event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
@@ -121,7 +129,7 @@ Be thorough in your exploration, testing, and reasoning. It's fine if your think
|
||||
)
|
||||
|
||||
if 'image_assets' in instance:
|
||||
assets = instance['image_assets']
|
||||
assets = json.loads(instance['image_assets'])
|
||||
assert (
|
||||
'problem_statement' in assets
|
||||
), 'problem_statement is required in image_assets'
|
||||
@@ -146,8 +154,8 @@ def get_instance_docker_image(
|
||||
# swebench/sweb.eval.x86_64.django_1776_django-11333:v1
|
||||
docker_image_prefix = 'docker.io/swebench/'
|
||||
repo, name = instance_id.split('__')
|
||||
image_name = f'swebench/sweb.eval.x86_64.{repo}_1776_{name}:latest'
|
||||
logger.info(f'Using official SWE-Bench image: {image_name}')
|
||||
image_name = f'swebench/sweb.eval.x86_64.{repo}_1776_{name}:latest'.lower()
|
||||
logger.debug(f'Using official SWE-Bench image: {image_name}')
|
||||
return image_name
|
||||
else:
|
||||
# OpenHands version of the image
|
||||
@@ -164,10 +172,7 @@ def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
# We use a different instance image for the each instance of swe-bench eval
|
||||
use_swebench_official_image = bool(
|
||||
('verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower())
|
||||
and 'swe-gym' not in metadata.dataset.lower()
|
||||
)
|
||||
use_swebench_official_image = 'swe-gym' not in metadata.dataset.lower()
|
||||
base_container_image = get_instance_docker_image(
|
||||
instance['instance_id'],
|
||||
swebench_official_image=use_swebench_official_image,
|
||||
@@ -229,16 +234,17 @@ def initialize_runtime(
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
obs: CmdOutputObservation
|
||||
|
||||
# Set instance id
|
||||
# Set instance id and git configuration
|
||||
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"""
|
||||
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 && git config --global core.pager "" && git config --global diff.binary false"""
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0, f'Failed to export SWE_INSTANCE_ID: {str(obs)}'
|
||||
obs.exit_code == 0,
|
||||
f'Failed to export SWE_INSTANCE_ID and configure git: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
|
||||
@@ -334,15 +340,18 @@ def initialize_runtime(
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command='which python')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0 and 'testbed' in obs.content,
|
||||
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
|
||||
)
|
||||
if 'multimodal' not in metadata.dataset.lower():
|
||||
# Only for non-multimodal datasets, we need to activate the testbed environment for Python
|
||||
# SWE-Bench multimodal datasets are not using the testbed environment
|
||||
action = CmdRunAction(command='which python')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0 and 'testbed' in obs.content,
|
||||
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
|
||||
)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Initialization Fn')
|
||||
@@ -452,11 +461,22 @@ def complete_runtime(
|
||||
f'Failed to git add -A: {str(obs)}',
|
||||
)
|
||||
|
||||
# Remove binary files from git staging
|
||||
action = CmdRunAction(command=remove_binary_files_from_git())
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to remove binary files: {str(obs)}',
|
||||
)
|
||||
|
||||
n_retries = 0
|
||||
git_patch = None
|
||||
while n_retries < 5:
|
||||
action = CmdRunAction(
|
||||
command=f'git diff --no-color --cached {instance["base_commit"]}'
|
||||
command=f'git diff --no-color --cached {instance["base_commit"]} > patch.diff'
|
||||
)
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
@@ -465,8 +485,28 @@ def complete_runtime(
|
||||
n_retries += 1
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
if obs.exit_code == 0:
|
||||
git_patch = obs.content.strip()
|
||||
break
|
||||
# Read the patch file
|
||||
action = FileReadAction(path='patch.diff')
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if isinstance(obs, FileReadObservation):
|
||||
git_patch = obs.content
|
||||
break
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
# Fall back to cat "patch.diff" to get the patch
|
||||
assert 'File could not be decoded as utf-8' in obs.content
|
||||
action = CmdRunAction(command='cat patch.diff')
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
assert isinstance(obs, CmdOutputObservation) and obs.exit_code == 0
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
git_patch = obs.content
|
||||
break
|
||||
else:
|
||||
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
|
||||
else:
|
||||
logger.info('Failed to get git diff, retrying...')
|
||||
sleep_if_should_continue(10)
|
||||
@@ -478,6 +518,9 @@ def complete_runtime(
|
||||
|
||||
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
|
||||
|
||||
# Remove binary diffs from the patch
|
||||
git_patch = remove_binary_diffs(git_patch)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Completion Fn')
|
||||
logger.info('-' * 30)
|
||||
@@ -761,9 +804,19 @@ if __name__ == '__main__':
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
history = [event_from_dict(event) for event in instance['history']]
|
||||
critic_result = critic.evaluate(history)
|
||||
if not critic_result.success:
|
||||
try:
|
||||
history = [
|
||||
event_from_dict(event) for event in instance['history']
|
||||
]
|
||||
critic_result = critic.evaluate(
|
||||
history, instance['test_result'].get('git_patch', '')
|
||||
)
|
||||
if not critic_result.success:
|
||||
instances_failed.append(instance['instance_id'])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Error loading history for instance {instance["instance_id"]}: {e}'
|
||||
)
|
||||
instances_failed.append(instance['instance_id'])
|
||||
logger.info(
|
||||
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
|
||||
@@ -792,7 +845,11 @@ if __name__ == '__main__':
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
if instance['instance_id'] not in added_instance_ids:
|
||||
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
|
||||
if (
|
||||
instance['instance_id'] not in added_instance_ids
|
||||
and instance['test_result']['git_patch'].strip()
|
||||
):
|
||||
fout.write(line)
|
||||
added_instance_ids.add(instance['instance_id'])
|
||||
logger.info(
|
||||
|
||||
@@ -18,6 +18,7 @@ if [[ -z "$item" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
WORKSPACE_NAME=$(echo "$item" | jq -r '(.repo | tostring) + "__" + (.version | tostring) | gsub("/"; "__")')
|
||||
|
||||
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
|
||||
@@ -36,5 +37,7 @@ mkdir -p /workspace
|
||||
cp -r /testbed /workspace/$WORKSPACE_NAME
|
||||
|
||||
# Activate instance-specific environment
|
||||
. /opt/miniconda3/etc/profile.d/conda.sh
|
||||
conda activate testbed
|
||||
if [ -d /opt/miniconda3 ]; then
|
||||
. /opt/miniconda3/etc/profile.d/conda.sh
|
||||
conda activate testbed
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
cd frontend
|
||||
lint-staged
|
||||
vitest run
|
||||
npm run check-unlocalized-strings
|
||||
npx lint-staged
|
||||
npm test
|
||||
@@ -3,14 +3,14 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("AnalyticsConsentFormModal", () => {
|
||||
it("should call saveUserSettings with consent", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseMock = vi.fn();
|
||||
const saveUserSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
|
||||
wrapper: ({ children }) => (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { WaitlistModal } from "#/components/features/waitlist/waitlist-modal";
|
||||
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
|
||||
describe("WaitlistModal", () => {
|
||||
describe("AuthModal", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
});
|
||||
@@ -14,7 +14,7 @@ describe("WaitlistModal", () => {
|
||||
});
|
||||
|
||||
it("should render a tos checkbox that is unchecked by default", () => {
|
||||
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
|
||||
render(<AuthModal githubAuthUrl={null} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).not.toBeChecked();
|
||||
@@ -22,7 +22,7 @@ describe("WaitlistModal", () => {
|
||||
|
||||
it("should only enable the GitHub button if the tos checkbox is checked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
|
||||
render(<AuthModal githubAuthUrl={null} />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("WaitlistModal", () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl="mock-url" />);
|
||||
render(<AuthModal githubAuthUrl="mock-url" />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
// These tests will now fail because the conversation panel is rendered through a portal
|
||||
// and technically not a child of the Sidebar component.
|
||||
@@ -18,7 +18,7 @@ const renderSidebar = () =>
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
|
||||
describe("Sidebar", () => {
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -3,20 +3,20 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { screen } from "@testing-library/react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("SettingsForm", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
const RouteStub = createRoutesStub([
|
||||
{
|
||||
Component: () => (
|
||||
<SettingsForm
|
||||
settings={DEFAULT_SETTINGS}
|
||||
models={[DEFAULT_SETTINGS.llm_model]}
|
||||
models={[DEFAULT_SETTINGS.LLM_MODEL]}
|
||||
onClose={onCloseMock}
|
||||
/>
|
||||
),
|
||||
@@ -33,7 +33,7 @@ describe("SettingsForm", () => {
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_model: DEFAULT_SETTINGS.llm_model,
|
||||
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("useSaveSettings", () => {
|
||||
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const { result } = renderHook(() => useSaveSettings(), {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
|
||||
@@ -7,7 +7,6 @@ import MainApp from "#/routes/root-layout";
|
||||
import i18n from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
describe("frontend/routes/_oh", () => {
|
||||
const RouteStub = createRoutesStub([{ Component: MainApp, path: "/" }]);
|
||||
@@ -60,7 +59,7 @@ describe("frontend/routes/_oh", () => {
|
||||
it.skip("should render and capture the user's consent if oss mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
|
||||
@@ -8,7 +8,6 @@ import MainApp from "#/routes/root-layout";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import Home from "#/routes/home";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
@@ -26,7 +25,7 @@ const createAxiosNotFoundErrorObject = () =>
|
||||
},
|
||||
);
|
||||
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
|
||||
@@ -7,26 +7,25 @@ import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
import AccountSettings from "#/routes/account-settings";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
import { GitProvider } from "#/api/settings-service/settings-service.types";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
const toggleAdvancedSettings = async (user: UserEvent) => {
|
||||
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
|
||||
await user.click(advancedSwitch);
|
||||
};
|
||||
|
||||
const MOCK_PROVIDER_TOKENS_ARE_SET: Record<GitProvider, boolean> = {
|
||||
const mock_provider_tokens_are_set: Record<Provider, boolean> = {
|
||||
github: true,
|
||||
gitlab: false,
|
||||
};
|
||||
|
||||
describe("Settings Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const resetSettingsSpy = vi.spyOn(SettingsService, "resetSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const resetSettingsSpy = vi.spyOn(OpenHands, "resetSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
const { handleLogoutMock } = vi.hoisted(() => ({
|
||||
@@ -66,9 +65,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Use queryAllByText to handle multiple elements with the same text
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(
|
||||
0,
|
||||
);
|
||||
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
|
||||
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
|
||||
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
|
||||
screen.getByText("BUTTON$SAVE");
|
||||
@@ -101,7 +98,9 @@ describe("Settings Screen", () => {
|
||||
|
||||
// TODO: Set a better unset indicator
|
||||
it.skip("should render an indicator if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -120,8 +119,8 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should set '<hidden>' placeholder if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -134,8 +133,8 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should render an indicator if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: mock_provider_tokens_are_set,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -151,6 +150,8 @@ describe("Settings Screen", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Tests for DISCONNECT_FROM_GITHUB button removed as the button is no longer included in main
|
||||
|
||||
it("should not render the 'Configure GitHub Repositories' button if OSS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
@@ -209,7 +210,7 @@ describe("Settings Screen", () => {
|
||||
it.skip("should not reset LLM Provider and Model if GitHub token is invalid", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
saveSettingsSpy.mockRejectedValueOnce(new Error("Invalid GitHub token"));
|
||||
@@ -330,8 +331,8 @@ describe("Settings Screen", () => {
|
||||
// TODO: Set a better unset indicator
|
||||
it.skip("should render an indicator if the LLM API key is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_api_key_set: null,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: null,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -351,7 +352,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should render an indicator if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
@@ -372,7 +373,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should set '<hidden>' placeholder if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
@@ -387,7 +388,7 @@ describe("Settings Screen", () => {
|
||||
describe("Basic Model Selector", () => {
|
||||
it("should set the provider and model", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
|
||||
@@ -454,7 +455,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
remote_runtime_resource_factor: 1,
|
||||
});
|
||||
|
||||
@@ -494,7 +495,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -545,7 +546,7 @@ describe("Settings Screen", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -562,7 +563,7 @@ describe("Settings Screen", () => {
|
||||
// Mock the settings that will be returned after reset
|
||||
// This should be the default settings with no advanced settings enabled
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
@@ -613,7 +614,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should toggle advanced if user had set a custom model", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "some/custom-model",
|
||||
});
|
||||
renderSettingsScreen();
|
||||
@@ -648,7 +649,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should have confirmation mode enabled if the user previously had it enabled", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
confirmation_mode: true,
|
||||
});
|
||||
|
||||
@@ -665,7 +666,7 @@ describe("Settings Screen", () => {
|
||||
// FIXME: security analyzer is not found for some reason...
|
||||
it.skip("should have the values set if the user previously had them set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
language: "no",
|
||||
user_consents_to_analytics: true,
|
||||
llm_base_url: "https://test.com",
|
||||
@@ -700,7 +701,7 @@ describe("Settings Screen", () => {
|
||||
it("should save the settings when the 'Save Changes' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -727,7 +728,7 @@ describe("Settings Screen", () => {
|
||||
it("should properly save basic LLM model settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -763,7 +764,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should reset the settings when the 'Reset to defaults' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -794,7 +795,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
// Mock the settings response after reset
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
@@ -819,7 +820,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -927,7 +928,7 @@ describe("Settings Screen", () => {
|
||||
it("should not send an empty LLM API Key if the user submits an empty string but already has it set", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import { render, screen } from "@testing-library/react";
|
||||
import { test, expect, describe, vi } from "vitest";
|
||||
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
|
||||
import { ChatInput } from "#/components/features/chat/chat-input";
|
||||
import path from 'path';
|
||||
import { scanDirectoryForUnlocalizedStrings } from "#/utils/scan-unlocalized-strings-ast";
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
@@ -15,22 +12,17 @@ vi.mock("react-i18next", () => ({
|
||||
describe("Check for hardcoded English strings", () => {
|
||||
test("InteractiveChatBox should not have hardcoded English strings", () => {
|
||||
const { container } = render(
|
||||
<InteractiveChatBox
|
||||
onSubmit={() => {}}
|
||||
onStop={() => {}}
|
||||
/>
|
||||
<InteractiveChatBox onSubmit={() => {}} onStop={() => {}} />,
|
||||
);
|
||||
|
||||
// Get all text content
|
||||
const text = container.textContent;
|
||||
|
||||
// List of English strings that should be translated
|
||||
const hardcodedStrings = [
|
||||
"What do you want to build?",
|
||||
];
|
||||
const hardcodedStrings = ["What do you want to build?"];
|
||||
|
||||
// Check each string
|
||||
hardcodedStrings.forEach(str => {
|
||||
hardcodedStrings.forEach((str) => {
|
||||
expect(text).not.toContain(str);
|
||||
});
|
||||
});
|
||||
@@ -39,23 +31,4 @@ describe("Check for hardcoded English strings", () => {
|
||||
render(<ChatInput onSubmit={() => {}} />);
|
||||
screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
|
||||
});
|
||||
|
||||
test("No unlocalized strings should exist in frontend code", () => {
|
||||
const srcPath = path.resolve(__dirname, '../../src');
|
||||
|
||||
// Get unlocalized strings using the AST scanner
|
||||
// The scanner now properly handles CSS classes using AST information
|
||||
const results = scanDirectoryForUnlocalizedStrings(srcPath);
|
||||
|
||||
// If we found any unlocalized strings, format them for output
|
||||
if (results.size > 0) {
|
||||
const formattedResults = Array.from(results.entries())
|
||||
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
|
||||
.join('\n');
|
||||
|
||||
throw new Error(
|
||||
`Found unlocalized strings in the following files:${formattedResults}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
llm_base_url: "test",
|
||||
LLM_BASE_URL: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
agent: "test",
|
||||
AGENT: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
remote_runtime_resource_factor: 999,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 999,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -39,7 +39,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
confirmation_mode: true,
|
||||
CONFIRMATION_MODE: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
security_analyzer: "test",
|
||||
SECURITY_ANALYZER: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
2441
frontend/package-lock.json
generated
2441
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,51 +1,51 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.31.0",
|
||||
"version": "0.32.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.5",
|
||||
"@heroui/react": "2.7.6",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.4.0",
|
||||
"@react-router/serve": "^7.4.0",
|
||||
"@react-router/node": "^7.5.0",
|
||||
"@react-router/serve": "^7.5.0",
|
||||
"@react-types/shared": "^3.28.0",
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"@stripe/react-stripe-js": "^3.5.1",
|
||||
"@stripe/stripe-js": "^6.1.0",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@stripe/react-stripe-js": "^3.6.0",
|
||||
"@stripe/stripe-js": "^7.0.0",
|
||||
"@tanstack/react-query": "^5.72.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.8.4",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.6.2",
|
||||
"framer-motion": "^12.6.3",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.25",
|
||||
"jose": "^6.0.10",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.233.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"posthog-js": "^1.235.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.4.0",
|
||||
"react-router": "^7.5.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.8",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"vite": "^6.2.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"vite": "^6.2.5",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
@@ -65,12 +65,8 @@
|
||||
"lint": "eslint src --ext .ts,.tsx,.js && prettier --check src/**/*.{ts,tsx}",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx,.js --fix && prettier --write src/**/*.{ts,tsx}",
|
||||
"prepare": "cd .. && husky frontend/.husky",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run test && lint-staged"
|
||||
}
|
||||
"typecheck": "react-router typegen && tsc",
|
||||
"check-unlocalized-strings": "node scripts/check-unlocalized-strings.cjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx,js}": [
|
||||
@@ -84,22 +80,22 @@
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@react-router/dev": "^7.4.0",
|
||||
"@react-router/dev": "^7.5.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.68.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.72.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -108,18 +104,18 @@
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.2.5",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"husky": "^9.1.6",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"lint-staged": "^15.5.0",
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"stripe": "^17.7.0",
|
||||
"stripe": "^18.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.2"
|
||||
|
||||
707
frontend/scripts/check-unlocalized-strings.cjs
Executable file
707
frontend/scripts/check-unlocalized-strings.cjs
Executable file
@@ -0,0 +1,707 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Pre-commit hook script to check for unlocalized strings in the frontend code
|
||||
* This script is based on the test in __tests__/utils/check-hardcoded-strings.test.tsx
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const parser = require('@babel/parser');
|
||||
const traverse = require('@babel/traverse').default;
|
||||
|
||||
// Files/directories to ignore
|
||||
const IGNORE_PATHS = [
|
||||
// Build and dependency files
|
||||
"node_modules",
|
||||
"dist",
|
||||
".git",
|
||||
"test",
|
||||
"__tests__",
|
||||
".d.ts",
|
||||
"i18n",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"tsconfig.json",
|
||||
|
||||
// Internal code that doesn't need localization
|
||||
"mocks", // Mock data
|
||||
"assets", // SVG paths and CSS classes
|
||||
"types", // Type definitions and constants
|
||||
"state", // Redux state management
|
||||
"api", // API endpoints
|
||||
"services", // Internal services
|
||||
"hooks", // React hooks
|
||||
"context", // React context
|
||||
"store", // Redux store
|
||||
"routes.ts", // Route definitions
|
||||
"root.tsx", // Root component
|
||||
"entry.client.tsx", // Client entry point
|
||||
"utils/scan-unlocalized-strings.ts", // Original scanner
|
||||
"utils/scan-unlocalized-strings-ast.ts", // This file itself
|
||||
];
|
||||
|
||||
// Extensions to scan
|
||||
const SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
||||
|
||||
// Attributes that typically don't contain user-facing text
|
||||
const NON_TEXT_ATTRIBUTES = [
|
||||
"className",
|
||||
"testId",
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"href",
|
||||
"src",
|
||||
"alt",
|
||||
"placeholder",
|
||||
"rel",
|
||||
"target",
|
||||
"style",
|
||||
"onClick",
|
||||
"onChange",
|
||||
"onSubmit",
|
||||
"data-testid",
|
||||
"aria-label",
|
||||
"aria-labelledby",
|
||||
"aria-describedby",
|
||||
"aria-hidden",
|
||||
"role",
|
||||
];
|
||||
|
||||
function shouldIgnorePath(filePath) {
|
||||
return IGNORE_PATHS.some((ignore) => filePath.includes(ignore));
|
||||
}
|
||||
|
||||
// Check if a string looks like a translation key
|
||||
// Translation keys typically use dots, underscores, or are all caps
|
||||
// Also check for the pattern with $ which is used in our translation keys
|
||||
function isLikelyTranslationKey(str) {
|
||||
return (
|
||||
/^[A-Z0-9_$.]+$/.test(str) ||
|
||||
str.includes(".") ||
|
||||
/[A-Z0-9_]+\$[A-Z0-9_]+/.test(str)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if a string is a raw translation key that should be wrapped in t()
|
||||
function isRawTranslationKey(str) {
|
||||
// Check for our specific translation key pattern (e.g., "SETTINGS$GITHUB_SETTINGS")
|
||||
// Exclude specific keys that are already properly used with i18next.t() in the code
|
||||
const excludedKeys = [
|
||||
"STATUS$ERROR_LLM_OUT_OF_CREDITS",
|
||||
"ERROR$GENERIC",
|
||||
"GITHUB$AUTH_SCOPE",
|
||||
];
|
||||
|
||||
if (excludedKeys.includes(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^[A-Z0-9_]+\$[A-Z0-9_]+$/.test(str);
|
||||
}
|
||||
|
||||
// Specific technical strings that should be excluded from localization
|
||||
const EXCLUDED_TECHNICAL_STRINGS = [
|
||||
"openid email profile", // OAuth scope string - not user-facing
|
||||
];
|
||||
|
||||
function isExcludedTechnicalString(str) {
|
||||
return EXCLUDED_TECHNICAL_STRINGS.includes(str);
|
||||
}
|
||||
|
||||
function isCommonDevelopmentString(str) {
|
||||
// Technical patterns that are definitely not UI strings
|
||||
const technicalPatterns = [
|
||||
// URLs and paths
|
||||
/^https?:\/\//, // URLs
|
||||
/^\/[a-zA-Z0-9_\-./]*$/, // File paths
|
||||
/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/, // File extensions, class names
|
||||
/^@[a-zA-Z0-9/-]+$/, // Import paths
|
||||
/^#\/[a-zA-Z0-9/-]+$/, // Alias imports
|
||||
/^[a-zA-Z0-9/-]+\/[a-zA-Z0-9/-]+$/, // Module paths
|
||||
/^data:image\/[a-zA-Z0-9;,]+$/, // Data URLs
|
||||
/^application\/[a-zA-Z0-9-]+$/, // MIME types
|
||||
/^!\[image]\(data:image\/png;base64,$/, // Markdown image with base64 data
|
||||
|
||||
// Numbers, IDs, and technical values
|
||||
/^\d+(\.\d+)?$/, // Numbers
|
||||
/^#[0-9a-fA-F]{3,8}$/, // Color codes
|
||||
/^[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+$/, // Key-value pairs
|
||||
/^mm:ss$/, // Time format
|
||||
/^[a-zA-Z0-9]+\/[a-zA-Z0-9-]+$/, // Provider/model format
|
||||
/^\?[a-zA-Z0-9_-]+$/, // URL parameters
|
||||
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i, // UUID
|
||||
/^[A-Za-z0-9+/=]+$/, // Base64
|
||||
|
||||
// HTML and CSS selectors
|
||||
/^[a-z]+(\[[^\]]+\])+$/, // CSS attribute selectors
|
||||
/^[a-z]+:[a-z-]+$/, // CSS pseudo-selectors
|
||||
/^[a-z]+\.[a-z0-9_-]+$/, // CSS class selectors
|
||||
/^[a-z]+#[a-z0-9_-]+$/, // CSS ID selectors
|
||||
/^[a-z]+\s*>\s*[a-z]+$/, // CSS child selectors
|
||||
/^[a-z]+\s+[a-z]+$/, // CSS descendant selectors
|
||||
|
||||
// CSS and styling patterns
|
||||
/^[a-z0-9-]+:[a-z0-9-]+$/, // CSS property:value
|
||||
/^[a-z0-9-]+:[a-z0-9-]+;[a-z0-9-]+:[a-z0-9-]+$/, // Multiple CSS properties
|
||||
];
|
||||
|
||||
// File extensions and media types
|
||||
const fileExtensionPattern =
|
||||
/^\.(png|jpg|jpeg|gif|svg|webp|bmp|ico|pdf|mp4|webm|ogg|mp3|wav|json|xml|csv|txt|md|html|css|js|jsx|ts|tsx)$/i;
|
||||
if (fileExtensionPattern.test(str)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// AI model and provider patterns
|
||||
const aiRelatedPattern =
|
||||
/^(AI|OpenAI|VertexAI|PaLM|Gemini|Anthropic|Anyscale|Databricks|Ollama|FriendliAI|Groq|DeepInfra|AI21|Replicate|OpenRouter|Azure|AWS|SageMaker|Bedrock|Mistral|Perplexity|Fireworks|Cloudflare|Workers|Voyage|claude-|gpt-|o1-|o3-)/i;
|
||||
if (aiRelatedPattern.test(str)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CSS units and values
|
||||
const cssUnitsPattern =
|
||||
/(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
|
||||
const cssValuesPattern =
|
||||
/(rgb|rgba|hsl|hsla|#[0-9a-fA-F]+|solid|absolute|relative|sticky|fixed|static|block|inline|flex|grid|none|auto|hidden|visible)/;
|
||||
|
||||
if (cssUnitsPattern.test(str) || cssValuesPattern.test(str)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for CSS class strings with brackets (common in the codebase)
|
||||
if (
|
||||
str.includes("[") &&
|
||||
str.includes("]") &&
|
||||
(str.includes("px") ||
|
||||
str.includes("rem") ||
|
||||
str.includes("em") ||
|
||||
str.includes("w-") ||
|
||||
str.includes("h-") ||
|
||||
str.includes("p-") ||
|
||||
str.includes("m-"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for CSS class strings with specific patterns
|
||||
if (
|
||||
str.includes("border-") ||
|
||||
str.includes("rounded-") ||
|
||||
str.includes("cursor-") ||
|
||||
str.includes("opacity-") ||
|
||||
str.includes("disabled:") ||
|
||||
str.includes("hover:") ||
|
||||
str.includes("focus-within:") ||
|
||||
str.includes("first-of-type:") ||
|
||||
str.includes("last-of-type:") ||
|
||||
str.includes("group-data-")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it looks like a Tailwind class string
|
||||
if (/^[a-z0-9-]+(\s+[a-z0-9-]+)*$/.test(str)) {
|
||||
// Common Tailwind prefixes and patterns
|
||||
const tailwindPrefixes = [
|
||||
"bg-", "text-", "border-", "rounded-", "p-", "m-", "px-", "py-", "mx-", "my-",
|
||||
"w-", "h-", "min-w-", "min-h-", "max-w-", "max-h-", "flex-", "grid-", "gap-",
|
||||
"space-", "items-", "justify-", "self-", "col-", "row-", "order-", "object-",
|
||||
"overflow-", "opacity-", "z-", "top-", "right-", "bottom-", "left-", "inset-",
|
||||
"font-", "tracking-", "leading-", "list-", "placeholder-", "shadow-", "ring-",
|
||||
"transition-", "duration-", "ease-", "delay-", "animate-", "scale-", "rotate-",
|
||||
"translate-", "skew-", "origin-", "cursor-", "select-", "resize-", "fill-", "stroke-",
|
||||
];
|
||||
|
||||
// Check if any word in the string starts with a Tailwind prefix
|
||||
const words = str.split(/\s+/);
|
||||
for (const word of words) {
|
||||
for (const prefix of tailwindPrefixes) {
|
||||
if (word.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Tailwind modifiers
|
||||
const tailwindModifiers = [
|
||||
"hover:", "focus:", "active:", "disabled:", "visited:", "first:", "last:",
|
||||
"odd:", "even:", "group-hover:", "focus-within:", "focus-visible:", "motion-safe:",
|
||||
"motion-reduce:", "dark:", "light:", "sm:", "md:", "lg:", "xl:", "2xl:",
|
||||
];
|
||||
|
||||
for (const word of words) {
|
||||
for (const modifier of tailwindModifiers) {
|
||||
if (word.includes(modifier)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for CSS property combinations
|
||||
const cssProperties = [
|
||||
"border", "rounded", "px", "py", "mx", "my", "p", "m", "w", "h", "flex",
|
||||
"grid", "gap", "transition", "duration", "font", "leading", "tracking",
|
||||
];
|
||||
|
||||
// If the string contains multiple CSS properties, it's likely a CSS class string
|
||||
let cssPropertyCount = 0;
|
||||
for (const word of words) {
|
||||
if (
|
||||
cssProperties.some(
|
||||
(prop) => word === prop || word.startsWith(`${prop}-`),
|
||||
)
|
||||
) {
|
||||
cssPropertyCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (cssPropertyCount >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for specific CSS class patterns that appear in the test failures
|
||||
if (
|
||||
str.match(
|
||||
/^(border|rounded|flex|grid|transition|duration|ease|hover:|focus:|active:|disabled:|placeholder:|text-|bg-|w-|h-|p-|m-|gap-|items-|justify-|self-|overflow-|cursor-|opacity-|z-|top-|right-|bottom-|left-|inset-|font-|tracking-|leading-|whitespace-|break-|truncate|shadow-|ring-|outline-|animate-|transform|rotate-|scale-|skew-|translate-|origin-|first-of-type:|last-of-type:|group-data-|max-|min-|px-|py-|mx-|my-|grow|shrink|resize-|underline|italic|normal)/,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// HTML tags and attributes
|
||||
if (
|
||||
/^<[a-z0-9]+>.*<\/[a-z0-9]+>$/.test(str) ||
|
||||
/^<[a-z0-9]+ [^>]+\/>$/.test(str)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific patterns in suggestions and examples
|
||||
if (
|
||||
str.includes("* ") &&
|
||||
(str.includes("create a") ||
|
||||
str.includes("build a") ||
|
||||
str.includes("make a"))
|
||||
) {
|
||||
// This is likely a suggestion or example, not a UI string
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for specific technical identifiers from the test failures
|
||||
if (
|
||||
/^(download_via_vscode_button_clicked|open-vscode-error-|set-indicator|settings_saved|openhands-trace-|provider-item-|last_browser_action_error)$/.test(
|
||||
str,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for URL paths and query parameters
|
||||
if (
|
||||
str.startsWith("?") ||
|
||||
str.startsWith("/") ||
|
||||
str.includes("auth.") ||
|
||||
str.includes("$1auth.")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific strings that should be excluded
|
||||
if (
|
||||
str === "Cache Hit:" ||
|
||||
str === "Cache Write:" ||
|
||||
str === "ADD_DOCS" ||
|
||||
str === "ADD_DOCKERFILE" ||
|
||||
str === "Verified" ||
|
||||
str === "Others" ||
|
||||
str === "Feedback" ||
|
||||
str === "JSON File" ||
|
||||
str === "mt-0.5 md:mt-0"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for long suggestion texts
|
||||
if (
|
||||
str.length > 100 &&
|
||||
(str.includes("Please write a bash script") ||
|
||||
str.includes("Please investigate the repo") ||
|
||||
str.includes("Please push the changes") ||
|
||||
str.includes("Examine the dependencies") ||
|
||||
str.includes("Investigate the documentation") ||
|
||||
str.includes("Investigate the current repo") ||
|
||||
str.includes("I want to create a Hello World app") ||
|
||||
str.includes("I want to create a VueJS app") ||
|
||||
str.includes("This should be a client-only app"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific error messages and UI text
|
||||
if (
|
||||
str === "All data associated with this project will be lost." ||
|
||||
str === "You will lose any unsaved information." ||
|
||||
str ===
|
||||
"This conversation does not exist, or you do not have permission to access it." ||
|
||||
str === "Failed to fetch settings. Please try reloading." ||
|
||||
str ===
|
||||
"If you tell OpenHands to start a web server, the app will appear here." ||
|
||||
str ===
|
||||
"Your browser doesn't support downloading files. Please use Chrome, Edge, or another browser that supports the File System Access API." ||
|
||||
str ===
|
||||
"Something went wrong while fetching settings. Please reload the page." ||
|
||||
str ===
|
||||
"To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data." ||
|
||||
str === "Please push the latest changes to the existing pull request."
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check against all technical patterns
|
||||
return technicalPatterns.some((pattern) => pattern.test(str));
|
||||
}
|
||||
|
||||
function isLikelyUserFacingText(str) {
|
||||
// Basic validation - skip very short strings or strings without letters
|
||||
if (!str || str.length <= 2 || !/[a-zA-Z]/.test(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a specifically excluded technical string
|
||||
if (isExcludedTechnicalString(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a raw translation key that should be wrapped in t()
|
||||
if (isRawTranslationKey(str)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a translation key pattern (e.g., "SETTINGS$BASE_URL")
|
||||
// These should be wrapped in t() or use I18nKey enum
|
||||
if (isLikelyTranslationKey(str) && /^[A-Z0-9_]+\$[A-Z0-9_]+$/.test(str)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// First, check if it's a common development string (not user-facing)
|
||||
if (isCommonDevelopmentString(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Multi-word phrases are likely UI text
|
||||
const hasMultipleWords = /\s+/.test(str) && str.split(/\s+/).length > 1;
|
||||
|
||||
// Sentences and questions are likely UI text
|
||||
const hasPunctuation = /[?!.,:]/.test(str);
|
||||
const isCapitalizedPhrase = /^[A-Z]/.test(str) && hasMultipleWords;
|
||||
const isTitleCase = hasMultipleWords && /\s[A-Z]/.test(str);
|
||||
const hasSentenceStructure = /^[A-Z].*[.!?]$/.test(str); // Starts with capital, ends with punctuation
|
||||
const hasQuestionForm =
|
||||
/^(What|How|Why|When|Where|Who|Can|Could|Would|Will|Is|Are|Do|Does|Did|Should|May|Might)/.test(
|
||||
str,
|
||||
);
|
||||
|
||||
// Product names and camelCase identifiers are likely UI text
|
||||
const hasInternalCapitals = /[a-z][A-Z]/.test(str); // CamelCase product names
|
||||
|
||||
// Instruction text patterns are likely UI text
|
||||
const looksLikeInstruction =
|
||||
/^(Enter|Type|Select|Choose|Provide|Specify|Search|Find|Input|Add|Write|Describe|Set|Pick|Browse|Upload|Download|Click|Tap|Press|Go to|Visit|Open|Close)/i.test(
|
||||
str,
|
||||
);
|
||||
|
||||
// Error and status messages are likely UI text
|
||||
const looksLikeErrorOrStatus =
|
||||
/(failed|error|invalid|required|missing|incorrect|wrong|unavailable|not found|not available|try again|success|completed|finished|done|saved|updated|created|deleted|removed|added)/i.test(
|
||||
str,
|
||||
);
|
||||
|
||||
// Single word check - assume it's UI text unless proven otherwise
|
||||
const isSingleWord =
|
||||
!str.includes(" ") && str.length > 1 && /^[a-zA-Z]+$/.test(str);
|
||||
|
||||
// For single words, we need to be more careful
|
||||
if (isSingleWord) {
|
||||
// Skip common programming terms and variable names
|
||||
const isCommonProgrammingTerm =
|
||||
/^(null|undefined|true|false|function|class|interface|type|enum|const|let|var|return|import|export|default|async|await|try|catch|finally|throw|new|this|super|extends|implements|instanceof|typeof|void|delete|in|of|for|while|do|if|else|switch|case|break|continue|yield|static|get|set|public|private|protected|readonly|abstract|implements|namespace|module|declare|as|from|with)$/i.test(
|
||||
str,
|
||||
);
|
||||
|
||||
if (isCommonProgrammingTerm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip common variable name patterns
|
||||
const looksLikeVariableName =
|
||||
/^[a-z][a-zA-Z0-9]*$/.test(str) && str.length <= 20;
|
||||
|
||||
if (looksLikeVariableName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip common CSS values
|
||||
const isCommonCssValue =
|
||||
/^(auto|none|hidden|visible|block|inline|flex|grid|row|column|wrap|nowrap|center|start|end|stretch|cover|contain|fixed|absolute|relative|static|sticky|pointer|default|inherit|initial|unset)$/i.test(
|
||||
str,
|
||||
);
|
||||
|
||||
if (isCommonCssValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip common file extensions
|
||||
const isFileExtension = /^\.[a-z0-9]+$/i.test(str);
|
||||
if (isFileExtension) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip common abbreviations
|
||||
const isCommonAbbreviation =
|
||||
/^(id|src|href|url|alt|img|btn|nav|div|span|ul|li|ol|dl|dt|dd|svg|png|jpg|gif|pdf|doc|txt|md|js|ts|jsx|tsx|css|scss|less|html|xml|json|yaml|yml|toml|csv|mp3|mp4|wav|avi|mov|mpeg|webm|webp|ttf|woff|eot|otf)$/i.test(
|
||||
str,
|
||||
);
|
||||
|
||||
if (isCommonAbbreviation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's a single word that's not a programming term, variable name, CSS value, file extension, or abbreviation,
|
||||
// it might be UI text, but we'll be conservative and return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it has multiple words, punctuation, or looks like a sentence, it's likely UI text
|
||||
return (
|
||||
hasMultipleWords ||
|
||||
hasPunctuation ||
|
||||
isCapitalizedPhrase ||
|
||||
isTitleCase ||
|
||||
hasSentenceStructure ||
|
||||
hasQuestionForm ||
|
||||
hasInternalCapitals ||
|
||||
looksLikeInstruction ||
|
||||
looksLikeErrorOrStatus
|
||||
);
|
||||
}
|
||||
|
||||
function isInTranslationContext(path) {
|
||||
// Check if the JSX text is inside a <Trans> component
|
||||
let current = path;
|
||||
while (current.parentPath) {
|
||||
if (
|
||||
current.isJSXElement() &&
|
||||
current.node.openingElement &&
|
||||
current.node.openingElement.name &&
|
||||
current.node.openingElement.name.name === "Trans"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
current = current.parentPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function scanFileForUnlocalizedStrings(filePath) {
|
||||
// Skip all suggestion files as they contain special strings
|
||||
if (filePath.includes("suggestions")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const unlocalizedStrings = [];
|
||||
|
||||
// Skip files that are too large
|
||||
if (content.length > 1000000) {
|
||||
console.warn(`Skipping large file: ${filePath}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the file
|
||||
const ast = parser.parse(content, {
|
||||
sourceType: "module",
|
||||
plugins: ["jsx", "typescript", "classProperties", "decorators-legacy"],
|
||||
});
|
||||
|
||||
// Traverse the AST
|
||||
traverse(ast, {
|
||||
// Find JSX text content
|
||||
JSXText(jsxTextPath) {
|
||||
const text = jsxTextPath.node.value.trim();
|
||||
if (
|
||||
text &&
|
||||
isLikelyUserFacingText(text) &&
|
||||
!isInTranslationContext(jsxTextPath)
|
||||
) {
|
||||
unlocalizedStrings.push(text);
|
||||
}
|
||||
},
|
||||
|
||||
// Find string literals in JSX attributes
|
||||
JSXAttribute(jsxAttrPath) {
|
||||
const attrName = jsxAttrPath.node.name.name.toString();
|
||||
|
||||
// Skip technical attributes that don't contain user-facing text
|
||||
if (NON_TEXT_ATTRIBUTES.includes(attrName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip styling attributes
|
||||
if (
|
||||
attrName === "className" ||
|
||||
attrName === "class" ||
|
||||
attrName === "style"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip data attributes and event handlers
|
||||
if (attrName.startsWith("data-") || attrName.startsWith("on")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the attribute value
|
||||
const value = jsxAttrPath.node.value;
|
||||
if (value && value.type === "StringLiteral") {
|
||||
const text = value.value.trim();
|
||||
if (text && isLikelyUserFacingText(text)) {
|
||||
unlocalizedStrings.push(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Find string literals in code
|
||||
StringLiteral(stringPath) {
|
||||
// Skip if parent is JSX attribute (already handled above)
|
||||
if (stringPath.parent.type === "JSXAttribute") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if parent is import/export declaration
|
||||
if (
|
||||
stringPath.parent.type === "ImportDeclaration" ||
|
||||
stringPath.parent.type === "ExportDeclaration"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if parent is object property key
|
||||
if (
|
||||
stringPath.parent.type === "ObjectProperty" &&
|
||||
stringPath.parent.key === stringPath.node
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if inside a t() call or Trans component
|
||||
let isInsideTranslation = false;
|
||||
let current = stringPath;
|
||||
|
||||
while (current.parentPath && !isInsideTranslation) {
|
||||
// Check for t() function call
|
||||
if (
|
||||
current.parent.type === "CallExpression" &&
|
||||
current.parent.callee &&
|
||||
((current.parent.callee.type === "Identifier" &&
|
||||
current.parent.callee.name === "t") ||
|
||||
(current.parent.callee.type === "MemberExpression" &&
|
||||
current.parent.callee.property &&
|
||||
current.parent.callee.property.name === "t"))
|
||||
) {
|
||||
isInsideTranslation = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for <Trans> component
|
||||
if (
|
||||
current.parent.type === "JSXElement" &&
|
||||
current.parent.openingElement &&
|
||||
current.parent.openingElement.name &&
|
||||
current.parent.openingElement.name.name === "Trans"
|
||||
) {
|
||||
isInsideTranslation = true;
|
||||
break;
|
||||
}
|
||||
|
||||
current = current.parentPath;
|
||||
}
|
||||
|
||||
if (!isInsideTranslation) {
|
||||
const text = stringPath.node.value.trim();
|
||||
if (text && isLikelyUserFacingText(text)) {
|
||||
unlocalizedStrings.push(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return unlocalizedStrings;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing file ${filePath}:`, error);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${filePath}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function scanDirectoryForUnlocalizedStrings(dirPath) {
|
||||
const results = new Map();
|
||||
|
||||
function scanDir(currentPath) {
|
||||
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
|
||||
if (!shouldIgnorePath(fullPath)) {
|
||||
if (entry.isDirectory()) {
|
||||
scanDir(fullPath);
|
||||
} else if (
|
||||
entry.isFile() &&
|
||||
SCAN_EXTENSIONS.includes(path.extname(fullPath))
|
||||
) {
|
||||
const unlocalized = scanFileForUnlocalizedStrings(fullPath);
|
||||
if (unlocalized.length > 0) {
|
||||
results.set(fullPath, unlocalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDir(dirPath);
|
||||
return results;
|
||||
}
|
||||
|
||||
// Run the check
|
||||
try {
|
||||
const srcPath = path.resolve(__dirname, '../src');
|
||||
console.log('Checking for unlocalized strings in frontend code...');
|
||||
|
||||
// Get unlocalized strings using the AST scanner
|
||||
const results = scanDirectoryForUnlocalizedStrings(srcPath);
|
||||
|
||||
// If we found any unlocalized strings, format them for output and exit with error
|
||||
if (results.size > 0) {
|
||||
const formattedResults = Array.from(results.entries())
|
||||
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
|
||||
.join('\n');
|
||||
|
||||
console.error(`Error: Found unlocalized strings in the following files:${formattedResults}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ No unlocalized strings found in frontend code.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error running unlocalized strings check:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
GetTrajectoryResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { GitUser, GitRepository } from "#/types/git";
|
||||
|
||||
class OpenHands {
|
||||
@@ -177,6 +178,33 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the settings from the server or use the default settings if not found
|
||||
*/
|
||||
static async getSettings(): Promise<ApiSettings> {
|
||||
const { data } = await openHands.get<ApiSettings>("/api/settings");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the settings to the server. Only valid settings are saved.
|
||||
* @param settings - the settings to save
|
||||
*/
|
||||
static async saveSettings(
|
||||
settings: Partial<PostApiSettings>,
|
||||
): Promise<boolean> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user settings in server
|
||||
*/
|
||||
static async resetSettings(): Promise<boolean> {
|
||||
const response = await openHands.post("/api/reset-settings");
|
||||
return response.status === 200;
|
||||
}
|
||||
|
||||
static async createCheckoutSession(amount: number): Promise<string> {
|
||||
const { data } = await openHands.post(
|
||||
"/api/billing/create-checkout-session",
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { UserSettings } from "./settings-service.types";
|
||||
|
||||
export class SettingsService {
|
||||
/**
|
||||
* Get the user's settings
|
||||
*/
|
||||
static async getSettings(): Promise<UserSettings> {
|
||||
const { data } = await openHands.get<UserSettings>("/api/settings");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save valid settings to the server
|
||||
* @param settings - The settings to save
|
||||
*/
|
||||
static async saveSettings(settings: Partial<UserSettings>): Promise<boolean> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the user's settings
|
||||
*/
|
||||
static async resetSettings(): Promise<boolean> {
|
||||
const response = await openHands.post("/api/settings/reset");
|
||||
return response.status === 200;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
export type GitProvider = "github" | "gitlab";
|
||||
|
||||
export interface UserSettings {
|
||||
llm_model: string;
|
||||
llm_base_url: string;
|
||||
agent: string;
|
||||
language: string;
|
||||
llm_api_key_set: boolean;
|
||||
confirmation_mode: boolean;
|
||||
security_analyzer: string;
|
||||
remote_runtime_resource_factor: number | null;
|
||||
github_token_is_set: boolean;
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens_set: Record<GitProvider, boolean>;
|
||||
}
|
||||
|
||||
// These are settings that are only used on the client side and should not be sent to the server
|
||||
export interface ClientUserSettings extends UserSettings {
|
||||
is_new_user: boolean;
|
||||
}
|
||||
|
||||
// These are settings that are used on the server side and should be sent to the server
|
||||
export interface ServerUserSettings extends UserSettings {
|
||||
github_token: string | null;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export function ChatMessage({
|
||||
onClick={handleCopyToClipboard}
|
||||
mode={isCopy ? "copied" : "copy"}
|
||||
/>
|
||||
<div className="text-sm overflow-auto break-words">
|
||||
<div className="text-sm break-words">
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
|
||||
@@ -189,7 +189,7 @@ export function ExpandableMessage({
|
||||
)}
|
||||
</div>
|
||||
{showDetails && (
|
||||
<div className="text-sm overflow-auto">
|
||||
<div className="text-sm">
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { JoinWaitlistAnchor } from "./join-waitlist-anchor";
|
||||
import { WaitlistMessage } from "./waitlist-message";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { TOSCheckbox } from "./tos-checkbox";
|
||||
@@ -11,15 +9,11 @@ import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
|
||||
interface WaitlistModalProps {
|
||||
ghTokenIsSet: boolean;
|
||||
interface AuthModalProps {
|
||||
githubAuthUrl: string | null;
|
||||
}
|
||||
|
||||
export function WaitlistModal({
|
||||
ghTokenIsSet,
|
||||
githubAuthUrl,
|
||||
}: WaitlistModalProps) {
|
||||
export function AuthModal({ githubAuthUrl }: AuthModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||
|
||||
@@ -34,23 +28,24 @@ export function WaitlistModal({
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="border border-tertiary">
|
||||
<AllHandsLogo width={68} height={46} />
|
||||
<WaitlistMessage content={ghTokenIsSet ? "waitlist" : "sign-in"} />
|
||||
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{t(I18nKey.AUTH$SIGN_IN_WITH_GITHUB)}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
|
||||
|
||||
{!ghTokenIsSet && (
|
||||
<BrandButton
|
||||
isDisabled={!isTosAccepted}
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleGitHubAuth}
|
||||
className="w-full"
|
||||
startContent={<GitHubLogo width={20} height={20} />}
|
||||
>
|
||||
{t(I18nKey.GITHUB$CONNECT_TO_GITHUB)}
|
||||
</BrandButton>
|
||||
)}
|
||||
{ghTokenIsSet && <JoinWaitlistAnchor />}
|
||||
<BrandButton
|
||||
isDisabled={!isTosAccepted}
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleGitHubAuth}
|
||||
className="w-full"
|
||||
startContent={<GitHubLogo width={20} height={20} />}
|
||||
>
|
||||
{t(I18nKey.GITHUB$CONNECT_TO_GITHUB)}
|
||||
</BrandButton>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function JoinWaitlistAnchor() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a
|
||||
href="https://www.all-hands.dev/join-waitlist"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded bg-[#FFE165] text-black text-sm font-bold py-[10px] w-full text-center hover:opacity-80"
|
||||
>
|
||||
{t(I18nKey.WAITLIST$JOIN_WAITLIST)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface WaitlistMessageProps {
|
||||
content: "waitlist" | "sign-in";
|
||||
}
|
||||
|
||||
export function WaitlistMessage({ content }: WaitlistMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{content === "sign-in" && t(I18nKey.AUTH$SIGN_IN_WITH_GITHUB)}
|
||||
{content === "waitlist" && t(I18nKey.WAITLIST$ALMOST_THERE)}
|
||||
</h1>
|
||||
{content === "sign-in" && (
|
||||
<p>
|
||||
{t(I18nKey.LANDING$OR)}{" "}
|
||||
<a
|
||||
href="https://www.all-hands.dev/join-waitlist"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="text-blue-500 hover:underline underline-offset-2"
|
||||
>
|
||||
{t(I18nKey.WAITLIST$JOIN)}
|
||||
</a>{" "}
|
||||
{t(I18nKey.WAITLIST$IF_NOT_JOINED)}
|
||||
</p>
|
||||
)}
|
||||
{content === "waitlist" && (
|
||||
<p className="text-sm">{t(I18nKey.WAITLIST$PATIENCE_MESSAGE)}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,16 +9,15 @@ import { extractSettings } from "#/utils/settings-utils";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
import { Settings } from "#/types/settings";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { HelpLink } from "#/components/features/settings/help-link";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
interface SettingsFormProps {
|
||||
settings: UserSettings | undefined;
|
||||
settings: Settings;
|
||||
models: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -43,16 +42,17 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
|
||||
const handleFormSubmission = async (formData: FormData) => {
|
||||
const newSettings = extractSettings(formData);
|
||||
saveUserSettings(newSettings, {
|
||||
|
||||
await saveUserSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
resetOngoingSession();
|
||||
|
||||
posthog.capture("settings_saved", {
|
||||
LLM_MODEL: newSettings.llm_model,
|
||||
LLM_API_KEY: newSettings.llm_api_key_set ? "SET" : "UNSET",
|
||||
LLM_MODEL: newSettings.LLM_MODEL,
|
||||
LLM_API_KEY_SET: newSettings.LLM_API_KEY_SET ? "SET" : "UNSET",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
newSettings.remote_runtime_resource_factor,
|
||||
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -74,7 +74,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const isLLMKeySet = !!settings?.llm_api_key_set;
|
||||
const isLLMKeySet = settings.LLM_API_KEY_SET;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -87,7 +87,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
<div className="flex flex-col gap-4">
|
||||
<ModelSelector
|
||||
models={organizeModelsAndProviders(models)}
|
||||
currentModel={settings?.llm_model || DEFAULT_SETTINGS.llm_model}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
/>
|
||||
|
||||
<SettingsInput
|
||||
|
||||
@@ -5,10 +5,11 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import { LoadingSpinner } from "../../loading-spinner";
|
||||
import { ModalBackdrop } from "../modal-backdrop";
|
||||
import { SettingsForm } from "./settings-form";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { Settings } from "#/types/settings";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
interface SettingsModalProps {
|
||||
settings: UserSettings | undefined;
|
||||
settings?: Settings;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
|
||||
)}
|
||||
{aiConfigOptions.data && (
|
||||
<SettingsForm
|
||||
settings={settings}
|
||||
settings={settings || DEFAULT_SETTINGS}
|
||||
models={aiConfigOptions.data?.models}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { PostSettings, PostApiSettings } from "#/types/settings";
|
||||
import { useSettings } from "../query/use-settings";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
|
||||
const saveSettingsMutationFn = async (
|
||||
settings: Partial<UserSettings> | null,
|
||||
settings: Partial<PostSettings> | null,
|
||||
) => {
|
||||
// If settings is null, we're resetting
|
||||
if (settings === null) {
|
||||
await SettingsService.resetSettings();
|
||||
await OpenHands.resetSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
const safeSettings: Partial<UserSettings> = {
|
||||
...settings,
|
||||
agent: settings.agent || DEFAULT_SETTINGS.agent,
|
||||
language: settings.language || DEFAULT_SETTINGS.language,
|
||||
llm_api_key_set:
|
||||
settings.llm_api_key_set === ""
|
||||
const apiSettings: Partial<PostApiSettings> = {
|
||||
llm_model: settings.LLM_MODEL,
|
||||
llm_base_url: settings.LLM_BASE_URL,
|
||||
agent: settings.AGENT || DEFAULT_SETTINGS.AGENT,
|
||||
language: settings.LANGUAGE || DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: settings.CONFIRMATION_MODE,
|
||||
security_analyzer: settings.SECURITY_ANALYZER,
|
||||
llm_api_key:
|
||||
settings.llm_api_key === ""
|
||||
? ""
|
||||
: settings.llm_api_key_set?.trim() || undefined,
|
||||
: settings.llm_api_key?.trim() || undefined,
|
||||
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: settings.user_consents_to_analytics,
|
||||
provider_tokens: settings.provider_tokens,
|
||||
};
|
||||
|
||||
await SettingsService.saveSettings(safeSettings);
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
};
|
||||
|
||||
export const useSaveSettings = () => {
|
||||
@@ -31,7 +39,7 @@ export const useSaveSettings = () => {
|
||||
const { data: currentSettings } = useSettings();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: Partial<UserSettings> | null) => {
|
||||
mutationFn: async (settings: Partial<PostSettings> | null) => {
|
||||
if (settings === null) {
|
||||
await saveSettingsMutationFn(null);
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { SettingsService } from "#/api/settings-service/settings-service.api";
|
||||
import { ClientUserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
const settingsQueryFn = async (): Promise<ClientUserSettings> => {
|
||||
const settings = await SettingsService.getSettings();
|
||||
return { ...settings, is_new_user: false };
|
||||
const getSettingsQueryFn = async () => {
|
||||
const apiSettings = await OpenHands.getSettings();
|
||||
|
||||
return {
|
||||
LLM_MODEL: apiSettings.llm_model,
|
||||
LLM_BASE_URL: apiSettings.llm_base_url,
|
||||
AGENT: apiSettings.agent,
|
||||
LANGUAGE: apiSettings.language,
|
||||
CONFIRMATION_MODE: apiSettings.confirmation_mode,
|
||||
SECURITY_ANALYZER: apiSettings.security_analyzer,
|
||||
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
|
||||
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
|
||||
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
|
||||
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
|
||||
PROVIDER_TOKENS: apiSettings.provider_tokens,
|
||||
IS_NEW_USER: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
@@ -17,7 +32,7 @@ export const useSettings = () => {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["settings", providerTokensSet],
|
||||
queryFn: settingsQueryFn,
|
||||
queryFn: getSettingsQueryFn,
|
||||
// Only retry if the error is not a 404 because we
|
||||
// would want to show the modal immediately if the
|
||||
// settings are not found
|
||||
@@ -30,36 +45,42 @@ export const useSettings = () => {
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.isFetched && query.data?.llm_api_key_set) {
|
||||
if (query.isFetched && query.data?.LLM_API_KEY_SET) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [query.data?.llm_api_key_set, query.isFetched]);
|
||||
}, [query.data?.LLM_API_KEY_SET, query.isFetched]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.isFetched && query.data?.provider_tokens_set) {
|
||||
const providers = query.data.provider_tokens_set;
|
||||
if (query.data?.PROVIDER_TOKENS_SET) {
|
||||
const providers = query.data.PROVIDER_TOKENS_SET;
|
||||
const setProviders = (
|
||||
Object.keys(providers) as Array<keyof typeof providers>
|
||||
).filter((key) => providers[key]);
|
||||
setProviderTokensSet(setProviders);
|
||||
const atLeastOneSet = Object.values(query.data.provider_tokens_set).some(
|
||||
const atLeastOneSet = Object.values(query.data.PROVIDER_TOKENS_SET).some(
|
||||
(value) => value,
|
||||
);
|
||||
setProvidersAreSet(atLeastOneSet);
|
||||
}
|
||||
}, [query.data?.provider_tokens_set, query.isFetched]);
|
||||
}, [query.data?.PROVIDER_TOKENS_SET, query.isFetched]);
|
||||
|
||||
// We want to return the defaults if the settings aren't found so the user can still see the
|
||||
// options to make their initial save. We don't set the defaults in `initialData` above because
|
||||
// that would prepopulate the data to the cache and mess with expectations. Read more:
|
||||
// https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#using-initialdata-to-prepopulate-a-query
|
||||
if (query.error?.status === 404) {
|
||||
// Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.
|
||||
// Only return the specific properties we need to avoid this.
|
||||
// Create a new object with only the properties we need, avoiding rest destructuring
|
||||
return {
|
||||
data: DEFAULT_SETTINGS,
|
||||
error: query.error,
|
||||
isError: query.isError,
|
||||
isLoading: query.isLoading,
|
||||
isFetched: query.isFetched,
|
||||
isFetching: query.isFetching,
|
||||
isFetched: query.isFetched,
|
||||
isSuccess: query.isSuccess,
|
||||
status: query.status,
|
||||
fetchStatus: query.fetchStatus,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useMigrateUserConsent = () => {
|
||||
if (userAnalyticsConsent) {
|
||||
args?.handleAnalyticsWasPresentInLocalStorage();
|
||||
|
||||
saveUserSettings(
|
||||
await saveUserSettings(
|
||||
{ user_consents_to_analytics: userAnalyticsConsent === "true" },
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useNotification = () => {
|
||||
// 4. Not a settings-related notification
|
||||
if (
|
||||
options?.playSound === true && // Must be explicitly true
|
||||
settings?.enable_sound_notifications &&
|
||||
settings?.ENABLE_SOUND_NOTIFICATIONS &&
|
||||
audioRef.current &&
|
||||
!title.includes("BUTTON$") // Don't play for button/settings actions
|
||||
) {
|
||||
@@ -49,7 +49,7 @@ export const useNotification = () => {
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[settings?.enable_sound_notifications],
|
||||
[settings?.ENABLE_SOUND_NOTIFICATIONS],
|
||||
);
|
||||
|
||||
return { notify };
|
||||
|
||||
@@ -301,6 +301,7 @@ export enum I18nKey {
|
||||
STATUS$ERROR_LLM_SERVICE_UNAVAILABLE = "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE",
|
||||
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
|
||||
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
|
||||
STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION = "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION",
|
||||
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
|
||||
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
|
||||
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
|
||||
|
||||
@@ -4,11 +4,36 @@ import {
|
||||
Conversation,
|
||||
ResultSet,
|
||||
} from "#/api/open-hands.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { STRIPE_BILLING_HANDLERS } from "./billing-handlers";
|
||||
import { SETTINGS_HANDLERS } from "./settings-handlers";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { FILE_SERVICE_HANDLERS } from "./file-service-handlers";
|
||||
import { GitUser } from "#/types/git";
|
||||
|
||||
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
llm_base_url: DEFAULT_SETTINGS.LLM_BASE_URL,
|
||||
llm_api_key: null,
|
||||
llm_api_key_set: DEFAULT_SETTINGS.LLM_API_KEY_SET,
|
||||
agent: DEFAULT_SETTINGS.AGENT,
|
||||
language: DEFAULT_SETTINGS.LANGUAGE,
|
||||
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
|
||||
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
remote_runtime_resource_factor:
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens_set: DEFAULT_SETTINGS.PROVIDER_TOKENS_SET,
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
|
||||
provider_tokens: DEFAULT_SETTINGS.PROVIDER_TOKENS,
|
||||
};
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: ApiSettings | PostApiSettings | null;
|
||||
} = {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
const conversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
@@ -79,7 +104,6 @@ const openHandsHandlers = [
|
||||
|
||||
export const handlers = [
|
||||
...STRIPE_BILLING_HANDLERS,
|
||||
...SETTINGS_HANDLERS,
|
||||
...FILE_SERVICE_HANDLERS,
|
||||
...openHandsHandlers,
|
||||
http.get("/api/user/repositories", () =>
|
||||
@@ -122,6 +146,40 @@ export const handlers = [
|
||||
|
||||
return HttpResponse.json(config);
|
||||
}),
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
if (Object.keys(settings.provider_tokens_set).length > 0)
|
||||
settings.provider_tokens_set = { github: false, gitlab: false };
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
let newSettings: Partial<PostApiSettings> = {};
|
||||
if (typeof body === "object") {
|
||||
newSettings = { ...body };
|
||||
}
|
||||
|
||||
const fullSettings = {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
...MOCK_USER_PREFERENCES.settings,
|
||||
...newSettings,
|
||||
};
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = fullSettings;
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
|
||||
http.post("/api/authenticate", async () =>
|
||||
HttpResponse.json({ message: "Authenticated" }),
|
||||
),
|
||||
@@ -202,4 +260,10 @@ export const handlers = [
|
||||
}),
|
||||
|
||||
http.post("/api/logout", () => HttpResponse.json(null, { status: 200 })),
|
||||
|
||||
http.post("/api/reset-settings", async () => {
|
||||
await delay();
|
||||
MOCK_USER_PREFERENCES.settings = { ...MOCK_DEFAULT_USER_SETTINGS };
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { delay, http, HttpResponse } from "msw";
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: UserSettings | null;
|
||||
} = {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
export const SETTINGS_HANDLERS = [
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
if (Object.keys(settings.provider_tokens_set).length > 0)
|
||||
settings.github_token_is_set = true;
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
let newSettings: Partial<UserSettings> = {};
|
||||
|
||||
if (typeof body === "object") {
|
||||
newSettings = { ...body };
|
||||
}
|
||||
|
||||
const fullSettings: UserSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...MOCK_USER_PREFERENCES.settings,
|
||||
...newSettings,
|
||||
};
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = fullSettings;
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
|
||||
http.post("/api/settings/reset", async () => {
|
||||
await delay();
|
||||
MOCK_USER_PREFERENCES.settings = { ...DEFAULT_SETTINGS };
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}),
|
||||
];
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ServerUserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { ProviderOptions } from "#/types/settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
@@ -68,10 +67,9 @@ function AccountSettings() {
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
isCustomModel(resources.models, settings.llm_model) ||
|
||||
isCustomModel(resources.models, settings.LLM_MODEL) ||
|
||||
hasAdvancedSettingsSet({
|
||||
...settings,
|
||||
provider_tokens_set: settings.provider_tokens_set || {},
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -80,12 +78,12 @@ function AccountSettings() {
|
||||
};
|
||||
|
||||
const hasAppSlug = !!config?.APP_SLUG;
|
||||
const isLLMKeySet = settings?.llm_api_key_set === "**********";
|
||||
const isAnalyticsEnabled = settings?.user_consents_to_analytics;
|
||||
const isGitHubTokenSet =
|
||||
providerTokensSet.includes(ProviderOptions.github) || false;
|
||||
const isGitLabTokenSet =
|
||||
providerTokensSet.includes(ProviderOptions.gitlab) || false;
|
||||
const isLLMKeySet = settings?.LLM_API_KEY_SET;
|
||||
const isAnalyticsEnabled = settings?.USER_CONSENTS_TO_ANALYTICS;
|
||||
const isAdvancedSettingsSet = determineWhetherToToggleAdvancedSettings();
|
||||
|
||||
const modelsAndProviders = organizeModelsAndProviders(
|
||||
@@ -96,7 +94,7 @@ function AccountSettings() {
|
||||
"basic" | "advanced"
|
||||
>(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
const [confirmationModeIsEnabled, setConfirmationModeIsEnabled] =
|
||||
React.useState(!!settings?.security_analyzer);
|
||||
React.useState(!!settings?.SECURITY_ANALYZER);
|
||||
const [resetSettingsModalIsOpen, setResetSettingsModalIsOpen] =
|
||||
React.useState(false);
|
||||
|
||||
@@ -119,10 +117,6 @@ function AccountSettings() {
|
||||
const remoteRuntimeResourceFactor = REMOTE_RUNTIME_OPTIONS.find(
|
||||
({ label }) => label === rawRemoteRuntimeResourceFactor,
|
||||
)?.key;
|
||||
const remoteRuntimeResourceFactorValue = parseInt(
|
||||
remoteRuntimeResourceFactor || "",
|
||||
10,
|
||||
);
|
||||
|
||||
const userConsentsToAnalytics =
|
||||
formData.get("enable-analytics-switch")?.toString() === "on";
|
||||
@@ -148,7 +142,7 @@ function AccountSettings() {
|
||||
: llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const newSettings: Partial<ServerUserSettings> = {
|
||||
const newSettings = {
|
||||
provider_tokens:
|
||||
githubToken || gitlabToken
|
||||
? {
|
||||
@@ -156,20 +150,21 @@ function AccountSettings() {
|
||||
gitlab: gitlabToken || "",
|
||||
}
|
||||
: undefined,
|
||||
language: languageValue,
|
||||
LANGUAGE: languageValue,
|
||||
user_consents_to_analytics: userConsentsToAnalytics,
|
||||
enable_default_condenser: enableMemoryCondenser,
|
||||
enable_sound_notifications: enableSoundNotifications,
|
||||
llm_model: finalLlmModel,
|
||||
llm_base_url: finalLlmBaseUrl,
|
||||
llm_api_key_set: finalLlmApiKey,
|
||||
agent: formData.get("agent-input")?.toString(),
|
||||
security_analyzer:
|
||||
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
LLM_MODEL: finalLlmModel,
|
||||
LLM_BASE_URL: finalLlmBaseUrl,
|
||||
llm_api_key: finalLlmApiKey,
|
||||
AGENT: formData.get("agent-input")?.toString(),
|
||||
SECURITY_ANALYZER:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
remote_runtime_resource_factor:
|
||||
remoteRuntimeResourceFactorValue ||
|
||||
DEFAULT_SETTINGS.remote_runtime_resource_factor,
|
||||
confirmation_mode: confirmationModeIsEnabled,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor !== null
|
||||
? Number(remoteRuntimeResourceFactor)
|
||||
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
};
|
||||
|
||||
saveSettings(newSettings, {
|
||||
@@ -210,7 +205,7 @@ function AccountSettings() {
|
||||
setLlmConfigMode(isToggled ? "advanced" : "basic");
|
||||
if (!isToggled) {
|
||||
// reset advanced state
|
||||
setConfirmationModeIsEnabled(!!settings?.security_analyzer);
|
||||
setConfirmationModeIsEnabled(!!settings?.SECURITY_ANALYZER);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -254,7 +249,7 @@ function AccountSettings() {
|
||||
{llmConfigMode === "basic" && !shouldHandleSpecialSaasCase && (
|
||||
<ModelSelector
|
||||
models={modelsAndProviders}
|
||||
currentModel={settings.llm_model}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -263,7 +258,7 @@ function AccountSettings() {
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={settings.llm_model}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
@@ -274,7 +269,7 @@ function AccountSettings() {
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label={t(I18nKey.SETTINGS$BASE_URL)}
|
||||
defaultValue={settings.llm_base_url}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
@@ -315,7 +310,7 @@ function AccountSettings() {
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.agent}
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
@@ -334,7 +329,7 @@ function AccountSettings() {
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={settings.remote_runtime_resource_factor?.toString()}
|
||||
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
/>
|
||||
@@ -344,7 +339,7 @@ function AccountSettings() {
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
onToggle={setConfirmationModeIsEnabled}
|
||||
defaultIsToggled={!!settings.confirmation_mode}
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
@@ -355,7 +350,7 @@ function AccountSettings() {
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={!!settings.enable_default_condenser}
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
@@ -373,7 +368,7 @@ function AccountSettings() {
|
||||
label: analyzer,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.security_analyzer}
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
/>
|
||||
@@ -506,7 +501,7 @@ function AccountSettings() {
|
||||
key: language.value,
|
||||
label: language.label,
|
||||
}))}
|
||||
defaultSelectedKey={settings.language}
|
||||
defaultSelectedKey={settings.LANGUAGE}
|
||||
isClearable={false}
|
||||
/>
|
||||
|
||||
@@ -521,7 +516,7 @@ function AccountSettings() {
|
||||
<SettingsSwitch
|
||||
testId="enable-sound-notifications-switch"
|
||||
name="enable-sound-notifications-switch"
|
||||
defaultIsToggled={!!settings.enable_sound_notifications}
|
||||
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
|
||||
@@ -198,13 +198,13 @@ function AppContent() {
|
||||
|
||||
<Controls
|
||||
setSecurityOpen={onSecurityModalOpen}
|
||||
showSecurityLock={!!settings?.security_analyzer}
|
||||
showSecurityLock={!!settings?.SECURITY_ANALYZER}
|
||||
/>
|
||||
{settings && (
|
||||
<Security
|
||||
isOpen={securityModalIsOpen}
|
||||
onOpenChange={onSecurityModalOpenChange}
|
||||
securityAnalyzer={settings.security_analyzer}
|
||||
securityAnalyzer={settings.SECURITY_ANALYZER}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,9 @@ import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import { WaitlistModal } from "#/components/features/waitlist/waitlist-modal";
|
||||
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
|
||||
import { useBalance } from "#/hooks/query/use-balance";
|
||||
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
|
||||
@@ -60,8 +59,7 @@ export default function MainApp() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: settings, isFetched: settingsIsFetched } = useSettings();
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: settings } = useSettings();
|
||||
const { error, isFetching } = useBalance();
|
||||
const { migrateUserConsent } = useMigrateUserConsent();
|
||||
const { t } = useTranslation();
|
||||
@@ -81,17 +79,17 @@ export default function MainApp() {
|
||||
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settingsIsFetched && settings?.language) {
|
||||
i18n.changeLanguage(settings.language);
|
||||
if (settings?.LANGUAGE) {
|
||||
i18n.changeLanguage(settings.LANGUAGE);
|
||||
}
|
||||
}, [settingsIsFetched, settings?.language]);
|
||||
}, [settings?.LANGUAGE]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const consentFormModalIsOpen =
|
||||
settingsIsFetched && settings?.user_consents_to_analytics === null;
|
||||
settings?.USER_CONSENTS_TO_ANALYTICS === null;
|
||||
|
||||
setConsentFormIsOpen(consentFormModalIsOpen);
|
||||
}, [settingsIsFetched, settings?.user_consents_to_analytics]);
|
||||
}, [settings]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Migrate user consent to the server if it was previously stored in localStorage
|
||||
@@ -114,7 +112,7 @@ export default function MainApp() {
|
||||
}, [error?.status, pathname, isFetching]);
|
||||
|
||||
const userIsAuthed = !!isAuthed && !authError;
|
||||
const renderWaitlistModal =
|
||||
const renderAuthModal =
|
||||
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
|
||||
|
||||
return (
|
||||
@@ -131,13 +129,7 @@ export default function MainApp() {
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{renderWaitlistModal && (
|
||||
<WaitlistModal
|
||||
ghTokenIsSet={providersAreSet}
|
||||
githubAuthUrl={gitHubAuthUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{renderAuthModal && <AuthModal githubAuthUrl={gitHubAuthUrl} />}
|
||||
{config.data?.APP_MODE === "oss" && consentFormIsOpen && (
|
||||
<AnalyticsConsentFormModal
|
||||
onClose={() => {
|
||||
@@ -148,7 +140,7 @@ export default function MainApp() {
|
||||
|
||||
{config.data?.FEATURE_FLAGS.ENABLE_BILLING &&
|
||||
config.data?.APP_MODE === "saas" &&
|
||||
settings?.is_new_user && <SetupPaymentModal />}
|
||||
settings?.IS_NEW_USER && <SetupPaymentModal />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { ClientUserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
export const LATEST_SETTINGS_VERSION = 5;
|
||||
|
||||
export const DEFAULT_SETTINGS: ClientUserSettings = {
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
llm_base_url: "",
|
||||
agent: "CodeActAgent",
|
||||
language: "en",
|
||||
llm_api_key_set: null,
|
||||
confirmation_mode: false,
|
||||
security_analyzer: "",
|
||||
remote_runtime_resource_factor: 1,
|
||||
github_token_is_set: false,
|
||||
enable_default_condenser: true,
|
||||
enable_sound_notifications: false,
|
||||
user_consents_to_analytics: false,
|
||||
provider_tokens_set: {
|
||||
github: false,
|
||||
gitlab: false,
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_MODEL: "anthropic/claude-3-5-sonnet-20241022",
|
||||
LLM_BASE_URL: "",
|
||||
AGENT: "CodeActAgent",
|
||||
LANGUAGE: "en",
|
||||
LLM_API_KEY_SET: false,
|
||||
CONFIRMATION_MODE: false,
|
||||
SECURITY_ANALYZER: "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: { github: false, gitlab: false },
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
PROVIDER_TOKENS: {
|
||||
github: "",
|
||||
gitlab: "",
|
||||
},
|
||||
is_new_user: true,
|
||||
IS_NEW_USER: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default settings
|
||||
*/
|
||||
export const getDefaultSettings = () => DEFAULT_SETTINGS;
|
||||
export const getDefaultSettings = (): Settings => DEFAULT_SETTINGS;
|
||||
|
||||
38
frontend/src/types/git.d.ts
vendored
Normal file
38
frontend/src/types/git.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitHubErrorReponse {
|
||||
message: string;
|
||||
documentation_url: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface GitUser {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
company: string | null;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
interface GitRepository {
|
||||
id: number;
|
||||
full_name: string;
|
||||
git_provider: Provider;
|
||||
stargazers_count?: number;
|
||||
link_header?: string;
|
||||
}
|
||||
|
||||
interface GitHubCommit {
|
||||
html_url: string;
|
||||
sha: string;
|
||||
commit: {
|
||||
author: {
|
||||
date: string; // ISO 8601
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GithubAppInstallation {
|
||||
installations: { id: number }[];
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { GitProvider } from "#/api/settings-service/settings-service.types";
|
||||
|
||||
export interface GitHubErrorReponse {
|
||||
message: string;
|
||||
documentation_url: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface GitUser {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
company: string | null;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
export interface GitRepository {
|
||||
id: number;
|
||||
full_name: string;
|
||||
git_provider: GitProvider;
|
||||
stargazers_count?: number;
|
||||
link_header?: string;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
export const hasAdvancedSettingsSet = (settings: UserSettings): boolean =>
|
||||
!!settings.llm_base_url ||
|
||||
settings.agent !== DEFAULT_SETTINGS.agent ||
|
||||
settings.remote_runtime_resource_factor !==
|
||||
DEFAULT_SETTINGS.remote_runtime_resource_factor ||
|
||||
settings.confirmation_mode ||
|
||||
!!settings.security_analyzer;
|
||||
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
|
||||
!!settings.LLM_BASE_URL ||
|
||||
settings.AGENT !== DEFAULT_SETTINGS.AGENT ||
|
||||
settings.REMOTE_RUNTIME_RESOURCE_FACTOR !==
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR ||
|
||||
settings.CONFIRMATION_MODE ||
|
||||
!!settings.SECURITY_ANALYZER;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import { UserSettings } from "#/api/settings-service/settings-service.types";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
const extractBasicFormData = (formData: FormData) => {
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
@@ -47,7 +47,9 @@ const extractAdvancedFormData = (formData: FormData) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const extractSettings = (formData: FormData): Partial<UserSettings> => {
|
||||
export const extractSettings = (
|
||||
formData: FormData,
|
||||
): Partial<Settings> & { llm_api_key?: string | null } => {
|
||||
const { LLM_MODEL, LLM_API_KEY, AGENT, LANGUAGE } =
|
||||
extractBasicFormData(formData);
|
||||
|
||||
@@ -72,14 +74,15 @@ export const extractSettings = (formData: FormData): Partial<UserSettings> => {
|
||||
}
|
||||
|
||||
return {
|
||||
llm_model: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
llm_api_key_set: LLM_API_KEY,
|
||||
agent: AGENT,
|
||||
language: LANGUAGE,
|
||||
llm_base_url: LLM_BASE_URL,
|
||||
confirmation_mode: CONFIRMATION_MODE,
|
||||
security_analyzer: SECURITY_ANALYZER,
|
||||
enable_default_condenser: ENABLE_DEFAULT_CONDENSER,
|
||||
provider_tokens_set: providerTokens,
|
||||
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
LLM_API_KEY_SET: !!LLM_API_KEY,
|
||||
AGENT,
|
||||
LANGUAGE,
|
||||
LLM_BASE_URL,
|
||||
CONFIRMATION_MODE,
|
||||
SECURITY_ANALYZER,
|
||||
ENABLE_DEFAULT_CONDENSER,
|
||||
PROVIDER_TOKENS: providerTokens,
|
||||
llm_api_key: LLM_API_KEY,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ from openhands.io import read_task
|
||||
prompt_session = PromptSession()
|
||||
|
||||
|
||||
def display_message(message: str):
|
||||
def display_message(message: str) -> None:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
@@ -55,7 +55,7 @@ def display_message(message: str):
|
||||
)
|
||||
|
||||
|
||||
def display_command(command: str):
|
||||
def display_command(command: str) -> None:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
@@ -67,7 +67,7 @@ def display_command(command: str):
|
||||
)
|
||||
|
||||
|
||||
def display_confirmation(confirmation_state: ActionConfirmationStatus):
|
||||
def display_confirmation(confirmation_state: ActionConfirmationStatus) -> None:
|
||||
if confirmation_state == ActionConfirmationStatus.CONFIRMED:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
@@ -100,7 +100,7 @@ def display_confirmation(confirmation_state: ActionConfirmationStatus):
|
||||
)
|
||||
|
||||
|
||||
def display_command_output(output: str):
|
||||
def display_command_output(output: str) -> None:
|
||||
lines = output.split('\n')
|
||||
for line in lines:
|
||||
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
|
||||
@@ -110,7 +110,7 @@ def display_command_output(output: str):
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_file_edit(event: FileEditAction | FileEditObservation):
|
||||
def display_file_edit(event: FileEditAction | FileEditObservation) -> None:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
@@ -121,7 +121,7 @@ def display_file_edit(event: FileEditAction | FileEditObservation):
|
||||
)
|
||||
|
||||
|
||||
def display_event(event: Event, config: AppConfig):
|
||||
def display_event(event: Event, config: AppConfig) -> None:
|
||||
if isinstance(event, Action):
|
||||
if hasattr(event, 'thought'):
|
||||
display_message(event.thought)
|
||||
@@ -175,7 +175,7 @@ async def read_confirmation_input():
|
||||
return False
|
||||
|
||||
|
||||
async def main(loop: asyncio.AbstractEventLoop):
|
||||
async def main(loop: asyncio.AbstractEventLoop) -> None:
|
||||
"""Runs the agent in CLI mode."""
|
||||
|
||||
args = parse_arguments()
|
||||
@@ -207,7 +207,7 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
|
||||
async def prompt_for_next_task():
|
||||
async def prompt_for_next_task() -> None:
|
||||
next_message = await read_prompt_input(config.cli_multiline_input)
|
||||
if not next_message.strip():
|
||||
await prompt_for_next_task()
|
||||
@@ -219,7 +219,7 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
action = MessageAction(content=next_message)
|
||||
event_stream.add_event(action, EventSource.USER)
|
||||
|
||||
async def on_event_async(event: Event):
|
||||
async def on_event_async(event: Event) -> None:
|
||||
display_event(event, config)
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state in [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import ClassVar
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
|
||||
@@ -50,7 +50,7 @@ class AppConfig(BaseModel):
|
||||
"""
|
||||
|
||||
llms: dict[str, LLMConfig] = Field(default_factory=dict)
|
||||
agents: dict = Field(default_factory=dict)
|
||||
agents: dict[str, AgentConfig] = Field(default_factory=dict)
|
||||
default_agent: str = Field(default=OH_DEFAULT_AGENT)
|
||||
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
|
||||
security: SecurityConfig = Field(default_factory=SecurityConfig)
|
||||
@@ -93,7 +93,7 @@ class AppConfig(BaseModel):
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
def get_llm_config(self, name='llm') -> LLMConfig:
|
||||
def get_llm_config(self, name: str = 'llm') -> LLMConfig:
|
||||
"""'llm' is the name for default config (for backward compatibility prior to 0.8)."""
|
||||
if name in self.llms:
|
||||
return self.llms[name]
|
||||
@@ -105,10 +105,10 @@ class AppConfig(BaseModel):
|
||||
self.llms['llm'] = LLMConfig()
|
||||
return self.llms['llm']
|
||||
|
||||
def set_llm_config(self, value: LLMConfig, name='llm') -> None:
|
||||
def set_llm_config(self, value: LLMConfig, name: str = 'llm') -> None:
|
||||
self.llms[name] = value
|
||||
|
||||
def get_agent_config(self, name='agent') -> AgentConfig:
|
||||
def get_agent_config(self, name: str = 'agent') -> AgentConfig:
|
||||
"""'agent' is the name for default config (for backward compatibility prior to 0.8)."""
|
||||
if name in self.agents:
|
||||
return self.agents[name]
|
||||
@@ -116,22 +116,24 @@ class AppConfig(BaseModel):
|
||||
self.agents['agent'] = AgentConfig()
|
||||
return self.agents['agent']
|
||||
|
||||
def set_agent_config(self, value: AgentConfig, name='agent') -> None:
|
||||
def set_agent_config(self, value: AgentConfig, name: str = 'agent') -> None:
|
||||
self.agents[name] = value
|
||||
|
||||
def get_agent_to_llm_config_map(self) -> dict[str, LLMConfig]:
|
||||
"""Get a map of agent names to llm configs."""
|
||||
return {name: self.get_llm_config_from_agent(name) for name in self.agents}
|
||||
|
||||
def get_llm_config_from_agent(self, name='agent') -> LLMConfig:
|
||||
def get_llm_config_from_agent(self, name: str = 'agent') -> LLMConfig:
|
||||
agent_config: AgentConfig = self.get_agent_config(name)
|
||||
llm_config_name = agent_config.llm_config
|
||||
llm_config_name = (
|
||||
agent_config.llm_config if agent_config.llm_config is not None else 'llm'
|
||||
)
|
||||
return self.get_llm_config(llm_config_name)
|
||||
|
||||
def get_agent_configs(self) -> dict[str, AgentConfig]:
|
||||
return self.agents
|
||||
|
||||
def model_post_init(self, __context):
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Post-initialization hook, called when the instance is created with only default values."""
|
||||
super().model_post_init(__context)
|
||||
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
|
||||
|
||||
@@ -151,7 +151,7 @@ class LLMConfig(BaseModel):
|
||||
|
||||
return llm_mapping
|
||||
|
||||
def model_post_init(self, __context: Any):
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Post-initialization hook to assign OpenRouter-related variables to environment variables.
|
||||
|
||||
This ensures that these values are accessible to litellm at runtime.
|
||||
|
||||
@@ -58,7 +58,7 @@ def load_from_env(
|
||||
return None
|
||||
|
||||
# helper function to set attributes based on env vars
|
||||
def set_attr_from_env(sub_config: BaseModel, prefix='') -> None:
|
||||
def set_attr_from_env(sub_config: BaseModel, prefix: str = '') -> None:
|
||||
"""Set attributes of a config model based on environment variables."""
|
||||
for field_name, field_info in sub_config.model_fields.items():
|
||||
field_value = getattr(sub_config, field_name)
|
||||
@@ -275,7 +275,7 @@ def get_or_create_jwt_secret(file_store: FileStore) -> str:
|
||||
return new_secret
|
||||
|
||||
|
||||
def finalize_config(cfg: AppConfig):
|
||||
def finalize_config(cfg: AppConfig) -> None:
|
||||
"""More tweaks to the config after it's been loaded."""
|
||||
if cfg.workspace_base is not None:
|
||||
cfg.workspace_base = os.path.abspath(cfg.workspace_base)
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from types import TracebackType
|
||||
from typing import Any, Literal, Mapping, TextIO
|
||||
from typing import Any, Literal, Mapping, MutableMapping, TextIO
|
||||
|
||||
import litellm
|
||||
from pythonjsonlogger.json import JsonFormatter
|
||||
@@ -304,7 +304,7 @@ def get_file_handler(
|
||||
return file_handler
|
||||
|
||||
|
||||
def json_formatter():
|
||||
def json_formatter() -> JsonFormatter:
|
||||
return JsonFormatter(
|
||||
'{message}{levelname}',
|
||||
style='{',
|
||||
@@ -471,11 +471,15 @@ llm_response_logger = _setup_llm_logger('response', current_log_level)
|
||||
class OpenHandsLoggerAdapter(logging.LoggerAdapter):
|
||||
extra: dict
|
||||
|
||||
def __init__(self, logger=openhands_logger, extra=None):
|
||||
def __init__(
|
||||
self, logger: logging.Logger = openhands_logger, extra: dict | None = None
|
||||
) -> None:
|
||||
self.logger = logger
|
||||
self.extra = extra or {}
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
def process(
|
||||
self, msg: str, kwargs: MutableMapping[str, Any]
|
||||
) -> tuple[str, MutableMapping[str, Any]]:
|
||||
"""
|
||||
If 'extra' is supplied in kwargs, merge it with the adapters 'extra' dict
|
||||
Starting in Python 3.13, LoggerAdapter's merge_extra option will do this.
|
||||
|
||||
@@ -12,14 +12,14 @@ async def run_agent_until_done(
|
||||
runtime: Runtime,
|
||||
memory: Memory,
|
||||
end_states: list[AgentState],
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
run_agent_until_done takes a controller and a runtime, and will run
|
||||
the agent until it reaches a terminal state.
|
||||
Note that runtime must be connected before being passed in here.
|
||||
"""
|
||||
|
||||
def status_callback(msg_type, msg_id, msg):
|
||||
def status_callback(msg_type: str, msg_id: str, msg: str) -> None:
|
||||
if msg_type == 'error':
|
||||
logger.error(msg)
|
||||
if controller:
|
||||
|
||||
@@ -163,7 +163,7 @@ async def run_controller(
|
||||
# init with the provided actions
|
||||
event_stream.add_event(initial_user_action, EventSource.USER)
|
||||
|
||||
def on_event(event: Event):
|
||||
def on_event(event: Event) -> None:
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state == AgentState.AWAITING_USER_INPUT:
|
||||
if exit_on_message:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from litellm import ChatCompletionMessageToolCall
|
||||
from pydantic import BaseModel, Field, model_serializer
|
||||
@@ -14,7 +14,7 @@ class Content(BaseModel):
|
||||
type: str
|
||||
cache_prompt: bool = False
|
||||
|
||||
@model_serializer
|
||||
@model_serializer(mode='plain')
|
||||
def serialize_model(
|
||||
self,
|
||||
) -> dict[str, str | dict[str, str]] | list[dict[str, str | dict[str, str]]]:
|
||||
@@ -25,7 +25,7 @@ class TextContent(Content):
|
||||
type: str = ContentType.TEXT.value
|
||||
text: str
|
||||
|
||||
@model_serializer
|
||||
@model_serializer(mode='plain')
|
||||
def serialize_model(self) -> dict[str, str | dict[str, str]]:
|
||||
data: dict[str, str | dict[str, str]] = {
|
||||
'type': self.type,
|
||||
@@ -40,7 +40,7 @@ class ImageContent(Content):
|
||||
type: str = ContentType.IMAGE_URL.value
|
||||
image_urls: list[str]
|
||||
|
||||
@model_serializer
|
||||
@model_serializer(mode='plain')
|
||||
def serialize_model(self) -> list[dict[str, str | dict[str, str]]]:
|
||||
images: list[dict[str, str | dict[str, str]]] = []
|
||||
for url in self.image_urls:
|
||||
@@ -71,8 +71,8 @@ class Message(BaseModel):
|
||||
def contains_image(self) -> bool:
|
||||
return any(isinstance(content, ImageContent) for content in self.content)
|
||||
|
||||
@model_serializer
|
||||
def serialize_model(self) -> dict:
|
||||
@model_serializer(mode='plain')
|
||||
def serialize_model(self) -> dict[str, Any]:
|
||||
# We need two kinds of serializations:
|
||||
# - into a single string: for providers that don't support list of content items (e.g. no vision, no tool calls)
|
||||
# - into a list of content items: the new APIs of providers with vision/prompt caching/tool calls
|
||||
@@ -84,18 +84,18 @@ class Message(BaseModel):
|
||||
# some providers, like HF and Groq/llama, don't support a list here, but a single string
|
||||
return self._string_serializer()
|
||||
|
||||
def _string_serializer(self) -> dict:
|
||||
def _string_serializer(self) -> dict[str, Any]:
|
||||
# convert content to a single string
|
||||
content = '\n'.join(
|
||||
item.text for item in self.content if isinstance(item, TextContent)
|
||||
)
|
||||
message_dict: dict = {'content': content, 'role': self.role}
|
||||
message_dict: dict[str, Any] = {'content': content, 'role': self.role}
|
||||
|
||||
# add tool call keys if we have a tool call or response
|
||||
return self._add_tool_call_keys(message_dict)
|
||||
|
||||
def _list_serializer(self) -> dict:
|
||||
content: list[dict] = []
|
||||
def _list_serializer(self) -> dict[str, Any]:
|
||||
content: list[dict[str, Any]] = []
|
||||
role_tool_with_prompt_caching = False
|
||||
for item in self.content:
|
||||
d = item.model_dump()
|
||||
@@ -120,7 +120,7 @@ class Message(BaseModel):
|
||||
# We know d is a list for ImageContent
|
||||
content.extend([d] if isinstance(d, dict) else d)
|
||||
|
||||
message_dict: dict = {'content': content, 'role': self.role}
|
||||
message_dict: dict[str, Any] = {'content': content, 'role': self.role}
|
||||
|
||||
if role_tool_with_prompt_caching:
|
||||
message_dict['cache_control'] = {'type': 'ephemeral'}
|
||||
@@ -128,7 +128,7 @@ class Message(BaseModel):
|
||||
# add tool call keys if we have a tool call or response
|
||||
return self._add_tool_call_keys(message_dict)
|
||||
|
||||
def _add_tool_call_keys(self, message_dict: dict) -> dict:
|
||||
def _add_tool_call_keys(self, message_dict: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Add tool call keys if we have a tool call or response.
|
||||
|
||||
NOTE: this is necessary for both native and non-native tool calling
|
||||
|
||||
@@ -23,9 +23,11 @@ class CriticResult(BaseModel):
|
||||
|
||||
class BaseCritic(abc.ABC):
|
||||
"""
|
||||
A critic is a function that takes in a list of events and returns a score about the quality of those events.
|
||||
A critic is a function that takes in a list of events, optional git patch, and returns a score about the quality of those events.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def evaluate(self, events: list[Event]) -> CriticResult:
|
||||
def evaluate(
|
||||
self, events: list[Event], git_patch: str | None = None
|
||||
) -> CriticResult:
|
||||
pass
|
||||
|
||||
@@ -5,16 +5,21 @@ from openhands.events.action import Action, AgentFinishAction
|
||||
|
||||
class AgentFinishedCritic(BaseCritic):
|
||||
"""This is a simple rule-based critic that checks if the last event is an AgentFinishAction.
|
||||
|
||||
If not, it will return a score of 0 and a message indicating that the agent did not finish.
|
||||
If the git patch is provided and is empty, it will return a score of 0 and a message indicating that the git patch is empty.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def evaluate(self, events: list[Event]) -> CriticResult:
|
||||
def evaluate(
|
||||
self, events: list[Event], git_patch: str | None = None
|
||||
) -> CriticResult:
|
||||
last_action = next((h for h in reversed(events) if isinstance(h, Action)), None)
|
||||
|
||||
if git_patch is not None and len(git_patch.strip()) == 0:
|
||||
return CriticResult(score=0, message='Git patch is empty.')
|
||||
|
||||
if isinstance(last_action, AgentFinishAction):
|
||||
return CriticResult(score=1, message='Agent finished.')
|
||||
else:
|
||||
|
||||
@@ -328,7 +328,7 @@ def main() -> None:
|
||||
|
||||
runtime_container_image = my_args.runtime_container_image
|
||||
if runtime_container_image is None:
|
||||
runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.31.0-nikolaik'
|
||||
runtime_container_image = 'ghcr.io/all-hands-ai/runtime:0.32.0-nikolaik'
|
||||
|
||||
owner, repo = my_args.selected_repo.split('/')
|
||||
token = my_args.token or os.getenv('GITHUB_TOKEN') or os.getenv('GITLAB_TOKEN')
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
@@ -7,7 +6,6 @@ from enum import Enum
|
||||
from typing import Callable
|
||||
|
||||
import httpx
|
||||
import pandas as pd
|
||||
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.logger import get_console_handler
|
||||
@@ -134,43 +132,6 @@ def cleanup() -> None:
|
||||
process.join()
|
||||
|
||||
|
||||
def prepare_dataset(
|
||||
dataset: pd.DataFrame, output_file: str, eval_n_limit: int
|
||||
) -> pd.DataFrame:
|
||||
assert 'instance_id' in dataset.columns, (
|
||||
"Expected 'instance_id' column in the dataset. You should define your own "
|
||||
"unique identifier for each instance and use it as the 'instance_id' column."
|
||||
)
|
||||
id_column = 'instance_id'
|
||||
logger.info(f'Writing evaluation output to {output_file}')
|
||||
finished_ids = set()
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f:
|
||||
for line in f:
|
||||
data = json.loads(line)
|
||||
finished_ids.add(data[id_column])
|
||||
logger.warning(
|
||||
f'Output file {output_file} already exists. Loaded '
|
||||
f'{len(finished_ids)} finished instances.'
|
||||
)
|
||||
|
||||
if eval_n_limit:
|
||||
dataset = dataset.head(eval_n_limit)
|
||||
logger.info(f'Limiting evaluation to first {eval_n_limit} instances.')
|
||||
|
||||
new_dataset = [
|
||||
instance
|
||||
for _, instance in dataset.iterrows()
|
||||
if instance[id_column] not in finished_ids
|
||||
]
|
||||
logger.info(
|
||||
f'Finished instances: {len(finished_ids)}, '
|
||||
f'Remaining instances: {len(new_dataset)}'
|
||||
)
|
||||
|
||||
return pd.DataFrame(new_dataset)
|
||||
|
||||
|
||||
def reset_logger_for_multiprocessing(
|
||||
logger: logging.Logger, instance_id: str, log_dir: str
|
||||
) -> None:
|
||||
|
||||
@@ -82,7 +82,7 @@ async def unset_settings_tokens(request: Request) -> JSONResponse:
|
||||
)
|
||||
|
||||
|
||||
@app.post('/settings/reset', response_model=dict[str, str])
|
||||
@app.post('/reset-settings', response_model=dict[str, str])
|
||||
async def reset_settings(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Resets user settings.
|
||||
|
||||
@@ -167,8 +167,9 @@ class Session:
|
||||
"""
|
||||
Initialize LLM, extracted for testing.
|
||||
"""
|
||||
agent_name = agent_cls if agent_cls is not None else 'agent'
|
||||
return LLM(
|
||||
config=self.config.get_llm_config_from_agent(agent_cls),
|
||||
config=self.config.get_llm_config_from_agent(agent_name),
|
||||
retry_listener=self._notify_on_llm_retry,
|
||||
)
|
||||
|
||||
|
||||
45
poetry.lock
generated
45
poetry.lock
generated
@@ -1191,7 +1191,7 @@ version = "1.17.1"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "runtime"]
|
||||
groups = ["main", "runtime", "test"]
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
@@ -1261,6 +1261,7 @@ files = [
|
||||
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
||||
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
@@ -1483,7 +1484,7 @@ version = "1.3.1"
|
||||
description = "Python library for calculating contours of 2D quadrilateral grids"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"},
|
||||
{file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"},
|
||||
@@ -1691,7 +1692,7 @@ version = "0.12.1"
|
||||
description = "Composable style cycles"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
|
||||
{file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
|
||||
@@ -1723,7 +1724,7 @@ version = "3.0.1"
|
||||
description = "HuggingFace community-driven open-source library of datasets"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "datasets-3.0.1-py3-none-any.whl", hash = "sha256:db080aab41c8cc68645117a0f172e5c6789cbc672f066de0aa5a08fc3eebc686"},
|
||||
{file = "datasets-3.0.1.tar.gz", hash = "sha256:40d63b09e76a3066c32e746d6fdc36fd3f29ed2acd49bf5b1a2100da32936511"},
|
||||
@@ -1887,7 +1888,7 @@ version = "0.3.8"
|
||||
description = "serialize all of Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
|
||||
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
|
||||
@@ -2235,7 +2236,7 @@ version = "4.56.0"
|
||||
description = "Tools to manipulate font files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"},
|
||||
{file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"},
|
||||
@@ -2513,7 +2514,7 @@ version = "24.11.1"
|
||||
description = "Coroutine-based network library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e"},
|
||||
{file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870"},
|
||||
@@ -2661,7 +2662,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi
|
||||
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
|
||||
proto-plus = [
|
||||
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0dev"},
|
||||
]
|
||||
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
|
||||
requests = ">=2.18.0,<3.0.0.dev0"
|
||||
@@ -2874,7 +2875,7 @@ google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
|
||||
grpc-google-iam-v1 = ">=0.14.0,<1.0.0dev"
|
||||
proto-plus = [
|
||||
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0dev"},
|
||||
{version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
|
||||
]
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
|
||||
|
||||
@@ -3010,7 +3011,7 @@ version = "3.1.1"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["main", "evaluation", "test"]
|
||||
files = [
|
||||
{file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
|
||||
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
|
||||
@@ -3086,6 +3087,7 @@ files = [
|
||||
{file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
|
||||
{file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "furo"]
|
||||
@@ -4133,7 +4135,7 @@ version = "1.4.8"
|
||||
description = "A fast implementation of the Cassowary constraint solver"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"},
|
||||
{file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"},
|
||||
@@ -4717,7 +4719,7 @@ version = "3.10.1"
|
||||
description = "Python plotting package"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"},
|
||||
@@ -5008,7 +5010,7 @@ version = "0.70.16"
|
||||
description = "better multiprocessing and multithreading in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"},
|
||||
{file = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"},
|
||||
@@ -6325,7 +6327,7 @@ version = "19.0.1"
|
||||
description = "Python library for Apache Arrow"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc28912a2dc924dddc2087679cc8b7263accc71b9ff025a1362b004711661a69"},
|
||||
{file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fca15aabbe9b8355800d923cc2e82c8ef514af321e18b437c3d782aa884eaeec"},
|
||||
@@ -6434,11 +6436,12 @@ version = "2.22"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "runtime"]
|
||||
groups = ["main", "runtime", "test"]
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodome"
|
||||
@@ -8166,7 +8169,7 @@ version = "0.13.2"
|
||||
description = "Statistical data visualization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"},
|
||||
{file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"},
|
||||
@@ -8205,7 +8208,7 @@ version = "75.8.2"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "evaluation", "runtime"]
|
||||
groups = ["main", "evaluation", "runtime", "test"]
|
||||
files = [
|
||||
{file = "setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f"},
|
||||
{file = "setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2"},
|
||||
@@ -9872,7 +9875,7 @@ version = "3.5.0"
|
||||
description = "Python binding for xxHash"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "evaluation"]
|
||||
groups = ["evaluation"]
|
||||
files = [
|
||||
{file = "xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212"},
|
||||
{file = "xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520"},
|
||||
@@ -10122,7 +10125,7 @@ version = "5.0"
|
||||
description = "Very basic event publishing system"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"},
|
||||
{file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"},
|
||||
@@ -10141,7 +10144,7 @@ version = "7.2"
|
||||
description = "Interfaces for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
groups = ["main", "test"]
|
||||
files = [
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
|
||||
@@ -10193,4 +10196,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "bd1e164559f6395718bc76482493aa71327e7aee9ebe6131facba65661d9abec"
|
||||
content-hash = "4081b88d1b970aa56603359e41430d4465486b3866bfd50371d5c4f77fb58fb4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.31.0"
|
||||
version = "0.32.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -14,8 +14,6 @@ packages = [
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
datasets = "*"
|
||||
pandas = "*"
|
||||
litellm = "^1.60.0"
|
||||
aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13
|
||||
google-generativeai = "*" # To use litellm with Gemini Pro API
|
||||
@@ -23,7 +21,6 @@ google-api-python-client = "^2.164.0" # For Google Sheets API
|
||||
google-auth-httplib2 = "*" # For Google Sheets authentication
|
||||
google-auth-oauthlib = "*" # For Google Sheets OAuth
|
||||
termcolor = "*"
|
||||
seaborn = "*"
|
||||
docker = "*"
|
||||
fastapi = "*"
|
||||
toml = "*"
|
||||
@@ -39,8 +36,6 @@ jinja2 = "^3.1.3"
|
||||
python-multipart = "*"
|
||||
boto3 = "*"
|
||||
minio = "^7.2.8"
|
||||
gevent = "^24.2.1"
|
||||
pyarrow = "19.0.1" # transitive dependency, pinned here to avoid conflicts
|
||||
tenacity = ">=8.5,<10.0"
|
||||
zope-interface = "7.2"
|
||||
pathspec = "^0.12.1"
|
||||
@@ -96,11 +91,11 @@ pytest-xdist = "*"
|
||||
openai = "*"
|
||||
pandas = "*"
|
||||
reportlab = "*"
|
||||
gevent = "^24.2.1"
|
||||
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -129,7 +124,6 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
@@ -149,6 +143,8 @@ browsergym-webarena = "0.13.3"
|
||||
browsergym-miniwob = "0.13.3"
|
||||
browsergym-visualwebarena = "0.13.3"
|
||||
boto3-stubs = {extras = ["s3"], version = "^1.37.19"}
|
||||
pyarrow = "19.0.1" # transitive dependency, pinned here to avoid conflicts
|
||||
datasets = "*"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
|
||||
Reference in New Issue
Block a user