mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e136651e46 | |||
| 760a14482e | |||
| b5338c69d6 | |||
| c99f031cdb | |||
| bcc28a12fe | |||
| c82b3378a6 | |||
| a6d3db3ce7 | |||
| 4cbbfd799c | |||
| 0b728c0c79 | |||
| e35c8ee173 | |||
| 9a9b143620 | |||
| 38578bd5f5 | |||
| 7b2c88ae6b | |||
| 0cbf3987f8 | |||
| d18edc8b30 | |||
| 42eb355a68 |
+1
-1
@@ -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
|
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.
|
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.34-nikolaik`
|
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.35-nikolaik`
|
||||||
|
|
||||||
## Develop inside Docker container
|
## Develop inside Docker container
|
||||||
|
|
||||||
|
|||||||
@@ -52,17 +52,17 @@ system requirements and more information.
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||||
|
|
||||||
docker run -it --rm --pull=always \
|
docker run -it --rm --pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e LOG_ALL_EVENTS=true \
|
-e LOG_ALL_EVENTS=true \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app \
|
--name openhands-app \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||||
```
|
```
|
||||||
|
|
||||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ type = "noop"
|
|||||||
#[llm.condenser]
|
#[llm.condenser]
|
||||||
#model = "gpt-4o"
|
#model = "gpt-4o"
|
||||||
#temperature = 0.1
|
#temperature = 0.1
|
||||||
#max_tokens = 1024
|
#max_input_tokens = 1024
|
||||||
|
|
||||||
#################################### Eval ####################################
|
#################################### Eval ####################################
|
||||||
# Configuration for the evaluation, please refer to the specific evaluation
|
# Configuration for the evaluation, please refer to the specific evaluation
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||||
#
|
#
|
||||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.34-nikolaik}
|
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.35-nikolaik}
|
||||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
|||||||
image: openhands:latest
|
image: openhands:latest
|
||||||
container_name: openhands-app-${DATE:-}
|
container_name: openhands-app-${DATE:-}
|
||||||
environment:
|
environment:
|
||||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik}
|
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.35-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
|
#- 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}
|
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -61,7 +61,7 @@ docker run -it \
|
|||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.cli
|
python -m openhands.core.cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -56,6 +56,6 @@ docker run -it \
|
|||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
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.
|
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||||
|
|
||||||
docker run -it --rm --pull=always \
|
docker run -it --rm --pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e LOG_ALL_EVENTS=true \
|
-e LOG_ALL_EVENTS=true \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app \
|
--name openhands-app \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
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 # ...
|
docker run # ...
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -44,7 +44,7 @@ docker run -it \
|
|||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.cli
|
python -m openhands.core.cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -42,7 +42,7 @@ docker run -it \
|
|||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ OpenHandsがリポジトリで動作する際:
|
|||||||
|
|
||||||
1. リポジトリに`.openhands/microagents/`が存在する場合、そこからリポジトリ固有の指示を読み込みます。
|
1. リポジトリに`.openhands/microagents/`が存在する場合、そこからリポジトリ固有の指示を読み込みます。
|
||||||
2. 会話のキーワードによってトリガーされる一般的なガイドラインを読み込みます。
|
2. 会話のキーワードによってトリガーされる一般的なガイドラインを読み込みます。
|
||||||
現在の[パブリックMicroagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)を参照してください。
|
現在の[パブリックMicroagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)を参照してください。
|
||||||
|
|
||||||
## Microagentのフォーマット
|
## Microagentのフォーマット
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -88,4 +88,4 @@ triggers:
|
|||||||
- ビルド時間とイメージサイズを最適化
|
- ビルド時間とイメージサイズを最適化
|
||||||
```
|
```
|
||||||
|
|
||||||
より多くの例については、[現在のパブリックマイクロエージェント](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)をご覧ください。
|
より多くの例については、[現在のパブリックマイクロエージェント](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)をご覧ください。
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run # ...
|
docker run # ...
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||||
@@ -82,5 +82,5 @@ docker network create openhands-network
|
|||||||
# 分離されたネットワークで OpenHands を実行
|
# 分離されたネットワークで OpenHands を実行
|
||||||
docker run # ... \
|
docker run # ... \
|
||||||
--network openhands-network \
|
--network openhands-network \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -45,7 +45,7 @@ docker run -it \
|
|||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.cli
|
python -m openhands.core.cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -43,7 +43,7 @@ docker run -it \
|
|||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.main -t "escreva um script bash que imprima oi"
|
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.
|
A maneira mais fácil de executar o OpenHands é no Docker.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||||
|
|
||||||
docker run -it --rm --pull=always \
|
docker run -it --rm --pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e LOG_ALL_EVENTS=true \
|
-e LOG_ALL_EVENTS=true \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app \
|
--name openhands-app \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||||
```
|
```
|
||||||
|
|
||||||
Você encontrará o OpenHands em execução em http://localhost:3000!
|
Você encontrará o OpenHands em execução em http://localhost:3000!
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ Quando o OpenHands trabalha com um repositório, ele:
|
|||||||
|
|
||||||
1. Carrega instruções específicas do repositório de `.openhands/microagents/`, se presentes no repositório.
|
1. Carrega instruções específicas do repositório de `.openhands/microagents/`, se presentes no repositório.
|
||||||
2. Carrega diretrizes gerais acionadas por palavras-chave nas conversas.
|
2. Carrega diretrizes gerais acionadas por palavras-chave nas conversas.
|
||||||
Veja os [Microagentes Públicos](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) atuais.
|
Veja os [Microagentes Públicos](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents) atuais.
|
||||||
|
|
||||||
## Formato do Microagente
|
## Formato do Microagente
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Microagentes públicos são diretrizes especializadas acionadas por palavras-chave para todos os usuários do OpenHands.
|
Microagentes públicos são diretrizes especializadas acionadas por palavras-chave para todos os usuários do OpenHands.
|
||||||
Eles são definidos em arquivos markdown no diretório
|
Eles são definidos em arquivos markdown no diretório
|
||||||
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
|
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents).
|
||||||
|
|
||||||
Microagentes públicos:
|
Microagentes públicos:
|
||||||
- Monitoram comandos recebidos em busca de suas palavras-chave de acionamento.
|
- Monitoram comandos recebidos em busca de suas palavras-chave de acionamento.
|
||||||
@@ -149,5 +149,5 @@ Lembre-se de:
|
|||||||
- Otimizar para tempo de build e tamanho da imagem
|
- Otimizar para tempo de build e tamanho da imagem
|
||||||
```
|
```
|
||||||
|
|
||||||
Veja os [microagentes públicos atuais](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) para
|
Veja os [microagentes públicos atuais](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents) para
|
||||||
mais exemplos.
|
mais exemplos.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
|
|||||||
|
|
||||||
```
|
```
|
||||||
docker run # ...
|
docker run # ...
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -59,7 +59,7 @@ docker run -it \
|
|||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.cli
|
python -m openhands.core.cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -57,6 +57,6 @@ docker run -it \
|
|||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,16 +11,16 @@
|
|||||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||||
|
|
||||||
docker run -it --rm --pull=always \
|
docker run -it --rm --pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e LOG_ALL_EVENTS=true \
|
-e LOG_ALL_EVENTS=true \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app \
|
--name openhands-app \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||||
```
|
```
|
||||||
|
|
||||||
你也可以在可脚本化的[无头模式](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)。
|
你也可以在可脚本化的[无头模式](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 # ...
|
docker run # ...
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -45,7 +45,7 @@ docker run -it \
|
|||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.cli
|
python -m openhands.core.cli
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# Using GitLab CI Runners
|
|
||||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
|||||||
```bash
|
```bash
|
||||||
docker run -it \
|
docker run -it \
|
||||||
--pull=always \
|
--pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e SANDBOX_USER_ID=$(id -u) \
|
-e SANDBOX_USER_ID=$(id -u) \
|
||||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||||
-e LLM_API_KEY=$LLM_API_KEY \
|
-e LLM_API_KEY=$LLM_API_KEY \
|
||||||
@@ -43,7 +43,7 @@ docker run -it \
|
|||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
|
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
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.
|
The easiest way to run OpenHands is in Docker.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
|
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||||
|
|
||||||
docker run -it --rm --pull=always \
|
docker run -it --rm --pull=always \
|
||||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
|
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||||
-e LOG_ALL_EVENTS=true \
|
-e LOG_ALL_EVENTS=true \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v ~/.openhands-state:/.openhands-state \
|
-v ~/.openhands-state:/.openhands-state \
|
||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--add-host host.docker.internal:host-gateway \
|
--add-host host.docker.internal:host-gateway \
|
||||||
--name openhands-app \
|
--name openhands-app \
|
||||||
docker.all-hands.dev/all-hands-ai/openhands:0.34
|
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||||
```
|
```
|
||||||
|
|
||||||
You'll find OpenHands running at http://localhost:3000!
|
You'll find OpenHands running at http://localhost:3000!
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ Keyword-triggered microagents:
|
|||||||
- Apply their specialized knowledge and capabilities.
|
- Apply their specialized knowledge and capabilities.
|
||||||
- Follow defined guidelines and restrictions.
|
- Follow defined guidelines and restrictions.
|
||||||
|
|
||||||
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
|
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
|
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
||||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
|
||||||
import * as AuthHook from "#/context/auth-context";
|
import * as AuthHook from "#/context/auth-context";
|
||||||
|
|
||||||
|
// Mock the useAuthUrl hook
|
||||||
|
vi.mock("#/hooks/use-auth-url", () => ({
|
||||||
|
useAuthUrl: () => "https://gitlab.com/oauth/authorize"
|
||||||
|
}));
|
||||||
|
|
||||||
describe("AuthModal", () => {
|
describe("AuthModal", () => {
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal("location", { href: "" });
|
vi.stubGlobal("location", { href: "" });
|
||||||
vi.spyOn(AuthHook, "useAuth").mockReturnValue({
|
vi.spyOn(AuthHook, "useAuth").mockReturnValue({
|
||||||
providersAreSet: false,
|
providersAreSet: false,
|
||||||
@@ -16,50 +20,29 @@ describe("AuthModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
vi.restoreAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render a tos checkbox that is unchecked by default", () => {
|
it("should render the GitHub and GitLab buttons", () => {
|
||||||
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
|
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
|
||||||
const checkbox = screen.getByRole("checkbox");
|
|
||||||
|
|
||||||
expect(checkbox).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only enable the identity provider buttons if the tos checkbox is checked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
|
|
||||||
|
|
||||||
const checkbox = screen.getByRole("checkbox");
|
|
||||||
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||||
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
|
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
|
||||||
|
|
||||||
expect(githubButton).toBeDisabled();
|
expect(githubButton).toBeInTheDocument();
|
||||||
expect(gitlabButton).toBeDisabled();
|
expect(gitlabButton).toBeInTheDocument();
|
||||||
|
|
||||||
await user.click(checkbox);
|
|
||||||
|
|
||||||
expect(githubButton).not.toBeDisabled();
|
|
||||||
expect(gitlabButton).not.toBeDisabled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set user analytics consent to true when the user checks the tos checkbox", async () => {
|
it("should redirect to GitHub auth URL when GitHub button is clicked", async () => {
|
||||||
const handleCaptureConsentSpy = vi.spyOn(
|
|
||||||
CaptureConsent,
|
|
||||||
"handleCaptureConsent",
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
|
const mockUrl = "https://github.com/login/oauth/authorize";
|
||||||
|
render(<AuthModal githubAuthUrl={mockUrl} appMode="saas" />);
|
||||||
|
|
||||||
const checkbox = screen.getByRole("checkbox");
|
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||||
await user.click(checkbox);
|
await user.click(githubButton);
|
||||||
|
|
||||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
expect(window.location.href).toBe(mockUrl);
|
||||||
await user.click(button);
|
|
||||||
|
|
||||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import AcceptTOS from "#/routes/accept-tos";
|
||||||
|
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||||
|
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { openHands } from "#/api/open-hands-axios";
|
||||||
|
|
||||||
|
// Mock the react-router hooks
|
||||||
|
vi.mock("react-router", () => ({
|
||||||
|
useNavigate: () => vi.fn(),
|
||||||
|
useSearchParams: () => [
|
||||||
|
{
|
||||||
|
get: (param: string) => {
|
||||||
|
if (param === "redirect_url") {
|
||||||
|
return "/dashboard";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the axios instance
|
||||||
|
vi.mock("#/api/open-hands-axios", () => ({
|
||||||
|
openHands: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the toast handlers
|
||||||
|
vi.mock("#/utils/custom-toast-handlers", () => ({
|
||||||
|
displayErrorToast: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create a wrapper with QueryClientProvider
|
||||||
|
const createWrapper = () => {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("AcceptTOS", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal("location", { href: "" });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a TOS checkbox that is unchecked by default", () => {
|
||||||
|
render(<AcceptTOS />, { wrapper: createWrapper() });
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||||
|
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
expect(continueButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enable the continue button when the TOS checkbox is checked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<AcceptTOS />, { wrapper: createWrapper() });
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||||
|
|
||||||
|
expect(continueButton).toBeDisabled();
|
||||||
|
|
||||||
|
await user.click(checkbox);
|
||||||
|
|
||||||
|
expect(continueButton).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set user analytics consent to true when the user accepts TOS", async () => {
|
||||||
|
const handleCaptureConsentSpy = vi.spyOn(
|
||||||
|
CaptureConsent,
|
||||||
|
"handleCaptureConsent",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the API response
|
||||||
|
vi.mocked(openHands.post).mockResolvedValue({
|
||||||
|
data: { redirect_url: "/dashboard" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<AcceptTOS />, { wrapper: createWrapper() });
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
await user.click(checkbox);
|
||||||
|
|
||||||
|
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||||
|
await user.click(continueButton);
|
||||||
|
|
||||||
|
// Wait for the mutation to complete
|
||||||
|
await new Promise(process.nextTick);
|
||||||
|
|
||||||
|
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||||
|
expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", {
|
||||||
|
redirect_url: "/dashboard",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle external redirect URLs", async () => {
|
||||||
|
// Mock the API response with an external URL
|
||||||
|
const externalUrl = "https://example.com/callback";
|
||||||
|
vi.mocked(openHands.post).mockResolvedValue({
|
||||||
|
data: { redirect_url: externalUrl },
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<AcceptTOS />, { wrapper: createWrapper() });
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
await user.click(checkbox);
|
||||||
|
|
||||||
|
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||||
|
await user.click(continueButton);
|
||||||
|
|
||||||
|
// Wait for the mutation to complete
|
||||||
|
await new Promise(process.nextTick);
|
||||||
|
|
||||||
|
expect(window.location.href).toBe(externalUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+1421
-2202
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "openhands-frontend",
|
"name": "openhands-frontend",
|
||||||
"version": "0.34.0",
|
"version": "0.35.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroui/react": "2.7.6",
|
"@heroui/react": "2.7.8",
|
||||||
"@microlink/react-json-view": "^1.26.1",
|
"@microlink/react-json-view": "^1.26.1",
|
||||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||||
"@react-router/node": "^7.5.2",
|
"@react-router/node": "^7.5.3",
|
||||||
"@react-router/serve": "^7.5.2",
|
"@react-router/serve": "^7.5.3",
|
||||||
"@react-types/shared": "^3.29.0",
|
"@react-types/shared": "^3.29.0",
|
||||||
"@reduxjs/toolkit": "^2.7.0",
|
"@reduxjs/toolkit": "^2.7.0",
|
||||||
"@stripe/react-stripe-js": "^3.6.0",
|
"@stripe/react-stripe-js": "^3.6.0",
|
||||||
"@stripe/stripe-js": "^7.2.0",
|
"@stripe/stripe-js": "^7.2.0",
|
||||||
"@tanstack/react-query": "^5.74.7",
|
"@tanstack/react-query": "^5.74.9",
|
||||||
"@vitejs/plugin-react": "^4.4.0",
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.4.0",
|
"@xterm/xterm": "^5.4.0",
|
||||||
@@ -24,14 +24,14 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||||
"framer-motion": "^12.9.2",
|
"framer-motion": "^12.9.2",
|
||||||
"i18next": "^25.0.1",
|
"i18next": "^25.0.2",
|
||||||
"i18next-browser-languagedetector": "^8.0.5",
|
"i18next-browser-languagedetector": "^8.0.5",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
"jose": "^6.0.10",
|
"jose": "^6.0.10",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"posthog-js": "^1.236.7",
|
"posthog-js": "^1.237.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-highlight": "^0.15.0",
|
"react-highlight": "^0.15.0",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^7.5.2",
|
"react-router": "^7.5.3",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"@babel/types": "^7.27.0",
|
"@babel/types": "^7.27.0",
|
||||||
"@mswjs/socket.io-binding": "^0.1.1",
|
"@mswjs/socket.io-binding": "^0.1.1",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@react-router/dev": "^7.5.2",
|
"@react-router/dev": "^7.5.3",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/eslint-plugin-query": "^5.74.7",
|
"@tanstack/eslint-plugin-query": "^5.74.7",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { I18nKey } from "#/i18n/declaration";
|
|||||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||||
import { TOSCheckbox } from "./tos-checkbox";
|
|
||||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
|
||||||
import { BrandButton } from "../settings/brand-button";
|
import { BrandButton } from "../settings/brand-button";
|
||||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||||
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
|
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
|
||||||
@@ -19,7 +17,6 @@ interface AuthModalProps {
|
|||||||
|
|
||||||
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
|
||||||
|
|
||||||
const gitlabAuthUrl = useAuthUrl({
|
const gitlabAuthUrl = useAuthUrl({
|
||||||
appMode: appMode || null,
|
appMode: appMode || null,
|
||||||
@@ -28,14 +25,14 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
|||||||
|
|
||||||
const handleGitHubAuth = () => {
|
const handleGitHubAuth = () => {
|
||||||
if (githubAuthUrl) {
|
if (githubAuthUrl) {
|
||||||
handleCaptureConsent(true);
|
// Always start the OIDC flow, let the backend handle TOS check
|
||||||
window.location.href = githubAuthUrl;
|
window.location.href = githubAuthUrl;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGitLabAuth = () => {
|
const handleGitLabAuth = () => {
|
||||||
if (gitlabAuthUrl) {
|
if (gitlabAuthUrl) {
|
||||||
handleCaptureConsent(true);
|
// Always start the OIDC flow, let the backend handle TOS check
|
||||||
window.location.href = gitlabAuthUrl;
|
window.location.href = gitlabAuthUrl;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -50,11 +47,8 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<BrandButton
|
<BrandButton
|
||||||
isDisabled={!isTosAccepted}
|
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleGitHubAuth}
|
onClick={handleGitHubAuth}
|
||||||
@@ -65,7 +59,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
|||||||
</BrandButton>
|
</BrandButton>
|
||||||
|
|
||||||
<BrandButton
|
<BrandButton
|
||||||
isDisabled={!isTosAccepted}
|
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleGitLabAuth}
|
onClick={handleGitLabAuth}
|
||||||
|
|||||||
@@ -13,21 +13,35 @@ import posthog from "posthog-js";
|
|||||||
import "./i18n";
|
import "./i18n";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
import { useConfig } from "./hooks/query/use-config";
|
|
||||||
import { AuthProvider } from "./context/auth-context";
|
import { AuthProvider } from "./context/auth-context";
|
||||||
import { queryClientConfig } from "./query-client-config";
|
import { queryClientConfig } from "./query-client-config";
|
||||||
|
import OpenHands from "./api/open-hands";
|
||||||
|
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||||
|
|
||||||
function PosthogInit() {
|
function PosthogInit() {
|
||||||
const { data: config } = useConfig();
|
const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (config?.POSTHOG_CLIENT_KEY) {
|
(async () => {
|
||||||
posthog.init(config.POSTHOG_CLIENT_KEY, {
|
try {
|
||||||
|
const config = await OpenHands.getConfig();
|
||||||
|
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
displayErrorToast("Error fetching PostHog client key");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (posthogClientKey) {
|
||||||
|
posthog.init(posthogClientKey, {
|
||||||
api_host: "https://us.i.posthog.com",
|
api_host: "https://us.i.posthog.com",
|
||||||
person_profiles: "identified_only",
|
person_profiles: "identified_only",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [posthogClientKey]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useConfig } from "./use-config";
|
import { useConfig } from "./use-config";
|
||||||
import OpenHands from "#/api/open-hands";
|
import OpenHands from "#/api/open-hands";
|
||||||
|
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||||
|
|
||||||
export const useBalance = () => {
|
export const useBalance = () => {
|
||||||
const { data: config } = useConfig();
|
const { data: config } = useConfig();
|
||||||
|
const isOnTosPage = useIsOnTosPage();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["user", "balance"],
|
queryKey: ["user", "balance"],
|
||||||
queryFn: OpenHands.getBalance,
|
queryFn: OpenHands.getBalance,
|
||||||
enabled:
|
enabled:
|
||||||
config?.APP_MODE === "saas" && config?.FEATURE_FLAGS.ENABLE_BILLING,
|
!isOnTosPage &&
|
||||||
|
config?.APP_MODE === "saas" &&
|
||||||
|
config?.FEATURE_FLAGS.ENABLE_BILLING,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import OpenHands from "#/api/open-hands";
|
import OpenHands from "#/api/open-hands";
|
||||||
|
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||||
|
|
||||||
export const useConfig = () =>
|
export const useConfig = () => {
|
||||||
useQuery({
|
const isOnTosPage = useIsOnTosPage();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
queryKey: ["config"],
|
queryKey: ["config"],
|
||||||
queryFn: OpenHands.getConfig,
|
queryFn: OpenHands.getConfig,
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
gcTime: 1000 * 60 * 15, // 15 minutes,
|
||||||
|
enabled: !isOnTosPage,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ import React from "react";
|
|||||||
import OpenHands from "#/api/open-hands";
|
import OpenHands from "#/api/open-hands";
|
||||||
import { useConfig } from "./use-config";
|
import { useConfig } from "./use-config";
|
||||||
import { useAuth } from "#/context/auth-context";
|
import { useAuth } from "#/context/auth-context";
|
||||||
|
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||||
|
|
||||||
export const useIsAuthed = () => {
|
export const useIsAuthed = () => {
|
||||||
const { providersAreSet } = useAuth();
|
const { providersAreSet } = useAuth();
|
||||||
const { data: config } = useConfig();
|
const { data: config } = useConfig();
|
||||||
|
const isOnTosPage = useIsOnTosPage();
|
||||||
|
|
||||||
const appMode = React.useMemo(() => config?.APP_MODE, [config]);
|
const appMode = React.useMemo(() => config?.APP_MODE, [config]);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["user", "authenticated", providersAreSet, appMode],
|
queryKey: ["user", "authenticated", providersAreSet, appMode],
|
||||||
queryFn: () => OpenHands.authenticate(appMode!),
|
queryFn: () => OpenHands.authenticate(appMode!),
|
||||||
enabled: !!appMode,
|
enabled: !!appMode && !isOnTosPage,
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import posthog from "posthog-js";
|
|||||||
import OpenHands from "#/api/open-hands";
|
import OpenHands from "#/api/open-hands";
|
||||||
import { useAuth } from "#/context/auth-context";
|
import { useAuth } from "#/context/auth-context";
|
||||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||||
|
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||||
import { Settings } from "#/types/settings";
|
import { Settings } from "#/types/settings";
|
||||||
|
|
||||||
const getSettingsQueryFn = async (): Promise<Settings> => {
|
const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||||
@@ -31,6 +32,8 @@ export const useSettings = () => {
|
|||||||
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
|
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
|
||||||
useAuth();
|
useAuth();
|
||||||
|
|
||||||
|
const isOnTosPage = useIsOnTosPage();
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["settings", providerTokensSet],
|
queryKey: ["settings", providerTokensSet],
|
||||||
queryFn: getSettingsQueryFn,
|
queryFn: getSettingsQueryFn,
|
||||||
@@ -40,6 +43,7 @@ export const useSettings = () => {
|
|||||||
retry: (_, error) => error.status !== 404,
|
retry: (_, error) => error.status !== 404,
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||||
|
enabled: !isOnTosPage,
|
||||||
meta: {
|
meta: {
|
||||||
disableToast: true,
|
disableToast: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if the current page is the Terms of Service acceptance page.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if the current page is the TOS acceptance page, false otherwise.
|
||||||
|
*/
|
||||||
|
export const useIsOnTosPage = (): boolean => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
return pathname === "/accept-tos";
|
||||||
|
};
|
||||||
@@ -469,4 +469,8 @@ export enum I18nKey {
|
|||||||
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
|
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
|
||||||
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
|
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
|
||||||
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
|
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
|
||||||
|
TOS$ACCEPT_TERMS_OF_SERVICE = "TOS$ACCEPT_TERMS_OF_SERVICE",
|
||||||
|
TOS$ACCEPT_TERMS_DESCRIPTION = "TOS$ACCEPT_TERMS_DESCRIPTION",
|
||||||
|
TOS$CONTINUE = "TOS$CONTINUE",
|
||||||
|
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6559,6 +6559,21 @@
|
|||||||
"tr": "belgelendirme",
|
"tr": "belgelendirme",
|
||||||
"de": "Dokumentation"
|
"de": "Dokumentation"
|
||||||
},
|
},
|
||||||
|
"AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED": {
|
||||||
|
"en": "The action has not been executed. This may have occurred because the user pressed the stop button, or because the runtime system crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.",
|
||||||
|
"ja": "アクションは実行されていません。これはユーザーが停止ボタンを押したか、リソース制約によりランタイムシステムがクラッシュして再起動したことが原因かもしれません。以前に確立されたシステム状態、依存関係、または環境変数は失われている可能性があります。",
|
||||||
|
"zh-CN": "该操作尚未执行。这可能是因为用户按下了停止按钮,或者因为运行时系统由于资源限制而崩溃并重新启动。任何先前建立的系统状态、依赖项或环境变量可能已丢失。",
|
||||||
|
"zh-TW": "該操作尚未執行。這可能是因為用戶按下了停止按鈕,或者因為運行時系統由於資源限制而崩潰並重新啟動。任何先前建立的系統狀態、依賴項或環境變數可能已丟失。",
|
||||||
|
"ko-KR": "작업이 실행되지 않았습니다. 이는 사용자가 중지 버튼을 눌렀거나 리소스 제약으로 인해 런타임 시스템이 충돌하고 재시작되었기 때문일 수 있습니다. 이전에 설정된 시스템 상태, 종속성 또는 환경 변수가 손실되었을 수 있습니다.",
|
||||||
|
"no": "Handlingen har ikke blitt utført. Dette kan ha skjedd fordi brukeren trykket på stoppknappen, eller fordi kjøretidssystemet krasjet og startet på nytt på grunn av ressursbegrensninger. Enhver tidligere etablert systemtilstand, avhengigheter eller miljøvariabler kan ha gått tapt.",
|
||||||
|
"it": "L'azione non è stata eseguita. Ciò potrebbe essere accaduto perché l'utente ha premuto il pulsante di arresto, o perché il sistema di runtime si è arrestato in modo anomalo e riavviato a causa di vincoli di risorse. Qualsiasi stato di sistema, dipendenza o variabile d'ambiente precedentemente stabilito potrebbe essere andato perso.",
|
||||||
|
"pt": "A ação não foi executada. Isso pode ter ocorrido porque o usuário pressionou o botão de parar, ou porque o sistema de tempo de execução travou e reiniciou devido a restrições de recursos. Qualquer estado do sistema, dependências ou variáveis de ambiente estabelecidos anteriormente podem ter sido perdidos.",
|
||||||
|
"es": "La acción no se ha ejecutado. Esto puede haber ocurrido porque el usuario presionó el botón de detener, o porque el sistema de tiempo de ejecución se bloqueó y reinició debido a restricciones de recursos. Cualquier estado del sistema, dependencias o variables de entorno establecidos previamente pueden haberse perdido.",
|
||||||
|
"ar": "لم يتم تنفيذ الإجراء. قد يكون هذا حدث لأن المستخدم ضغط على زر التوقف، أو لأن نظام التشغيل تعطل وأعيد تشغيله بسبب قيود الموارد. قد تكون أي حالة نظام أو تبعيات أو متغيرات بيئية تم إنشاؤها مسبقًا قد فُقدت.",
|
||||||
|
"fr": "L'action n'a pas été exécutée. Cela peut s'être produit parce que l'utilisateur a appuyé sur le bouton d'arrêt, ou parce que le système d'exécution s'est planté et a redémarré en raison de contraintes de ressources. Tout état du système, dépendances ou variables d'environnement précédemment établis peuvent avoir été perdus.",
|
||||||
|
"tr": "Eylem yürütülmedi. Bu, kullanıcının durdurma düğmesine basması veya çalışma zamanı sisteminin kaynak kısıtlamaları nedeniyle çökmesi ve yeniden başlaması nedeniyle olmuş olabilir. Daha önce kurulmuş olan herhangi bir sistem durumu, bağımlılıklar veya ortam değişkenleri kaybolmuş olabilir.",
|
||||||
|
"de": "Die Aktion wurde nicht ausgeführt. Dies kann passiert sein, weil der Benutzer die Stopp-Taste gedrückt hat oder weil das Laufzeitsystem aufgrund von Ressourcenbeschränkungen abgestürzt und neu gestartet wurde. Alle zuvor eingerichteten Systemzustände, Abhängigkeiten oder Umgebungsvariablen sind möglicherweise verloren gegangen."
|
||||||
|
},
|
||||||
"DIFF_VIEWER$LOADING": {
|
"DIFF_VIEWER$LOADING": {
|
||||||
"en": "Loading...",
|
"en": "Loading...",
|
||||||
"ja": "読み込み中...",
|
"ja": "読み込み中...",
|
||||||
@@ -6754,4 +6769,53 @@
|
|||||||
"es": "No hay herramientas disponibles para este agente",
|
"es": "No hay herramientas disponibles para este agente",
|
||||||
"tr": "Bu ajan için kullanılabilir araç yok"
|
"tr": "Bu ajan için kullanılabilir araç yok"
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
"TOS$ACCEPT_TERMS_OF_SERVICE": {
|
||||||
|
"en": "Accept Terms of Service",
|
||||||
|
"ja": "利用規約に同意する",
|
||||||
|
"zh-CN": "接受服务条款",
|
||||||
|
"zh-TW": "接受服務條款",
|
||||||
|
"ko-KR": "서비스 약관 동의",
|
||||||
|
"fr": "Accepter les conditions d'utilisation",
|
||||||
|
"es": "Aceptar términos de servicio",
|
||||||
|
"de": "Nutzungsbedingungen akzeptieren",
|
||||||
|
"it": "Accetta i termini di servizio",
|
||||||
|
"pt": "Aceitar termos de serviço"
|
||||||
|
},
|
||||||
|
"TOS$ACCEPT_TERMS_DESCRIPTION": {
|
||||||
|
"en": "Please review and accept our terms of service before continuing",
|
||||||
|
"ja": "続行する前に利用規約を確認して同意してください",
|
||||||
|
"zh-CN": "请在继续之前查看并接受我们的服务条款",
|
||||||
|
"zh-TW": "請在繼續之前查看並接受我們的服務條款",
|
||||||
|
"ko-KR": "계속하기 전에 서비스 약관을 검토하고 동의해 주세요",
|
||||||
|
"fr": "Veuillez examiner et accepter nos conditions d'utilisation avant de continuer",
|
||||||
|
"es": "Por favor, revise y acepte nuestros términos de servicio antes de continuar",
|
||||||
|
"de": "Bitte überprüfen und akzeptieren Sie unsere Nutzungsbedingungen, bevor Sie fortfahren",
|
||||||
|
"it": "Si prega di rivedere e accettare i nostri termini di servizio prima di continuare",
|
||||||
|
"pt": "Por favor, revise e aceite nossos termos de serviço antes de continuar"
|
||||||
|
},
|
||||||
|
"TOS$CONTINUE": {
|
||||||
|
"en": "Continue",
|
||||||
|
"ja": "続行",
|
||||||
|
"zh-CN": "继续",
|
||||||
|
"zh-TW": "繼續",
|
||||||
|
"ko-KR": "계속",
|
||||||
|
"fr": "Continuer",
|
||||||
|
"es": "Continuar",
|
||||||
|
"de": "Fortfahren",
|
||||||
|
"it": "Continua",
|
||||||
|
"pt": "Continuar"
|
||||||
|
},
|
||||||
|
"TOS$ERROR_ACCEPTING": {
|
||||||
|
"en": "Error accepting Terms of Service",
|
||||||
|
"ja": "利用規約の承諾中にエラーが発生しました",
|
||||||
|
"zh-CN": "接受服务条款时出错",
|
||||||
|
"zh-TW": "接受服務條款時出錯",
|
||||||
|
"ko-KR": "서비스 약관 수락 중 오류 발생",
|
||||||
|
"fr": "Erreur lors de l'acceptation des conditions d'utilisation",
|
||||||
|
"es": "Error al aceptar los Términos de Servicio",
|
||||||
|
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
|
||||||
|
"it": "Errore nell'accettazione dei Termini di Servizio",
|
||||||
|
"pt": "Erro ao aceitar os Termos de Serviço"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
export default [
|
export default [
|
||||||
layout("routes/root-layout.tsx", [
|
layout("routes/root-layout.tsx", [
|
||||||
index("routes/home.tsx"),
|
index("routes/home.tsx"),
|
||||||
|
route("accept-tos", "routes/accept-tos.tsx"),
|
||||||
route("settings", "routes/settings.tsx", [
|
route("settings", "routes/settings.tsx", [
|
||||||
index("routes/llm-settings.tsx"),
|
index("routes/llm-settings.tsx"),
|
||||||
route("git", "routes/git-settings.tsx"),
|
route("git", "routes/git-settings.tsx"),
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { I18nKey } from "#/i18n/declaration";
|
||||||
|
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||||
|
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
|
||||||
|
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||||
|
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||||
|
import { openHands } from "#/api/open-hands-axios";
|
||||||
|
|
||||||
|
export default function AcceptTOS() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||||
|
|
||||||
|
// Get the redirect URL from the query parameters
|
||||||
|
const redirectUrl = searchParams.get("redirect_url") || "/";
|
||||||
|
|
||||||
|
// Use mutation for accepting TOS
|
||||||
|
const { mutate: acceptTOS, isPending: isSubmitting } = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
// Set consent for analytics
|
||||||
|
handleCaptureConsent(true);
|
||||||
|
|
||||||
|
// Call the API to record TOS acceptance in the database
|
||||||
|
return openHands.post("/api/accept_tos", {
|
||||||
|
redirect_url: redirectUrl,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (response) => {
|
||||||
|
// Get the redirect URL from the response
|
||||||
|
const finalRedirectUrl = response.data.redirect_url || redirectUrl;
|
||||||
|
|
||||||
|
// Check if the redirect URL is an external URL (starts with http or https)
|
||||||
|
if (
|
||||||
|
finalRedirectUrl.startsWith("http://") ||
|
||||||
|
finalRedirectUrl.startsWith("https://")
|
||||||
|
) {
|
||||||
|
// For external URLs, redirect using window.location
|
||||||
|
window.location.href = finalRedirectUrl;
|
||||||
|
} else {
|
||||||
|
// For internal routes, use navigate
|
||||||
|
navigate(finalRedirectUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAcceptTOS = () => {
|
||||||
|
if (isTosAccepted && !isSubmitting) {
|
||||||
|
acceptTOS();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<div className="border border-tertiary p-8 rounded-lg max-w-md w-full flex flex-col gap-6 items-center bg-base-secondary">
|
||||||
|
<AllHandsLogo width={68} height={46} />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||||
|
<h1 className="text-2xl font-bold">
|
||||||
|
{t(I18nKey.TOS$ACCEPT_TERMS_OF_SERVICE)}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{t(I18nKey.TOS$ACCEPT_TERMS_DESCRIPTION)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
|
||||||
|
|
||||||
|
<BrandButton
|
||||||
|
isDisabled={!isTosAccepted || isSubmitting}
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleAcceptTOS}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isSubmitting ? t(I18nKey.HOME$LOADING) : t(I18nKey.TOS$CONTINUE)}
|
||||||
|
</BrandButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
|
|||||||
import { useBalance } from "#/hooks/query/use-balance";
|
import { useBalance } from "#/hooks/query/use-balance";
|
||||||
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
|
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
|
||||||
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||||
|
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||||
|
|
||||||
export function ErrorBoundary() {
|
export function ErrorBoundary() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
@@ -58,6 +59,7 @@ export function ErrorBoundary() {
|
|||||||
export default function MainApp() {
|
export default function MainApp() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const tosPageStatus = useIsOnTosPage();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { data: settings } = useSettings();
|
const { data: settings } = useSettings();
|
||||||
const { error, isFetching } = useBalance();
|
const { error, isFetching } = useBalance();
|
||||||
@@ -71,49 +73,75 @@ export default function MainApp() {
|
|||||||
isError: authError,
|
isError: authError,
|
||||||
} = useIsAuthed();
|
} = useIsAuthed();
|
||||||
|
|
||||||
|
// Always call the hook, but we'll only use the result when not on TOS page
|
||||||
const gitHubAuthUrl = useGitHubAuthUrl({
|
const gitHubAuthUrl = useGitHubAuthUrl({
|
||||||
appMode: config.data?.APP_MODE || null,
|
appMode: config.data?.APP_MODE || null,
|
||||||
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
|
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When on TOS page, we don't use the GitHub auth URL
|
||||||
|
const effectiveGitHubAuthUrl = tosPageStatus ? null : gitHubAuthUrl;
|
||||||
|
|
||||||
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
|
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (settings?.LANGUAGE) {
|
// Don't change language when on TOS page
|
||||||
|
if (!tosPageStatus && settings?.LANGUAGE) {
|
||||||
i18n.changeLanguage(settings.LANGUAGE);
|
i18n.changeLanguage(settings.LANGUAGE);
|
||||||
}
|
}
|
||||||
}, [settings?.LANGUAGE]);
|
}, [settings?.LANGUAGE, tosPageStatus]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const consentFormModalIsOpen =
|
// Don't show consent form when on TOS page
|
||||||
settings?.USER_CONSENTS_TO_ANALYTICS === null;
|
if (!tosPageStatus) {
|
||||||
|
const consentFormModalIsOpen =
|
||||||
|
settings?.USER_CONSENTS_TO_ANALYTICS === null;
|
||||||
|
|
||||||
setConsentFormIsOpen(consentFormModalIsOpen);
|
setConsentFormIsOpen(consentFormModalIsOpen);
|
||||||
}, [settings]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// Migrate user consent to the server if it was previously stored in localStorage
|
|
||||||
migrateUserConsent({
|
|
||||||
handleAnalyticsWasPresentInLocalStorage: () => {
|
|
||||||
setConsentFormIsOpen(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// Don't allow users to use the app if it 402s
|
|
||||||
if (error?.status === 402 && pathname !== "/") {
|
|
||||||
navigate("/");
|
|
||||||
} else if (!isFetching && searchParams.get("free_credits") === "success") {
|
|
||||||
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
|
|
||||||
searchParams.delete("free_credits");
|
|
||||||
navigate("/");
|
|
||||||
}
|
}
|
||||||
}, [error?.status, pathname, isFetching]);
|
}, [settings, tosPageStatus]);
|
||||||
|
|
||||||
const userIsAuthed = !!isAuthed && !authError;
|
React.useEffect(() => {
|
||||||
|
// Don't migrate user consent when on TOS page
|
||||||
|
if (!tosPageStatus) {
|
||||||
|
// Migrate user consent to the server if it was previously stored in localStorage
|
||||||
|
migrateUserConsent({
|
||||||
|
handleAnalyticsWasPresentInLocalStorage: () => {
|
||||||
|
setConsentFormIsOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [tosPageStatus]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Don't do any redirects when on TOS page
|
||||||
|
if (!tosPageStatus) {
|
||||||
|
// Don't allow users to use the app if it 402s
|
||||||
|
if (error?.status === 402 && pathname !== "/") {
|
||||||
|
navigate("/");
|
||||||
|
} else if (
|
||||||
|
!isFetching &&
|
||||||
|
searchParams.get("free_credits") === "success"
|
||||||
|
) {
|
||||||
|
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
|
||||||
|
searchParams.delete("free_credits");
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [error?.status, pathname, isFetching, tosPageStatus]);
|
||||||
|
|
||||||
|
// When on TOS page, we don't make any API calls, so we need to handle this case
|
||||||
|
const userIsAuthed = tosPageStatus ? false : !!isAuthed && !authError;
|
||||||
|
|
||||||
|
// Only show the auth modal if:
|
||||||
|
// 1. User is not authenticated
|
||||||
|
// 2. We're not currently on the TOS page
|
||||||
|
// 3. We're in SaaS mode
|
||||||
const renderAuthModal =
|
const renderAuthModal =
|
||||||
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
|
!isFetchingAuth &&
|
||||||
|
!userIsAuthed &&
|
||||||
|
!tosPageStatus &&
|
||||||
|
config.data?.APP_MODE === "saas";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -131,7 +159,7 @@ export default function MainApp() {
|
|||||||
|
|
||||||
{renderAuthModal && (
|
{renderAuthModal && (
|
||||||
<AuthModal
|
<AuthModal
|
||||||
githubAuthUrl={gitHubAuthUrl}
|
githubAuthUrl={effectiveGitHubAuthUrl}
|
||||||
appMode={config.data?.APP_MODE}
|
appMode={config.data?.APP_MODE}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Checks if the current page is the Terms of Service acceptance page.
|
||||||
|
* This function works outside of React Router context by checking window.location directly.
|
||||||
|
*
|
||||||
|
* @param {string} [pathname] - Optional pathname from React Router's useLocation hook
|
||||||
|
* @returns {boolean} True if the current page is the TOS acceptance page, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isOnTosPage = (pathname?: string): boolean => {
|
||||||
|
// If pathname is provided (from React Router), use it
|
||||||
|
if (pathname !== undefined) {
|
||||||
|
return pathname === "/accept-tos";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise check window.location (works outside React Router context)
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return window.location.pathname === "/accept-tos";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -18,6 +18,10 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("#/hooks/use-is-on-tos-page", () => ({
|
||||||
|
useIsOnTosPage: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock requests during tests
|
// Mock requests during tests
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
|
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ Your primary role is to assist users by executing commands, modifying code, and
|
|||||||
* For bug fixes: Create tests to verify issues before implementing fixes
|
* For bug fixes: Create tests to verify issues before implementing fixes
|
||||||
* For new features: Consider test-driven development when appropriate
|
* For new features: Consider test-driven development when appropriate
|
||||||
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
|
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
|
||||||
|
* If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies
|
||||||
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
|
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
|
||||||
5. VERIFICATION: Test your implementation thoroughly, including edge cases
|
5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests.
|
||||||
</PROBLEM_SOLVING_WORKFLOW>
|
</PROBLEM_SOLVING_WORKFLOW>
|
||||||
|
|
||||||
<SECURITY>
|
<SECURITY>
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ from openhands.llm.metrics import Metrics, TokenUsage
|
|||||||
TRAFFIC_CONTROL_REMINDER = (
|
TRAFFIC_CONTROL_REMINDER = (
|
||||||
"Please click on resume button if you'd like to continue, or start a new task."
|
"Please click on resume button if you'd like to continue, or start a new task."
|
||||||
)
|
)
|
||||||
|
ERROR_ACTION_NOT_EXECUTED_ID = 'AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED'
|
||||||
|
ERROR_ACTION_NOT_EXECUTED = 'The action has not been executed. This may have occurred because the user pressed the stop button, or because the runtime system crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.'
|
||||||
|
|
||||||
|
|
||||||
class AgentController:
|
class AgentController:
|
||||||
@@ -566,7 +568,10 @@ class AgentController:
|
|||||||
|
|
||||||
# make a new ErrorObservation with the tool call metadata
|
# make a new ErrorObservation with the tool call metadata
|
||||||
if not found_observation:
|
if not found_observation:
|
||||||
obs = ErrorObservation(content='The action has not been executed.')
|
obs = ErrorObservation(
|
||||||
|
content=ERROR_ACTION_NOT_EXECUTED,
|
||||||
|
error_id=ERROR_ACTION_NOT_EXECUTED_ID,
|
||||||
|
)
|
||||||
obs.tool_call_metadata = self._pending_action.tool_call_metadata
|
obs.tool_call_metadata = self._pending_action.tool_call_metadata
|
||||||
obs._cause = self._pending_action.id # type: ignore[attr-defined]
|
obs._cause = self._pending_action.id # type: ignore[attr-defined]
|
||||||
self.event_stream.add_event(obs, EventSource.AGENT)
|
self.event_stream.add_event(obs, EventSource.AGENT)
|
||||||
@@ -842,6 +847,8 @@ class AgentController:
|
|||||||
'contextwindowexceedederror' in error_str
|
'contextwindowexceedederror' in error_str
|
||||||
or 'prompt is too long' in error_str
|
or 'prompt is too long' in error_str
|
||||||
or 'input length and `max_tokens` exceed context limit' in error_str
|
or 'input length and `max_tokens` exceed context limit' in error_str
|
||||||
|
or 'please reduce the length of either one'
|
||||||
|
in error_str # For OpenRouter context window errors
|
||||||
or isinstance(e, ContextWindowExceededError)
|
or isinstance(e, ContextWindowExceededError)
|
||||||
):
|
):
|
||||||
if self.agent.config.enable_history_truncation:
|
if self.agent.config.enable_history_truncation:
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA = Annotated[
|
|||||||
PROVIDER_TOKEN_TYPE,
|
PROVIDER_TOKEN_TYPE,
|
||||||
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}),
|
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}),
|
||||||
]
|
]
|
||||||
|
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA = Annotated[
|
||||||
|
CUSTOM_SECRETS_TYPE,
|
||||||
|
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class SecretStore(BaseModel):
|
class SecretStore(BaseModel):
|
||||||
@@ -67,8 +71,8 @@ class SecretStore(BaseModel):
|
|||||||
default_factory=lambda: MappingProxyType({})
|
default_factory=lambda: MappingProxyType({})
|
||||||
)
|
)
|
||||||
|
|
||||||
custom_secrets: CUSTOM_SECRETS_TYPE = Field(
|
custom_secrets: CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA = Field(
|
||||||
default_factory=lambda: MappingProxyType({})
|
default_factory=lambda: MappingProxyType({}),
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
|
|||||||
@@ -230,8 +230,8 @@ class ConversationMemory:
|
|||||||
pending_tool_call_action_messages[llm_response.id] = Message(
|
pending_tool_call_action_messages[llm_response.id] = Message(
|
||||||
role=getattr(assistant_msg, 'role', 'assistant'),
|
role=getattr(assistant_msg, 'role', 'assistant'),
|
||||||
# tool call content SHOULD BE a string
|
# tool call content SHOULD BE a string
|
||||||
content=[TextContent(text=assistant_msg.content or '')]
|
content=[TextContent(text=assistant_msg.content)]
|
||||||
if assistant_msg.content is not None
|
if assistant_msg.content and assistant_msg.content.strip()
|
||||||
else [],
|
else [],
|
||||||
tool_calls=assistant_msg.tool_calls,
|
tool_calls=assistant_msg.tool_calls,
|
||||||
)
|
)
|
||||||
|
|||||||
+116
-66
@@ -1,141 +1,191 @@
|
|||||||
# OpenHands GitHub & GitLab Issue Resolver 🙌
|
# OpenHands Github & Gitlab Issue Resolver 🙌
|
||||||
|
|
||||||
Need help resolving GitHub or GitLab issues? Let an AI agent help you out!
|
Need help resolving a GitHub issue but don't have the time to do it yourself? Let an AI agent help you out!
|
||||||
|
|
||||||
This tool uses [OpenHands](https://github.com/all-hands-ai/openhands) AI agents to automatically resolve issues in your repositories. It's designed to handle one issue at a time with high quality.
|
This tool allows you to use open-source AI agents based on [OpenHands](https://github.com/all-hands-ai/openhands)
|
||||||
|
to attempt to resolve GitHub issues automatically. While it can handle multiple issues, it's primarily designed
|
||||||
|
to help you resolve one issue at a time with high quality.
|
||||||
|
|
||||||
## 1. Setting Up for GitHub (Action Workflow)
|
Getting started is simple - just follow the instructions below.
|
||||||
|
|
||||||
### Prerequisites
|
## Using the GitHub Actions Workflow
|
||||||
|
|
||||||
- [Create a personal access token](https://github.com/settings/tokens?type=beta) with read/write scope for
|
This repository includes a GitHub Actions workflow that can automatically attempt to fix individual issues labeled with 'fix-me'.
|
||||||
|
Follow these steps to use this workflow in your own repository:
|
||||||
|
|
||||||
- "contents"
|
1. [Create a personal access token](https://github.com/settings/tokens?type=beta) with read/write scope for "contents", "issues", "pull requests", and "workflows"
|
||||||
- "issues"
|
|
||||||
- "pull requests"
|
|
||||||
- "workflows"
|
|
||||||
|
|
||||||
- Create an LLM API key (e,g [Claude API](https://www.anthropic.com/api))
|
Note: If you're working with an organizational repository, you may need to configure the organization's personal access token policy first. See [Setting a personal access token policy for your organization](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization) for details.
|
||||||
|
|
||||||
### Installation
|
2. Create an API key for the [Claude API](https://www.anthropic.com/api) (recommended) or another supported LLM service
|
||||||
|
|
||||||
1. Copy `examples/openhands-resolver.yml` to your repository's `.github/workflows/` directory
|
3. Copy `examples/openhands-resolver.yml` to your repository's `.github/workflows/` directory
|
||||||
|
|
||||||
2. Configure repository permissions:
|
4. Configure repository permissions:
|
||||||
|
- Go to `Settings -> Actions -> General -> Workflow permissions`
|
||||||
|
- Select "Read and write permissions"
|
||||||
|
- Enable "Allow Github Actions to create and approve pull requests"
|
||||||
|
|
||||||
- Go to `Settings -> Actions -> General -> Workflow permissions`
|
Note: If the "Read and write permissions" option is greyed out:
|
||||||
- Select **Read and write permissions**
|
- First check if permissions need to be set at the organization level
|
||||||
- Enable **Allow Github Actions to create and approve pull requests**
|
- If still greyed out at the organization level, permissions need to be set in the [Enterprise policy settings](https://docs.github.com/en/enterprise-cloud@latest/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise#enforcing-a-policy-for-workflow-permissions-in-your-enterprise)
|
||||||
|
|
||||||
> If "Read and write permissions" is greyed out:
|
|
||||||
>
|
|
||||||
> - Check organization settings first
|
|
||||||
> - Otherwise, permissions might need to be set in [Enterprise policy settings](https://docs.github.com/en/enterprise-cloud@latest/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise#enforcing-a-policy-for-workflow-permissions-in-your-enterprise)
|
|
||||||
|
|
||||||
3. Set up [GitHub secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions):
|
|
||||||
|
|
||||||
|
5. Set up [GitHub secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions):
|
||||||
- Required:
|
- Required:
|
||||||
- `LLM_API_KEY`: Your LLM API key
|
- `LLM_API_KEY`: Your LLM API key
|
||||||
- Optional:
|
- Optional:
|
||||||
- `PAT_USERNAME`: GitHub username for the personal access token
|
- `PAT_USERNAME`: GitHub username for the personal access token
|
||||||
- `PAT_TOKEN`: The personal access token
|
- `PAT_TOKEN`: The personal access token
|
||||||
- `LLM_BASE_URL`: Base URL for LLM API (only if using a proxy)
|
- `LLM_BASE_URL`: Base URL for LLM API (only if using a proxy)
|
||||||
- [See how to customize more configurations](https://docs.all-hands.dev/modules/usage/how-to/github-action#custom-configurations)
|
|
||||||
|
|
||||||
## 2. Setting up GitLab (CI Runner)
|
Note: You can set these secrets at the organization level to use across multiple repositories.
|
||||||
|
|
||||||
### Prerequisites
|
6. Set up any [custom configurations required](https://docs.all-hands.dev/modules/usage/how-to/github-action#custom-configurations)
|
||||||
|
|
||||||
Create a GitLab Personal Access Token with API, read/write access
|
7. Usage:
|
||||||
|
There are two ways to trigger the OpenHands agent:
|
||||||
|
|
||||||
### Installation
|
a. Using the 'fix-me' label:
|
||||||
|
- Add the 'fix-me' label to any issue you want the AI to resolve
|
||||||
|
- The agent will consider all comments in the issue thread when resolving
|
||||||
|
- The workflow will:
|
||||||
|
1. Attempt to resolve the issue using OpenHands
|
||||||
|
2. Create a draft PR if successful, or push a branch if unsuccessful
|
||||||
|
3. Comment on the issue with the results
|
||||||
|
4. Remove the 'fix-me' label once processed
|
||||||
|
|
||||||
## 3. Triggering OpenHands Agent
|
b. Using `@openhands-agent` mention:
|
||||||
|
- Create a new comment containing `@openhands-agent` in any issue
|
||||||
|
- The agent will only consider the comment where it's mentioned
|
||||||
|
- The workflow will:
|
||||||
|
1. Attempt to resolve the issue based on the specific comment
|
||||||
|
2. Create a draft PR if successful, or push a branch if unsuccessful
|
||||||
|
3. Comment on the issue with the results
|
||||||
|
|
||||||
You can trigger OpenHands in two shared ways (works for both GitHub and GitLab):
|
Need help? Feel free to [open an issue](https://github.com/all-hands-ai/openhands/issues) or email us at [contact@all-hands.dev](mailto:contact@all-hands.dev).
|
||||||
|
|
||||||
Using the 'fix-me' label:
|
## Manual Installation
|
||||||
|
|
||||||
- Add the 'fix-me' label to any issue you want the AI to resolve
|
If you prefer to run the resolver programmatically instead of using GitHub Actions, follow these steps:
|
||||||
- The agent will consider all comments in the issue thread when resolving
|
|
||||||
|
|
||||||
Using `@openhands-agent` in an issue/pr comment:
|
1. Install the package:
|
||||||
|
|
||||||
- Create a new comment containing `@openhands-agent`
|
|
||||||
- The agent will only consider the comment + comment thread where it's mentioned
|
|
||||||
|
|
||||||
## 4. Running Locally
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install openhands-ai
|
pip install openhands-ai
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setup
|
2. Create a GitHub or GitLab access token:
|
||||||
|
- Create a GitHub acces token
|
||||||
|
- Visit [GitHub's token settings](https://github.com/settings/personal-access-tokens/new)
|
||||||
|
- Create a fine-grained token with these scopes:
|
||||||
|
- "Content"
|
||||||
|
- "Pull requests"
|
||||||
|
- "Issues"
|
||||||
|
- "Workflows"
|
||||||
|
- If you don't have push access to the target repo, you can fork it first
|
||||||
|
|
||||||
Create a GitHub or GitLab access token with appropriate permissions
|
- Create a GitLab acces token
|
||||||
|
- Visit [GitLab's token settings](https://gitlab.com/-/user_settings/personal_access_tokens)
|
||||||
|
- Create a fine-grained token with these scopes:
|
||||||
|
- 'api'
|
||||||
|
- 'read_api'
|
||||||
|
- 'read_user'
|
||||||
|
- 'read_repository'
|
||||||
|
- 'write_repository'
|
||||||
|
|
||||||
Set up environment variables:
|
3. Set up environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GitHub credentials
|
|
||||||
export GITHUB_TOKEN="your-github-token"
|
|
||||||
export GIT_USERNAME="your-github-username"
|
|
||||||
|
|
||||||
# GitLab credentials (if using GitLab)
|
# GitHub credentials
|
||||||
|
|
||||||
|
export GITHUB_TOKEN="your-github-token"
|
||||||
|
export GIT_USERNAME="your-github-username" # Optional, defaults to token owner
|
||||||
|
|
||||||
|
# GitLab credentials if you're using GitLab repo
|
||||||
|
|
||||||
export GITLAB_TOKEN="your-gitlab-token"
|
export GITLAB_TOKEN="your-gitlab-token"
|
||||||
export GIT_USERNAME="your-gitlab-username"
|
export GIT_USERNAME="your-gitlab-username" # Optional, defaults to token owner
|
||||||
|
|
||||||
# LLM configuration
|
# LLM configuration
|
||||||
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
|
|
||||||
|
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022" # Recommended
|
||||||
export LLM_API_KEY="your-llm-api-key"
|
export LLM_API_KEY="your-llm-api-key"
|
||||||
export LLM_BASE_URL="your-api-url" # Optional
|
export LLM_BASE_URL="your-api-url" # Optional, for API proxies
|
||||||
```
|
```
|
||||||
|
|
||||||
### Resolving Issues
|
Note: OpenHands works best with powerful models like Anthropic's Claude or OpenAI's GPT-4. While other models are supported, they may not perform as well for complex issue resolution.
|
||||||
|
|
||||||
Resolve a single issue:
|
## Resolving Issues
|
||||||
|
|
||||||
|
The resolver can automatically attempt to fix a single issue in your repository using the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m openhands.resolver.resolve_issue --selected-repo [OWNER]/[REPO] --issue-number [NUMBER]
|
python -m openhands.resolver.resolve_issue --selected-repo [OWNER]/[REPO] --issue-number [NUMBER]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Responding to PR Comments
|
For instance, if you want to resolve issue #100 in this repo, you would run:
|
||||||
|
|
||||||
Respond to comments on pull requests:
|
```bash
|
||||||
|
python -m openhands.resolver.resolve_issue --selected-repo all-hands-ai/openhands --issue-number 100
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will be written to the `output/` directory.
|
||||||
|
|
||||||
|
If you've installed the package from source using poetry, you can use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry run python openhands/resolver/resolve_issue.py --selected-repo all-hands-ai/openhands --issue-number 100
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responding to PR Comments
|
||||||
|
|
||||||
|
The resolver can also respond to comments on pull requests using:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m openhands.resolver.send_pull_request --issue-number PR_NUMBER --issue-type pr
|
python -m openhands.resolver.send_pull_request --issue-number PR_NUMBER --issue-type pr
|
||||||
```
|
```
|
||||||
|
|
||||||
### Visualizing Results
|
This functionality is available both through the GitHub Actions workflow and when running the resolver locally.
|
||||||
|
|
||||||
View successful PRs:
|
## Visualizing successful PRs
|
||||||
|
|
||||||
|
To find successful PRs, you can run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep '"success":true' output/output.jsonl | sed 's/.*\("number":[0-9]*\).*/\1/g'
|
grep '"success":true' output/output.jsonl | sed 's/.*\("number":[0-9]*\).*/\1/g'
|
||||||
```
|
```
|
||||||
|
|
||||||
Visualize specific PR:
|
Then you can go through and visualize the ones you'd like.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m openhands.resolver.visualize_resolver_output --issue-number ISSUE_NUMBER --vis-method json
|
python -m openhands.resolver.visualize_resolver_output --issue-number ISSUE_NUMBER --vis-method json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Uploading PRs
|
## Uploading PRs
|
||||||
|
|
||||||
Upload your changes in one of three ways:
|
If you find any PRs that were successful, you can upload them.
|
||||||
|
There are three ways you can upload:
|
||||||
|
|
||||||
|
1. `branch` - upload a branch without creating a PR
|
||||||
|
2. `draft` - create a draft PR
|
||||||
|
3. `ready` - create a non-draft PR that's ready for review
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m openhands.resolver.send_pull_request --issue-number ISSUE_NUMBER --username YOUR_GITHUB_OR_GITLAB_USERNAME --pr-type [branch|draft|ready]
|
python -m openhands.resolver.send_pull_request --issue-number ISSUE_NUMBER --username YOUR_GITHUB_OR_GITLAB_USERNAME --pr-type draft
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Instructions
|
If you want to upload to a fork, you can do so by specifying the `fork-owner`:
|
||||||
|
|
||||||
Add repository-specific instructions by creating a file at `.openhands/microagents/repo.md` in your repository. For more information about repository microagents, see [Repository Instructions](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents#2-repository-instructions-private).
|
```bash
|
||||||
|
python -m openhands.resolver.send_pull_request --issue-number ISSUE_NUMBER --username YOUR_GITHUB_OR_GITLAB_USERNAME --pr-type draft --fork-owner YOUR_GITHUB_OR_GITLAB_USERNAME
|
||||||
|
```
|
||||||
|
|
||||||
|
## Providing Custom Instructions
|
||||||
|
|
||||||
|
You can customize how the AI agent approaches issue resolution by adding a repository microagent file at `.openhands/microagents/repo.md` in your repository. This file's contents will be automatically loaded in the prompt when working with your repository. For more information about repository microagents, see [Repository Instructions](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents#2-repository-instructions-private).
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If you have any issues, please open an issue on this github repo, we're happy to help!
|
If you have any issues, please open an issue on this github or gitlab repo, we're happy to help!
|
||||||
Alternatively, you can [email us](mailto:contact@all-hands.dev) or join the OpenHands Slack workspace (see [the README](/README.md) for an invite link).
|
Alternatively, you can [email us](mailto:contact@all-hands.dev) or join the OpenHands Slack workspace (see [the README](/README.md) for an invite link).
|
||||||
|
|||||||
@@ -98,14 +98,21 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
return self.runtime_url
|
return self.runtime_url
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
self.log('debug', f'Connecting to remote runtime with session ID: {self.sid}')
|
||||||
try:
|
try:
|
||||||
|
self.log('debug', 'Starting or attaching to runtime...')
|
||||||
await call_sync_from_async(self._start_or_attach_to_runtime)
|
await call_sync_from_async(self._start_or_attach_to_runtime)
|
||||||
except Exception:
|
self.log('debug', 'Runtime started/attached successfully')
|
||||||
|
except Exception as e:
|
||||||
|
self.log('error', f'Runtime failed to start: {str(e)}')
|
||||||
self.close()
|
self.close()
|
||||||
self.log('error', 'Runtime failed to start')
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
self.log('debug', 'Setting up initial environment...')
|
||||||
await call_sync_from_async(self.setup_initial_env)
|
await call_sync_from_async(self.setup_initial_env)
|
||||||
|
self.log('debug', 'Initial environment setup complete')
|
||||||
self._runtime_initialized = True
|
self._runtime_initialized = True
|
||||||
|
self.log('info', f'Remote runtime connection established. Runtime ID: {self.runtime_id}, URL: {self.runtime_url}')
|
||||||
|
|
||||||
def _start_or_attach_to_runtime(self):
|
def _start_or_attach_to_runtime(self):
|
||||||
existing_runtime = self._check_existing_runtime()
|
existing_runtime = self._check_existing_runtime()
|
||||||
@@ -145,17 +152,21 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
self.send_status_message(' ')
|
self.send_status_message(' ')
|
||||||
|
|
||||||
def _check_existing_runtime(self) -> bool:
|
def _check_existing_runtime(self) -> bool:
|
||||||
|
self.log('debug', f'Checking for existing runtime with session ID: {self.sid}')
|
||||||
try:
|
try:
|
||||||
response = self._send_runtime_api_request(
|
response = self._send_runtime_api_request(
|
||||||
'GET',
|
'GET',
|
||||||
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
|
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
self.log('debug', f'Existing runtime check response: {data}')
|
||||||
status = data.get('status')
|
status = data.get('status')
|
||||||
if status == 'running' or status == 'paused':
|
if status == 'running' or status == 'paused':
|
||||||
|
self.log('debug', f'Found existing runtime with status: {status}')
|
||||||
self._parse_runtime_response(response)
|
self._parse_runtime_response(response)
|
||||||
except httpx.HTTPError as e:
|
except httpx.HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
|
self.log('debug', f'No existing runtime found for session ID: {self.sid}')
|
||||||
return False
|
return False
|
||||||
self.log('debug', f'Error while looking for remote runtime: {e}')
|
self.log('debug', f'Error while looking for remote runtime: {e}')
|
||||||
raise
|
raise
|
||||||
@@ -167,6 +178,7 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
if status == 'running':
|
if status == 'running':
|
||||||
|
self.log('info', f'Using existing running runtime for session ID: {self.sid}')
|
||||||
return True
|
return True
|
||||||
elif status == 'stopped':
|
elif status == 'stopped':
|
||||||
self.log('debug', 'Found existing remote runtime, but it is stopped')
|
self.log('debug', 'Found existing remote runtime, but it is stopped')
|
||||||
@@ -224,16 +236,20 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _start_runtime(self):
|
def _start_runtime(self):
|
||||||
|
self.log('debug', 'Starting remote runtime...')
|
||||||
# Prepare the request body for the /start endpoint
|
# Prepare the request body for the /start endpoint
|
||||||
command = get_action_execution_server_startup_command(
|
command = get_action_execution_server_startup_command(
|
||||||
server_port=self.port,
|
server_port=self.port,
|
||||||
plugins=self.plugins,
|
plugins=self.plugins,
|
||||||
app_config=self.config,
|
app_config=self.config,
|
||||||
)
|
)
|
||||||
|
self.log('debug', f'Action execution server command: {command}')
|
||||||
|
|
||||||
environment = {}
|
environment = {}
|
||||||
if self.config.debug or os.environ.get('DEBUG', 'false').lower() == 'true':
|
if self.config.debug or os.environ.get('DEBUG', 'false').lower() == 'true':
|
||||||
environment['DEBUG'] = 'true'
|
environment['DEBUG'] = 'true'
|
||||||
environment.update(self.config.sandbox.runtime_startup_env_vars)
|
environment.update(self.config.sandbox.runtime_startup_env_vars)
|
||||||
|
|
||||||
start_request = {
|
start_request = {
|
||||||
'image': self.container_image,
|
'image': self.container_image,
|
||||||
'command': command,
|
'command': command,
|
||||||
@@ -242,10 +258,16 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
'session_id': self.sid,
|
'session_id': self.sid,
|
||||||
'resource_factor': self.config.sandbox.remote_runtime_resource_factor,
|
'resource_factor': self.config.sandbox.remote_runtime_resource_factor,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.sandbox.remote_runtime_class == 'sysbox':
|
if self.config.sandbox.remote_runtime_class == 'sysbox':
|
||||||
start_request['runtime_class'] = 'sysbox-runc'
|
start_request['runtime_class'] = 'sysbox-runc'
|
||||||
|
self.log('debug', 'Using sysbox runtime class')
|
||||||
|
else:
|
||||||
|
self.log('debug', 'Using default (gvisor) runtime class')
|
||||||
# We ignore other runtime classes for now, because both None and 'gvisor' map to 'gvisor'
|
# We ignore other runtime classes for now, because both None and 'gvisor' map to 'gvisor'
|
||||||
|
|
||||||
|
self.log('debug', f'Sending start request with parameters: {start_request}')
|
||||||
|
|
||||||
# Start the sandbox using the /start endpoint
|
# Start the sandbox using the /start endpoint
|
||||||
try:
|
try:
|
||||||
response = self._send_runtime_api_request(
|
response = self._send_runtime_api_request(
|
||||||
@@ -255,8 +277,8 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
)
|
)
|
||||||
self._parse_runtime_response(response)
|
self._parse_runtime_response(response)
|
||||||
self.log(
|
self.log(
|
||||||
'debug',
|
'info',
|
||||||
f'Runtime started. URL: {self.runtime_url}',
|
f'Runtime started successfully. Runtime ID: {self.runtime_id}, URL: {self.runtime_url}',
|
||||||
)
|
)
|
||||||
except httpx.HTTPError as e:
|
except httpx.HTTPError as e:
|
||||||
self.log('error', f'Unable to start runtime: {str(e)}')
|
self.log('error', f'Unable to start runtime: {str(e)}')
|
||||||
@@ -269,15 +291,27 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
3. Poll for the runtime to be ready
|
3. Poll for the runtime to be ready
|
||||||
4. Update env vars
|
4. Update env vars
|
||||||
"""
|
"""
|
||||||
|
self.log('debug', f'Resuming paused runtime with ID: {self.runtime_id}')
|
||||||
self.send_status_message('STATUS$STARTING_RUNTIME')
|
self.send_status_message('STATUS$STARTING_RUNTIME')
|
||||||
self._send_runtime_api_request(
|
|
||||||
'POST',
|
try:
|
||||||
f'{self.config.sandbox.remote_runtime_api_url}/resume',
|
self.log('debug', 'Sending resume request to runtime API')
|
||||||
json={'runtime_id': self.runtime_id},
|
self._send_runtime_api_request(
|
||||||
)
|
'POST',
|
||||||
self._wait_until_alive()
|
f'{self.config.sandbox.remote_runtime_api_url}/resume',
|
||||||
self.setup_initial_env()
|
json={'runtime_id': self.runtime_id},
|
||||||
self.log('debug', 'Runtime resumed.')
|
)
|
||||||
|
|
||||||
|
self.log('debug', 'Waiting for resumed runtime to become alive')
|
||||||
|
self._wait_until_alive()
|
||||||
|
|
||||||
|
self.log('debug', 'Setting up initial environment for resumed runtime')
|
||||||
|
self.setup_initial_env()
|
||||||
|
|
||||||
|
self.log('info', f'Runtime with ID {self.runtime_id} resumed successfully')
|
||||||
|
except Exception as e:
|
||||||
|
self.log('error', f'Failed to resume runtime: {str(e)}')
|
||||||
|
raise
|
||||||
|
|
||||||
def _parse_runtime_response(self, response: httpx.Response):
|
def _parse_runtime_response(self, response: httpx.Response):
|
||||||
start_response = response.json()
|
start_response = response.json()
|
||||||
@@ -324,53 +358,62 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
return retry_decorator(self._wait_until_alive_impl)()
|
return retry_decorator(self._wait_until_alive_impl)()
|
||||||
|
|
||||||
def _wait_until_alive_impl(self):
|
def _wait_until_alive_impl(self):
|
||||||
self.log('debug', f'Waiting for runtime to be alive at url: {self.runtime_url}')
|
self.log('debug', f'Checking if runtime is alive at url: {self.runtime_url}')
|
||||||
runtime_info_response = self._send_runtime_api_request(
|
runtime_info_response = self._send_runtime_api_request(
|
||||||
'GET',
|
'GET',
|
||||||
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
|
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
|
||||||
)
|
)
|
||||||
runtime_data = runtime_info_response.json()
|
runtime_data = runtime_info_response.json()
|
||||||
assert 'runtime_id' in runtime_data
|
self.log('debug', f'Runtime info response: {runtime_data}')
|
||||||
assert runtime_data['runtime_id'] == self.runtime_id
|
|
||||||
assert 'pod_status' in runtime_data
|
assert 'runtime_id' in runtime_data, "Missing runtime_id in response"
|
||||||
|
assert runtime_data['runtime_id'] == self.runtime_id, f"Runtime ID mismatch: {runtime_data['runtime_id']} != {self.runtime_id}"
|
||||||
|
assert 'pod_status' in runtime_data, "Missing pod_status in response"
|
||||||
|
|
||||||
pod_status = runtime_data['pod_status'].lower()
|
pod_status = runtime_data['pod_status'].lower()
|
||||||
self.log('debug', f'Pod status: {pod_status}')
|
self.log('debug', f'Pod status: {pod_status}')
|
||||||
|
|
||||||
restart_count = runtime_data.get('restart_count', 0)
|
restart_count = runtime_data.get('restart_count', 0)
|
||||||
if restart_count != 0:
|
if restart_count != 0:
|
||||||
restart_reasons = runtime_data.get('restart_reasons')
|
restart_reasons = runtime_data.get('restart_reasons')
|
||||||
self.log(
|
self.log(
|
||||||
'debug', f'Pod restarts: {restart_count}, reasons: {restart_reasons}'
|
'warning', f'Pod has restarted {restart_count} times, reasons: {restart_reasons}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# FIXME: We should fix it at the backend of /start endpoint, make sure
|
# FIXME: We should fix it at the backend of /start endpoint, make sure
|
||||||
# the pod is created before returning the response.
|
# the pod is created before returning the response.
|
||||||
# Retry a period of time to give the cluster time to start the pod
|
# Retry a period of time to give the cluster time to start the pod
|
||||||
if pod_status == 'ready':
|
if pod_status == 'ready':
|
||||||
|
self.log('debug', 'Pod status is ready, checking if action execution server is alive')
|
||||||
try:
|
try:
|
||||||
self.check_if_alive()
|
self.check_if_alive()
|
||||||
|
self.log('info', 'Runtime is fully alive and ready to accept commands')
|
||||||
|
return
|
||||||
except httpx.HTTPError as e:
|
except httpx.HTTPError as e:
|
||||||
self.log(
|
self.log(
|
||||||
'warning',
|
'warning',
|
||||||
f"Runtime /alive failed, but pod says it's ready: {str(e)}",
|
f"Runtime /alive check failed, but pod says it's ready: {str(e)}",
|
||||||
)
|
)
|
||||||
raise AgentRuntimeNotReadyError(
|
raise AgentRuntimeNotReadyError(
|
||||||
f'Runtime /alive failed to respond with 200: {str(e)}'
|
f'Runtime /alive failed to respond with 200: {str(e)}'
|
||||||
)
|
)
|
||||||
return
|
|
||||||
elif (
|
elif (
|
||||||
pod_status == 'not found'
|
pod_status == 'not found'
|
||||||
or pod_status == 'pending'
|
or pod_status == 'pending'
|
||||||
or pod_status == 'running'
|
or pod_status == 'running'
|
||||||
): # nb: Running is not yet Ready
|
): # nb: Running is not yet Ready
|
||||||
|
self.log('debug', f'Pod is in transitional state: {pod_status}')
|
||||||
raise AgentRuntimeNotReadyError(
|
raise AgentRuntimeNotReadyError(
|
||||||
f'Runtime (ID={self.runtime_id}) is not yet ready. Status: {pod_status}'
|
f'Runtime (ID={self.runtime_id}) is not yet ready. Status: {pod_status}'
|
||||||
)
|
)
|
||||||
elif pod_status in ('failed', 'unknown', 'crashloopbackoff'):
|
elif pod_status in ('failed', 'unknown', 'crashloopbackoff'):
|
||||||
if pod_status == 'crashloopbackoff':
|
if pod_status == 'crashloopbackoff':
|
||||||
|
self.log('error', f'Pod is in CrashLoopBackOff state. Restart count: {restart_count}')
|
||||||
raise AgentRuntimeUnavailableError(
|
raise AgentRuntimeUnavailableError(
|
||||||
'Runtime crashed and is being restarted, potentially due to memory usage. Please try again.'
|
'Runtime crashed and is being restarted, potentially due to memory usage. Please try again.'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
self.log('error', f'Pod is in failed state: {pod_status}')
|
||||||
raise AgentRuntimeUnavailableError(
|
raise AgentRuntimeUnavailableError(
|
||||||
f'Runtime is unavailable (status: {pod_status}). Please try again.'
|
f'Runtime is unavailable (status: {pod_status}). Please try again.'
|
||||||
)
|
)
|
||||||
@@ -385,11 +428,18 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
raise AgentRuntimeNotReadyError()
|
raise AgentRuntimeNotReadyError()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
self.log('debug', f'Closing remote runtime with ID: {self.runtime_id}')
|
||||||
|
|
||||||
if self.attach_to_existing:
|
if self.attach_to_existing:
|
||||||
|
self.log('debug', 'Runtime was attached to existing instance, not stopping it')
|
||||||
super().close()
|
super().close()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.config.sandbox.keep_runtime_alive:
|
if self.config.sandbox.keep_runtime_alive:
|
||||||
|
self.log('debug', 'keep_runtime_alive is set, not stopping the runtime')
|
||||||
|
|
||||||
if self.config.sandbox.pause_closed_runtimes:
|
if self.config.sandbox.pause_closed_runtimes:
|
||||||
|
self.log('debug', 'pause_closed_runtimes is set, pausing the runtime')
|
||||||
try:
|
try:
|
||||||
if not self._runtime_closed:
|
if not self._runtime_closed:
|
||||||
self._send_runtime_api_request(
|
self._send_runtime_api_request(
|
||||||
@@ -397,12 +447,14 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
f'{self.config.sandbox.remote_runtime_api_url}/pause',
|
f'{self.config.sandbox.remote_runtime_api_url}/pause',
|
||||||
json={'runtime_id': self.runtime_id},
|
json={'runtime_id': self.runtime_id},
|
||||||
)
|
)
|
||||||
self.log('debug', 'Runtime paused.')
|
self.log('info', f'Runtime with ID {self.runtime_id} paused successfully')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log('error', f'Unable to pause runtime: {str(e)}')
|
self.log('error', f'Unable to pause runtime: {str(e)}')
|
||||||
raise e
|
raise e
|
||||||
super().close()
|
super().close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.log('debug', 'Stopping the runtime')
|
||||||
try:
|
try:
|
||||||
if not self._runtime_closed:
|
if not self._runtime_closed:
|
||||||
self._send_runtime_api_request(
|
self._send_runtime_api_request(
|
||||||
@@ -410,7 +462,7 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
f'{self.config.sandbox.remote_runtime_api_url}/stop',
|
f'{self.config.sandbox.remote_runtime_api_url}/stop',
|
||||||
json={'runtime_id': self.runtime_id},
|
json={'runtime_id': self.runtime_id},
|
||||||
)
|
)
|
||||||
self.log('debug', 'Runtime stopped.')
|
self.log('info', f'Runtime with ID {self.runtime_id} stopped successfully')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log('error', f'Unable to stop runtime: {str(e)}')
|
self.log('error', f'Unable to stop runtime: {str(e)}')
|
||||||
raise e
|
raise e
|
||||||
@@ -418,15 +470,21 @@ class RemoteRuntime(ActionExecutionClient):
|
|||||||
super().close()
|
super().close()
|
||||||
|
|
||||||
def _send_runtime_api_request(self, method, url, **kwargs):
|
def _send_runtime_api_request(self, method, url, **kwargs):
|
||||||
|
self.log('debug', f'Sending {method} request to {url} with kwargs: {kwargs}')
|
||||||
try:
|
try:
|
||||||
kwargs['timeout'] = self.config.sandbox.remote_runtime_api_timeout
|
kwargs['timeout'] = self.config.sandbox.remote_runtime_api_timeout
|
||||||
return send_request(self.session, method, url, **kwargs)
|
response = send_request(self.session, method, url, **kwargs)
|
||||||
|
self.log('debug', f'Received response from {url}: status={response.status_code}')
|
||||||
|
return response
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
self.log(
|
self.log(
|
||||||
'error',
|
'error',
|
||||||
f'No response received within the timeout period for url: {url}',
|
f'No response received within the timeout period for url: {url}',
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
except httpx.HTTPError as e:
|
||||||
|
self.log('error', f'HTTP error when calling {url}: {str(e)}')
|
||||||
|
raise
|
||||||
|
|
||||||
def _send_action_server_request(self, method, url, **kwargs):
|
def _send_action_server_request(self, method, url, **kwargs):
|
||||||
if not self.config.sandbox.remote_runtime_enable_retries:
|
if not self.config.sandbox.remote_runtime_enable_retries:
|
||||||
|
|||||||
@@ -45,7 +45,14 @@ def create_provider_tokens_object(
|
|||||||
async def connect(connection_id: str, environ):
|
async def connect(connection_id: str, environ):
|
||||||
logger.info(f'sio:connect: {connection_id}')
|
logger.info(f'sio:connect: {connection_id}')
|
||||||
query_params = parse_qs(environ.get('QUERY_STRING', ''))
|
query_params = parse_qs(environ.get('QUERY_STRING', ''))
|
||||||
latest_event_id = int(query_params.get('latest_event_id', [-1])[0])
|
latest_event_id_str = query_params.get('latest_event_id', [-1])[0]
|
||||||
|
try:
|
||||||
|
latest_event_id = int(latest_event_id_str)
|
||||||
|
except ValueError:
|
||||||
|
logger.debug(
|
||||||
|
f'Invalid latest_event_id value: {latest_event_id_str}, defaulting to -1'
|
||||||
|
)
|
||||||
|
latest_event_id = -1
|
||||||
conversation_id = query_params.get('conversation_id', [None])[0]
|
conversation_id = query_params.get('conversation_id', [None])[0]
|
||||||
raw_list = query_params.get('providers_set', [])
|
raw_list = query_params.get('providers_set', [])
|
||||||
providers_list = []
|
providers_list = []
|
||||||
|
|||||||
Generated
+29
-29
@@ -207,14 +207,14 @@ vertex = ["google-auth[requests] (>=2,<3)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.8.0"
|
version = "4.9.0"
|
||||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main", "evaluation", "runtime", "test"]
|
groups = ["main", "evaluation", "runtime", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"},
|
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
|
||||||
{file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"},
|
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -223,8 +223,8 @@ sniffio = ">=1.1"
|
|||||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
||||||
trio = ["trio (>=0.26.1)"]
|
trio = ["trio (>=0.26.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -496,18 +496,18 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.38.3"
|
version = "1.38.4"
|
||||||
description = "The AWS SDK for Python"
|
description = "The AWS SDK for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "boto3-1.38.3-py3-none-any.whl", hash = "sha256:9218f86e2164e1bddb75d435bbde4fa651aa58687213d7e3e1b50f7eb8868f66"},
|
{file = "boto3-1.38.4-py3-none-any.whl", hash = "sha256:ab315d38409f5b3262b653a10b0fac786bcff7e51e03dcb99ff38ba16bf85630"},
|
||||||
{file = "boto3-1.38.3.tar.gz", hash = "sha256:655d51abcd68a40a33c52dbaa2ca73fc63c746b894e2ae22ed8ddc1912ddd93f"},
|
{file = "boto3-1.38.4.tar.gz", hash = "sha256:4990df0087fe7be944ba06c2d1e6512b5a24f821af5a4881f24309e13ae29e68"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
botocore = ">=1.38.3,<1.39.0"
|
botocore = ">=1.38.4,<1.39.0"
|
||||||
jmespath = ">=0.7.1,<2.0.0"
|
jmespath = ">=0.7.1,<2.0.0"
|
||||||
s3transfer = ">=0.12.0,<0.13.0"
|
s3transfer = ">=0.12.0,<0.13.0"
|
||||||
|
|
||||||
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3-stubs"
|
name = "boto3-stubs"
|
||||||
version = "1.38.3"
|
version = "1.38.4"
|
||||||
description = "Type annotations for boto3 1.38.3 generated with mypy-boto3-builder 8.10.1"
|
description = "Type annotations for boto3 1.38.4 generated with mypy-boto3-builder 8.10.1"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["evaluation"]
|
groups = ["evaluation"]
|
||||||
files = [
|
files = [
|
||||||
{file = "boto3_stubs-1.38.3-py3-none-any.whl", hash = "sha256:93a2c38987dd0ee19a661e8fd9a77fb4b4a30e56f63115701c307bfc55e2695c"},
|
{file = "boto3_stubs-1.38.4-py3-none-any.whl", hash = "sha256:ae931b5f40ebf70cd7ddc36065d058d406c6aaecc766bd843be855487fc6345e"},
|
||||||
{file = "boto3_stubs-1.38.3.tar.gz", hash = "sha256:e406626de8daf537984678355ad0e32d838865c4ea3d223268964d4e6fb44534"},
|
{file = "boto3_stubs-1.38.4.tar.gz", hash = "sha256:eff7f741a9b9df5b0af1e7018878a56028f05bb3d9c57cd50cc4f60bdae7960b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -579,7 +579,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (
|
|||||||
bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"]
|
bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"]
|
||||||
billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"]
|
billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"]
|
||||||
billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"]
|
billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"]
|
||||||
boto3 = ["boto3 (==1.38.3)"]
|
boto3 = ["boto3 (==1.38.4)"]
|
||||||
braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"]
|
braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"]
|
||||||
budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"]
|
budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"]
|
||||||
ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"]
|
ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"]
|
||||||
@@ -943,14 +943,14 @@ xray = ["mypy-boto3-xray (>=1.38.0,<1.39.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.38.3"
|
version = "1.38.4"
|
||||||
description = "Low-level, data-driven core of boto 3."
|
description = "Low-level, data-driven core of boto 3."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "botocore-1.38.3-py3-none-any.whl", hash = "sha256:96f823240fe3704b99c17d1d1b2fd2d1679cf56d2a55b095f00255b76087cbf0"},
|
{file = "botocore-1.38.4-py3-none-any.whl", hash = "sha256:6206cf07be1069efaead2ddc858eb752dafef276ebbe88ac32b5c427b1d90570"},
|
||||||
{file = "botocore-1.38.3.tar.gz", hash = "sha256:790f8f966201781f5fcf486d48b4492e9f734446bbf9d19ef8159d08be854243"},
|
{file = "botocore-1.38.4.tar.gz", hash = "sha256:6143546bb56f1da4dff8d285cb6a3b8b0b6442451fe5937cb48a62bf7275386f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2663,7 +2663,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\""}
|
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
|
||||||
proto-plus = [
|
proto-plus = [
|
||||||
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
|
{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"
|
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"
|
requests = ">=2.18.0,<3.0.0.dev0"
|
||||||
@@ -2878,7 +2878,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"
|
grpc-google-iam-v1 = ">=0.14.0,<1.0.0dev"
|
||||||
proto-plus = [
|
proto-plus = [
|
||||||
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
|
{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"
|
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"
|
||||||
|
|
||||||
@@ -3794,14 +3794,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "json-repair"
|
name = "json-repair"
|
||||||
version = "0.43.0"
|
version = "0.44.0"
|
||||||
description = "A package to repair broken json strings"
|
description = "A package to repair broken json strings"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "json_repair-0.43.0-py3-none-any.whl", hash = "sha256:3f2b66819c9f5e29edd5dd4851223b72d10ed816b6423b3c92e424090c3ffc1d"},
|
{file = "json_repair-0.44.0-py3-none-any.whl", hash = "sha256:4646b59decf8763c20cd218079a12e334202d658d760ddd999f23812f07bb9cd"},
|
||||||
{file = "json_repair-0.43.0.tar.gz", hash = "sha256:77cc6eda6f407ff5fe9544f962e42b332cca1e8c9f3f9f9dc660327028e0d651"},
|
{file = "json_repair-0.44.0.tar.gz", hash = "sha256:732469762471e535de8aadcb4ec5a4b47e96c677d036cbc1c78ab396925c6a71"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4882,14 +4882,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "modal"
|
name = "modal"
|
||||||
version = "0.74.30"
|
version = "0.74.35"
|
||||||
description = "Python client library for Modal"
|
description = "Python client library for Modal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main", "evaluation"]
|
groups = ["main", "evaluation"]
|
||||||
files = [
|
files = [
|
||||||
{file = "modal-0.74.30-py3-none-any.whl", hash = "sha256:46006cb57309171fe36ee41528a7cc8c0e67c88afd9bf04a9900313c18925aa4"},
|
{file = "modal-0.74.35-py3-none-any.whl", hash = "sha256:845c3176e5fc2d0856ff1d88a5fc7699552e6542135aedae6cd52598faa55fc0"},
|
||||||
{file = "modal-0.74.30.tar.gz", hash = "sha256:14bd2ea0ebc9ab1ebce29ea76ddf12047f23599983725c5f82990ae97bea05c7"},
|
{file = "modal-0.74.35.tar.gz", hash = "sha256:bef2a40f18a40514e7502dbe543fc026b0a2542597669cd0bc0fa0db1149600a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -5633,14 +5633,14 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openhands-aci"
|
name = "openhands-aci"
|
||||||
version = "0.2.10"
|
version = "0.2.11"
|
||||||
description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands."
|
description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.12"
|
python-versions = "<4.0,>=3.12"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "openhands_aci-0.2.10-py3-none-any.whl", hash = "sha256:0703eb117e24326d80d990b81a0c71e28a364f56999095b95c146c934e40fc55"},
|
{file = "openhands_aci-0.2.11-py3-none-any.whl", hash = "sha256:bb6cdd30da8cf57292f7d1d87b2396a31af3f3abc2f5d6f02c22cb648956b41e"},
|
||||||
{file = "openhands_aci-0.2.10.tar.gz", hash = "sha256:a5e6bf46cbd9a99c5c592548419a9a3b3091c4e82f0227e8aaf470b18a261cc9"},
|
{file = "openhands_aci-0.2.11.tar.gz", hash = "sha256:38ae6abbeedfc0f6018dc5db58ad0ccc38295d649fe27218a0acab689df0e6c7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -10269,4 +10269,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "d3f933e9abf6be481ec137e14f8f7ac502afd591a9ba74b315737fd894ca5cfe"
|
content-hash = "e8326a1441d5ce74c017755566e1e0d865551712290c00202d257c931b7dc5bd"
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "openhands-ai"
|
name = "openhands-ai"
|
||||||
version = "0.34.0"
|
version = "0.35.0"
|
||||||
description = "OpenHands: Code Less, Make More"
|
description = "OpenHands: Code Less, Make More"
|
||||||
authors = ["OpenHands"]
|
authors = ["OpenHands"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -62,7 +62,7 @@ runloop-api-client = "0.32.0"
|
|||||||
libtmux = ">=0.37,<0.40"
|
libtmux = ">=0.37,<0.40"
|
||||||
pygithub = "^2.5.0"
|
pygithub = "^2.5.0"
|
||||||
joblib = "*"
|
joblib = "*"
|
||||||
openhands-aci = "^0.2.10"
|
openhands-aci = "0.2.11"
|
||||||
python-socketio = "^5.11.4"
|
python-socketio = "^5.11.4"
|
||||||
redis = "^5.2.0"
|
redis = "^5.2.0"
|
||||||
sse-starlette = "^2.1.3"
|
sse-starlette = "^2.1.3"
|
||||||
@@ -76,6 +76,7 @@ mcp = "1.6.0"
|
|||||||
python-json-logger = "^3.2.1"
|
python-json-logger = "^3.2.1"
|
||||||
playwright = "^1.51.0"
|
playwright = "^1.51.0"
|
||||||
prompt-toolkit = "^3.0.50"
|
prompt-toolkit = "^3.0.50"
|
||||||
|
anyio = "4.9.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = "0.11.7"
|
ruff = "0.11.7"
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ def test_str_replace_multi_line_with_tabs(temp_dir, runtime_cls, run_as_openhand
|
|||||||
obs.content
|
obs.content
|
||||||
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
|
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
|
||||||
1\tdef test():
|
1\tdef test():
|
||||||
2\t{'\t'.expandtabs()}print("Hello, Universe!")
|
2\t\tprint("Hello, Universe!")
|
||||||
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
Review the changes and make sure they are as expected. Edit the file again if necessary."""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from litellm import ContentPolicyViolationError, ContextWindowExceededError
|
from litellm import (
|
||||||
|
BadRequestError,
|
||||||
|
ContentPolicyViolationError,
|
||||||
|
ContextWindowExceededError,
|
||||||
|
)
|
||||||
|
|
||||||
from openhands.controller.agent import Agent
|
from openhands.controller.agent import Agent
|
||||||
from openhands.controller.agent_controller import AgentController
|
from openhands.controller.agent_controller import AgentController
|
||||||
@@ -499,7 +503,10 @@ async def test_reset_with_pending_action_no_observation(mock_agent, mock_event_s
|
|||||||
args, kwargs = mock_event_stream.add_event.call_args
|
args, kwargs = mock_event_stream.add_event.call_args
|
||||||
error_obs, source = args
|
error_obs, source = args
|
||||||
assert isinstance(error_obs, ErrorObservation)
|
assert isinstance(error_obs, ErrorObservation)
|
||||||
assert error_obs.content == 'The action has not been executed.'
|
assert (
|
||||||
|
error_obs.content
|
||||||
|
== 'The action has not been executed. This may have occurred because the user pressed the stop button, or because the runtime system crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.'
|
||||||
|
)
|
||||||
assert error_obs.tool_call_metadata == pending_action.tool_call_metadata
|
assert error_obs.tool_call_metadata == pending_action.tool_call_metadata
|
||||||
assert error_obs._cause == pending_action.id
|
assert error_obs._cause == pending_action.id
|
||||||
assert source == EventSource.AGENT
|
assert source == EventSource.AGENT
|
||||||
@@ -1486,3 +1493,71 @@ def test_system_message_in_event_stream(mock_agent, test_event_stream):
|
|||||||
assert isinstance(events[0], SystemMessageAction)
|
assert isinstance(events[0], SystemMessageAction)
|
||||||
assert events[0].content == 'Test system message'
|
assert events[0].content == 'Test system message'
|
||||||
assert events[0].tools == ['test_tool']
|
assert events[0].tools == ['test_tool']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_openrouter_context_window_exceeded_error(
|
||||||
|
mock_agent, test_event_stream, mock_status_callback
|
||||||
|
):
|
||||||
|
"""Test that OpenRouter context window exceeded errors are properly detected and handled."""
|
||||||
|
max_iterations = 5
|
||||||
|
error_after = 2
|
||||||
|
|
||||||
|
class StepState:
|
||||||
|
def __init__(self):
|
||||||
|
self.has_errored = False
|
||||||
|
self.index = 0
|
||||||
|
self.views = []
|
||||||
|
|
||||||
|
def step(self, state: State):
|
||||||
|
self.views.append(state.view)
|
||||||
|
|
||||||
|
# Wait until the right step to throw the error, and make sure we
|
||||||
|
# only throw it once.
|
||||||
|
if self.index < error_after or self.has_errored:
|
||||||
|
self.index += 1
|
||||||
|
return MessageAction(content=f'Test message {self.index}')
|
||||||
|
|
||||||
|
# Create a BadRequestError with the OpenRouter context window exceeded message pattern
|
||||||
|
error = BadRequestError(
|
||||||
|
message='litellm.BadRequestError: OpenrouterException - This endpoint\'s maximum context length is 40960 tokens. However, you requested about 42988 tokens (38892 of text input, 4096 in the output). Please reduce the length of either one, or use the "middle-out" transform to compress your prompt automatically.',
|
||||||
|
model='openrouter/qwen/qwen3-30b-a3b',
|
||||||
|
llm_provider='openrouter',
|
||||||
|
)
|
||||||
|
self.has_errored = True
|
||||||
|
raise error
|
||||||
|
|
||||||
|
step_state = StepState()
|
||||||
|
mock_agent.step = step_state.step
|
||||||
|
mock_agent.config = AgentConfig(enable_history_truncation=True)
|
||||||
|
|
||||||
|
controller = AgentController(
|
||||||
|
agent=mock_agent,
|
||||||
|
event_stream=test_event_stream,
|
||||||
|
max_iterations=max_iterations,
|
||||||
|
sid='test',
|
||||||
|
confirmation_mode=False,
|
||||||
|
headless_mode=True,
|
||||||
|
status_callback=mock_status_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the agent state to RUNNING
|
||||||
|
controller.state.agent_state = AgentState.RUNNING
|
||||||
|
|
||||||
|
# Run the controller until it hits the error
|
||||||
|
for _ in range(error_after + 2): # +2 to ensure we go past the error
|
||||||
|
await controller._step()
|
||||||
|
if step_state.has_errored:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Verify that the error was handled as a context window exceeded error
|
||||||
|
# by checking that _handle_long_context_error was called (which adds a CondensationAction)
|
||||||
|
events = list(test_event_stream.get_events())
|
||||||
|
condensation_actions = [e for e in events if isinstance(e, CondensationAction)]
|
||||||
|
|
||||||
|
# There should be at least one CondensationAction if the error was handled correctly
|
||||||
|
assert (
|
||||||
|
len(condensation_actions) > 0
|
||||||
|
), 'OpenRouter context window exceeded error was not handled correctly'
|
||||||
|
|
||||||
|
await controller.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user