mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
18 Commits
openhands-
...
openhands/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9367e3d7d1 | ||
|
|
985e20d529 | ||
|
|
98cb2e24ee | ||
|
|
de175dcc87 | ||
|
|
976019ce11 | ||
|
|
709b6ff39a | ||
|
|
767d092f8f | ||
|
|
7244e5df9f | ||
|
|
dfbb968ea0 | ||
|
|
e4c3bbbc08 | ||
|
|
6e0fbfeeda | ||
|
|
03aa5d7456 | ||
|
|
6032d2620d | ||
|
|
0fc86b4063 | ||
|
|
e39d904a1f | ||
|
|
9887813b41 | ||
|
|
d36cde5060 | ||
|
|
fc4ad2f8c3 |
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -1,12 +1,12 @@
|
||||
- [ ] This change is worth documenting at https://docs.all-hands.dev/
|
||||
- [ ] Include this change in the Release Notes. If checked, you **must** provide an **end-user friendly** description for your change below
|
||||
|
||||
**End-user friendly description of the problem this fixes or functionality that this introduces.**
|
||||
**End-user friendly description of the problem this fixes or functionality this introduces.**
|
||||
|
||||
|
||||
---
|
||||
**Give a summary of what the PR does, explaining any non-trivial design decisions.**
|
||||
**Summarize what the PR does, explaining any non-trivial design decisions.**
|
||||
|
||||
|
||||
---
|
||||
**Link of any specific issues this addresses.**
|
||||
**Link of any specific issues this addresses:**
|
||||
|
||||
@@ -118,7 +118,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.35-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.36-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -51,17 +51,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.35-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.36-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -44,7 +44,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -8,24 +8,52 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
|
||||
|
||||
## ファイルシステムへの接続
|
||||
ここでの便利な機能の1つは、ローカルファイルシステムに接続する機能です。ファイルシステムをランタイムにマウントするには:
|
||||
|
||||
### RUNTIME_MOUNT の使用(推奨)
|
||||
|
||||
ローカルファイルシステムをマウントする最も簡単な方法は、`RUNTIME_MOUNT` 環境変数を使用することです:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e RUNTIME_MOUNT=/path/to/your/code:/workspace:rw \
|
||||
# ...
|
||||
```
|
||||
|
||||
`RUNTIME_MOUNT` の形式は:`ホストパス:コンテナパス[:モード]`
|
||||
|
||||
- `ホストパス`:マウントしたいホストマシン上のパス
|
||||
- `コンテナパス`:ホストパスがマウントされるコンテナ内のパス(通常は `/workspace`)
|
||||
- `モード`:オプションのマウントモード、`rw`(読み書き可能、デフォルト)または `ro`(読み取り専用)
|
||||
|
||||
例:
|
||||
|
||||
```bash
|
||||
# Linux と Mac の例
|
||||
export RUNTIME_MOUNT=$HOME/OpenHands:/workspace:rw
|
||||
|
||||
# Windows の WSL の例
|
||||
export RUNTIME_MOUNT=/mnt/c/dev/OpenHands:/workspace:rw
|
||||
|
||||
# 読み取り専用マウントの例
|
||||
export RUNTIME_MOUNT=/path/to/reference/code:/workspace:ro
|
||||
```
|
||||
|
||||
### WORKSPACE_* 変数の使用(非推奨)
|
||||
|
||||
> **注意:** この方法は非推奨であり、将来のバージョンで削除される予定です。代わりに `RUNTIME_MOUNT` を使用してください。
|
||||
|
||||
1. `WORKSPACE_BASE` を設定します:
|
||||
|
||||
```bash
|
||||
export WORKSPACE_BASE=/path/to/your/code
|
||||
|
||||
# Linux と Mac の例
|
||||
# export WORKSPACE_BASE=$HOME/OpenHands
|
||||
# $WORKSPACE_BASE を /home/<username>/OpenHands に設定します
|
||||
#
|
||||
# Windows の WSL の例
|
||||
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
|
||||
# $WORKSPACE_BASE を C:\dev\OpenHands に設定します
|
||||
```
|
||||
|
||||
2. 以下のオプションを `docker run` コマンドに追加します:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
@@ -82,5 +110,5 @@ docker network create openhands-network
|
||||
# 分離されたネットワークで OpenHands を実行
|
||||
docker run # ... \
|
||||
--network openhands-network \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.main -t "escreva um script bash que imprima oi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@
|
||||
A maneira mais fácil de executar o OpenHands é no Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36
|
||||
```
|
||||
|
||||
Você encontrará o OpenHands em execução em http://localhost:3000!
|
||||
|
||||
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.36-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.35
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.36
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
96
docs/modules/usage/mcp.md
Normal file
96
docs/modules/usage/mcp.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Model Context Protocol (MCP)
|
||||
|
||||
:::note
|
||||
This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you to extend the agent's capabilities with custom tools.
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
Model Context Protocol (MCP) is a mechanism that allows OpenHands to communicate with external tool servers. These servers can provide additional functionality to the agent, such as specialized data processing, external API access, or custom tools. MCP is based on the open standard defined at [modelcontextprotocol.io](https://modelcontextprotocol.io).
|
||||
|
||||
## Configuration
|
||||
|
||||
MCP configuration is defined in the `[mcp]` section of your `config.toml` file.
|
||||
|
||||
### Configuration Example
|
||||
|
||||
```toml
|
||||
[mcp]
|
||||
# SSE Servers - External servers that communicate via Server-Sent Events
|
||||
sse_servers = [
|
||||
# Basic SSE server with just a URL
|
||||
"http://example.com:8080/mcp",
|
||||
|
||||
# SSE server with API key authentication
|
||||
{url="https://secure-example.com/mcp", api_key="your-api-key"}
|
||||
]
|
||||
|
||||
# Stdio Servers - Local processes that communicate via standard input/output
|
||||
stdio_servers = [
|
||||
# Basic stdio server
|
||||
{name="fetch", command="uvx", args=["mcp-server-fetch"]},
|
||||
|
||||
# Stdio server with environment variables
|
||||
{
|
||||
name="data-processor",
|
||||
command="python",
|
||||
args=["-m", "my_mcp_server"],
|
||||
env={
|
||||
"DEBUG": "true",
|
||||
"PORT": "8080"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### SSE Servers
|
||||
|
||||
SSE servers are configured using either a string URL or an object with the following properties:
|
||||
|
||||
- `url` (required)
|
||||
- Type: `str`
|
||||
- Description: The URL of the SSE server
|
||||
|
||||
- `api_key` (optional)
|
||||
- Type: `str`
|
||||
- Default: `None`
|
||||
- Description: API key for authentication with the SSE server
|
||||
|
||||
### Stdio Servers
|
||||
|
||||
Stdio servers are configured using an object with the following properties:
|
||||
|
||||
- `name` (required)
|
||||
- Type: `str`
|
||||
- Description: A unique name for the server
|
||||
|
||||
- `command` (required)
|
||||
- Type: `str`
|
||||
- Description: The command to run the server
|
||||
|
||||
- `args` (optional)
|
||||
- Type: `list of str`
|
||||
- Default: `[]`
|
||||
- Description: Command-line arguments to pass to the server
|
||||
|
||||
- `env` (optional)
|
||||
- Type: `dict of str to str`
|
||||
- Default: `{}`
|
||||
- Description: Environment variables to set for the server process
|
||||
|
||||
## How MCP Works
|
||||
|
||||
When OpenHands starts, it:
|
||||
|
||||
1. Reads the MCP configuration from `config.toml`
|
||||
2. Connects to any configured SSE servers
|
||||
3. Starts any configured stdio servers
|
||||
4. Registers the tools provided by these servers with the agent
|
||||
|
||||
The agent can then use these tools just like any built-in tool. When the agent calls an MCP tool:
|
||||
|
||||
1. OpenHands routes the call to the appropriate MCP server
|
||||
2. The server processes the request and returns a response
|
||||
3. OpenHands converts the response to an observation and presents it to the agent
|
||||
@@ -9,19 +9,47 @@ You can also [build your own runtime image](../how-to/custom-sandbox-guide).
|
||||
|
||||
## Connecting to Your filesystem
|
||||
A useful feature is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
|
||||
|
||||
### Using RUNTIME_MOUNT (Recommended)
|
||||
|
||||
The simplest way to mount your local filesystem is to use the `RUNTIME_MOUNT` environment variable:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e RUNTIME_MOUNT=/path/to/your/code:/workspace:rw \
|
||||
# ...
|
||||
```
|
||||
|
||||
The `RUNTIME_MOUNT` format is: `host_path:container_path[:mode]`
|
||||
|
||||
- `host_path`: The path on your host machine that you want to mount
|
||||
- `container_path`: The path inside the container where the host path will be mounted (typically `/workspace`)
|
||||
- `mode`: Optional mount mode, either `rw` (read-write, default) or `ro` (read-only)
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Linux and Mac Example
|
||||
export RUNTIME_MOUNT=$HOME/OpenHands:/workspace:rw
|
||||
|
||||
# WSL on Windows Example
|
||||
export RUNTIME_MOUNT=/mnt/c/dev/OpenHands:/workspace:rw
|
||||
|
||||
# Read-only mount example
|
||||
export RUNTIME_MOUNT=/path/to/reference/code:/workspace:ro
|
||||
```
|
||||
|
||||
### Using WORKSPACE_* variables (Deprecated)
|
||||
|
||||
> **Note:** This method is deprecated and will be removed in a future version. Please use `RUNTIME_MOUNT` instead.
|
||||
|
||||
1. Set `WORKSPACE_BASE`:
|
||||
|
||||
```bash
|
||||
export WORKSPACE_BASE=/path/to/your/code
|
||||
|
||||
# Linux and Mac Example
|
||||
# export WORKSPACE_BASE=$HOME/OpenHands
|
||||
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
|
||||
#
|
||||
# WSL on Windows Example
|
||||
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
|
||||
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
|
||||
```
|
||||
|
||||
2. Add the following options to the `docker run` command:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -55,4 +55,4 @@
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"packageManager": "npm@10.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
15
docs/static/openapi.json
vendored
15
docs/static/openapi.json
vendored
@@ -858,14 +858,15 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"selected_repository": {
|
||||
"type": "object",
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"properties": {
|
||||
"full_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
"description": "Full name of the repository (e.g., owner/repo)"
|
||||
},
|
||||
"git_provider": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The Git provider (e.g., github or gitlab). If omitted, all configured providers are checked for the repository."
|
||||
},
|
||||
"selected_branch": {
|
||||
"type": "string",
|
||||
|
||||
@@ -36,13 +36,12 @@ from openhands.core.config import (
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action import CmdRunAction, MessageAction, FileReadAction
|
||||
from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
|
||||
from openhands.events.observation import CmdOutputObservation, ErrorObservation
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
from openhands.utils.shutdown_listener import sleep_if_should_continue
|
||||
import pdb
|
||||
|
||||
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
|
||||
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'true').lower() == 'true'
|
||||
@@ -51,7 +50,7 @@ RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'tru
|
||||
# TODO: migrate all swe-bench docker to ghcr.io/openhands
|
||||
# TODO: 适应所有的语言
|
||||
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', '')
|
||||
LANGUAGE =os.environ.get('LANGUAGE', 'python')
|
||||
LANGUAGE = os.environ.get('LANGUAGE', 'python')
|
||||
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
|
||||
|
||||
|
||||
@@ -71,7 +70,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
# Instruction based on Anthropic's official trajectory
|
||||
# https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs
|
||||
instructions = {
|
||||
"python":(
|
||||
'python': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -96,7 +95,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"java": (
|
||||
'java': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -121,7 +120,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
" Make sure all these tests pass with your changes.\n"
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"go": (
|
||||
'go': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -146,7 +145,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"c": (
|
||||
'c': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -171,7 +170,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"cpp": (
|
||||
'cpp': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -196,7 +195,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"javascript": (
|
||||
'javascript': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -221,7 +220,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"typescript":(
|
||||
'typescript': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -246,7 +245,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
),
|
||||
"rust":(
|
||||
'rust': (
|
||||
'<uploaded_files>\n'
|
||||
f'/workspace/{workspace_dir_name}\n'
|
||||
'</uploaded_files>\n'
|
||||
@@ -270,11 +269,10 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
' - The functions you changed\n'
|
||||
' Make sure all these tests pass with your changes.\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
)
|
||||
),
|
||||
}
|
||||
instruction = instructions.get(LANGUAGE.lower())
|
||||
|
||||
|
||||
if instruction and RUN_WITH_BROWSING:
|
||||
instruction += (
|
||||
'<IMPORTANT!>\n'
|
||||
@@ -284,7 +282,6 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
return instruction
|
||||
|
||||
|
||||
|
||||
# TODO: 适应所有的语言
|
||||
# def get_instance_docker_image(instance_id: str) -> str:
|
||||
# image_name = 'sweb.eval.x86_64.' + instance_id
|
||||
@@ -307,16 +304,15 @@ def get_instance_docker_image(instance: pd.Series):
|
||||
container_name = container_name.replace('/', '_m_')
|
||||
instance_id = instance.get('instance_id', '')
|
||||
tag_suffix = instance_id.split('-')[-1] if instance_id else ''
|
||||
container_tag = f"pr-{tag_suffix}"
|
||||
container_tag = f'pr-{tag_suffix}'
|
||||
# pdb.set_trace()
|
||||
return f"mswebench/{container_name}:{container_tag}"
|
||||
return f'mswebench/{container_name}:{container_tag}'
|
||||
# return "kong/insomnia:pr-8284"
|
||||
# return "'sweb.eval.x86_64.local_insomnia"
|
||||
# return "local_insomnia_why"
|
||||
# return "local/kong-insomnia:pr-8117"
|
||||
|
||||
|
||||
|
||||
def get_config(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
@@ -569,7 +565,6 @@ def complete_runtime(
|
||||
f'Failed to git config --global core.pager "": {str(obs)}',
|
||||
)
|
||||
|
||||
|
||||
action = CmdRunAction(command='git add -A')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
@@ -582,14 +577,14 @@ def complete_runtime(
|
||||
|
||||
##删除二进制文件
|
||||
action = CmdRunAction(
|
||||
command=f'''
|
||||
command="""
|
||||
for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do
|
||||
if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then
|
||||
git rm -f "$file" 2>/dev/null || rm -f "$file"
|
||||
echo "Removed: $file"
|
||||
fi
|
||||
done
|
||||
'''
|
||||
"""
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
@@ -626,14 +621,12 @@ def complete_runtime(
|
||||
else:
|
||||
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
|
||||
|
||||
action = FileReadAction(
|
||||
path='patch.diff'
|
||||
)
|
||||
action = FileReadAction(path='patch.diff')
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
git_patch = obs.content
|
||||
# pdb.set_trace()
|
||||
# pdb.set_trace()
|
||||
|
||||
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
|
||||
|
||||
@@ -714,12 +707,12 @@ def process_instance(
|
||||
is_binary_block = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("diff --git "):
|
||||
if line.startswith('diff --git '):
|
||||
if block and not is_binary_block:
|
||||
cleaned_lines.extend(block)
|
||||
block = [line]
|
||||
is_binary_block = False
|
||||
elif "Binary files" in line:
|
||||
elif 'Binary files' in line:
|
||||
is_binary_block = True
|
||||
block.append(line)
|
||||
else:
|
||||
@@ -727,7 +720,8 @@ def process_instance(
|
||||
|
||||
if block and not is_binary_block:
|
||||
cleaned_lines.extend(block)
|
||||
return "\n".join(cleaned_lines)
|
||||
return '\n'.join(cleaned_lines)
|
||||
|
||||
git_patch = remove_binary_diffs(git_patch)
|
||||
test_result = {
|
||||
'git_patch': git_patch,
|
||||
@@ -797,7 +791,7 @@ if __name__ == '__main__':
|
||||
# so we don't need to manage file uploading to OpenHands's repo
|
||||
# dataset = load_dataset(args.dataset, split=args.split)
|
||||
# dataset = load_dataset(args.dataset)
|
||||
dataset = load_dataset("json", data_files = args.dataset)
|
||||
dataset = load_dataset('json', data_files=args.dataset)
|
||||
dataset = dataset[args.split]
|
||||
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
logger.info(
|
||||
|
||||
@@ -3,7 +3,9 @@ import json
|
||||
input_file = 'XXX.jsonl'
|
||||
output_file = 'YYY.jsonl'
|
||||
|
||||
with open(input_file, 'r', encoding='utf-8') as fin, open(output_file, 'w', encoding='utf-8') as fout:
|
||||
with open(input_file, 'r', encoding='utf-8') as fin, open(
|
||||
output_file, 'w', encoding='utf-8'
|
||||
) as fout:
|
||||
for line in fin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
@@ -13,18 +15,22 @@ with open(input_file, 'r', encoding='utf-8') as fin, open(output_file, 'w', enco
|
||||
item = data
|
||||
|
||||
# 提取原始数据
|
||||
org = item.get("org", "")
|
||||
repo = item.get("repo", "")
|
||||
number = str(item.get("number", ""))
|
||||
org = item.get('org', '')
|
||||
repo = item.get('repo', '')
|
||||
number = str(item.get('number', ''))
|
||||
|
||||
new_item = {}
|
||||
new_item["repo"] = f"{org}/{repo}"
|
||||
new_item["instance_id"] = f"{org}__{repo}-{number}"
|
||||
new_item["problem_statement"] = item["resolved_issues"][0].get("title", "") + "\n" + item["resolved_issues"][0].get("body", "")
|
||||
new_item["FAIL_TO_PASS"] = []
|
||||
new_item["PASS_TO_PASS"] = []
|
||||
new_item["base_commit"] = item['base'].get("sha","")
|
||||
new_item["version"] = "0.1" # depends
|
||||
new_item['repo'] = f'{org}/{repo}'
|
||||
new_item['instance_id'] = f'{org}__{repo}-{number}'
|
||||
new_item['problem_statement'] = (
|
||||
item['resolved_issues'][0].get('title', '')
|
||||
+ '\n'
|
||||
+ item['resolved_issues'][0].get('body', '')
|
||||
)
|
||||
new_item['FAIL_TO_PASS'] = []
|
||||
new_item['PASS_TO_PASS'] = []
|
||||
new_item['base_commit'] = item['base'].get('sha', '')
|
||||
new_item['version'] = '0.1' # depends
|
||||
|
||||
output_data = new_item
|
||||
fout.write(json.dumps(output_data, ensure_ascii=False) + "\n")
|
||||
fout.write(json.dumps(output_data, ensure_ascii=False) + '\n')
|
||||
|
||||
@@ -15,7 +15,7 @@ def main():
|
||||
'org': groups.group(1),
|
||||
'repo': groups.group(2),
|
||||
'number': groups.group(3),
|
||||
'fix_patch': data['test_result']['git_patch']
|
||||
'fix_patch': data['test_result']['git_patch'],
|
||||
}
|
||||
fout.write(json.dumps(patch) + '\n')
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("AuthModal", () => {
|
||||
|
||||
it("should render the GitHub and GitLab buttons", () => {
|
||||
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
|
||||
|
||||
|
||||
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ describe("HomeHeader", () => {
|
||||
"gui",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { createRoutesStub, Outlet } from "react-router";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import * as GitService from "#/api/git";
|
||||
import { RepoConnector } from "#/components/features/home/repo-connector";
|
||||
|
||||
const renderRepoConnector = (initialProvidersAreSet = true) => {
|
||||
@@ -74,13 +73,10 @@ describe("RepoConnector", () => {
|
||||
|
||||
it("should render the available repositories in the dropdown", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -96,13 +92,10 @@ describe("RepoConnector", () => {
|
||||
|
||||
it("should only enable the launch button if a repo is selected", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -145,13 +138,10 @@ describe("RepoConnector", () => {
|
||||
it("should create a conversation and redirect with the selected repo when pressing the launch button", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -175,12 +165,8 @@ describe("RepoConnector", () => {
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
"gui",
|
||||
{
|
||||
full_name: "rbren/polaris",
|
||||
git_provider: "github",
|
||||
id: 1,
|
||||
is_public: true,
|
||||
},
|
||||
"rbren/polaris",
|
||||
"github",
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
@@ -190,13 +176,10 @@ describe("RepoConnector", () => {
|
||||
|
||||
it("should change the launch button text to 'Loading...' when creating a conversation", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { TaskCard } from "#/components/features/home/tasks/task-card";
|
||||
import * as GitService from "#/api/git";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
const MOCK_TASK_1: SuggestedTask = {
|
||||
@@ -20,30 +19,6 @@ const MOCK_TASK_1: SuggestedTask = {
|
||||
git_provider: "github",
|
||||
};
|
||||
|
||||
const MOCK_TASK_2: SuggestedTask = {
|
||||
issue_number: 456,
|
||||
repo: "repo2",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
};
|
||||
|
||||
const MOCK_TASK_3: SuggestedTask = {
|
||||
issue_number: 789,
|
||||
repo: "repo3",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "gitlab",
|
||||
};
|
||||
|
||||
const MOCK_TASK_4: SuggestedTask = {
|
||||
issue_number: 101112,
|
||||
repo: "repo4",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "gitlab",
|
||||
};
|
||||
|
||||
const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
{ id: 1, full_name: "repo1", git_provider: "github", is_public: true },
|
||||
{ id: 2, full_name: "repo2", git_provider: "github", is_public: true },
|
||||
@@ -98,13 +73,10 @@ describe("TaskCard", () => {
|
||||
describe("creating suggested task conversation", () => {
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
});
|
||||
|
||||
it("should call create conversation with suggest task trigger and selected suggested task", async () => {
|
||||
@@ -117,7 +89,8 @@ describe("TaskCard", () => {
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
"suggested_task",
|
||||
MOCK_RESPOSITORIES[0],
|
||||
MOCK_RESPOSITORIES[0].full_name,
|
||||
MOCK_RESPOSITORIES[0].git_provider,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
|
||||
@@ -43,7 +43,7 @@ const createWrapper = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
@@ -61,7 +61,7 @@ describe("AcceptTOS", () => {
|
||||
|
||||
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" });
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("AcceptTOS", () => {
|
||||
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" });
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("AcceptTOS", () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<AcceptTOS />, { wrapper: createWrapper() });
|
||||
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("AcceptTOS", () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<AcceptTOS />, { wrapper: createWrapper() });
|
||||
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
@@ -133,4 +133,4 @@ describe("AcceptTOS", () => {
|
||||
|
||||
expect(window.location.href).toBe(externalUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,8 +245,8 @@ describe("Form submission", () => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider_tokens: {
|
||||
github: "test-token",
|
||||
gitlab: "",
|
||||
github: { token: "test-token" },
|
||||
gitlab: { token: "" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -258,8 +258,8 @@ describe("Form submission", () => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider_tokens: {
|
||||
github: "",
|
||||
gitlab: "test-token",
|
||||
github: { token: "" },
|
||||
gitlab: { token: "test-token" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { setupStore } from "test-utils";
|
||||
import { AxiosError } from "axios";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import * as GitService from "#/api/git";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
@@ -102,13 +101,10 @@ describe("HomeScreen", () => {
|
||||
|
||||
it("should filter the suggested tasks based on the selected repository", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
@@ -140,13 +136,10 @@ describe("HomeScreen", () => {
|
||||
|
||||
it("should reset the filtered tasks when the selected repository is cleared", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
@@ -218,13 +211,10 @@ describe("HomeScreen", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
OpenHands,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
|
||||
});
|
||||
|
||||
it("should disable the other launch buttons when the header launch button is clicked", async () => {
|
||||
|
||||
462
frontend/package-lock.json
generated
462
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.35.0",
|
||||
"version": "0.36.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.35.0",
|
||||
"version": "0.36.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.8",
|
||||
"@microlink/react-json-view": "^1.26.1",
|
||||
@@ -17,22 +17,22 @@
|
||||
"@reduxjs/toolkit": "^2.7.0",
|
||||
"@stripe/react-stripe-js": "^3.6.0",
|
||||
"@stripe/stripe-js": "^7.2.0",
|
||||
"@tanstack/react-query": "^5.74.9",
|
||||
"@tanstack/react-query": "^5.75.1",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.9.2",
|
||||
"framer-motion": "^12.9.4",
|
||||
"i18next": "^25.0.2",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.27",
|
||||
"jose": "^6.0.10",
|
||||
"lucide-react": "^0.503.0",
|
||||
"lucide-react": "^0.506.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.237.0",
|
||||
"posthog-js": "^1.239.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -48,13 +48,13 @@
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"vite": "^6.3.3",
|
||||
"vite": "^6.3.4",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
@@ -92,7 +92,7 @@
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"stripe": "^18.0.0",
|
||||
"stripe": "^18.1.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
@@ -157,44 +157,44 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
|
||||
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz",
|
||||
"integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
|
||||
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.26.10",
|
||||
"@babel/helper-compilation-targets": "^7.26.5",
|
||||
"@babel/helper-module-transforms": "^7.26.0",
|
||||
"@babel/helpers": "^7.26.10",
|
||||
"@babel/parser": "^7.26.10",
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/traverse": "^7.26.10",
|
||||
"@babel/types": "^7.26.10",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/helper-compilation-targets": "^7.27.1",
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helpers": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -219,13 +219,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
|
||||
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
||||
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -235,26 +235,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-annotate-as-pure": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
|
||||
"integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
|
||||
"integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
|
||||
"integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz",
|
||||
"integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/helper-validator-option": "^7.25.9",
|
||||
"@babel/compat-data": "^7.27.1",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
@@ -273,18 +273,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
|
||||
"integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
|
||||
"integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||
"@babel/helper-replace-supers": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-member-expression-to-functions": "^7.27.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -305,41 +305,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-member-expression-to-functions": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
|
||||
"integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
|
||||
"integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
|
||||
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
|
||||
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
|
||||
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"@babel/traverse": "^7.25.9"
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -349,37 +349,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-optimise-call-expression": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz",
|
||||
"integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
|
||||
"integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
|
||||
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-replace-supers": {
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
|
||||
"integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
|
||||
"integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||
"@babel/traverse": "^7.26.5"
|
||||
"@babel/helper-member-expression-to-functions": "^7.27.1",
|
||||
"@babel/helper-optimise-call-expression": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -389,66 +389,66 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz",
|
||||
"integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
|
||||
"integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
|
||||
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
|
||||
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
|
||||
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.0",
|
||||
"@babel/types": "^7.27.0"
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
|
||||
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.0"
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -458,13 +458,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-decorators": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz",
|
||||
"integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz",
|
||||
"integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -474,13 +474,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-jsx": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz",
|
||||
"integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
|
||||
"integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -490,13 +490,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-typescript": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
|
||||
"integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
|
||||
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -506,14 +506,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-commonjs": {
|
||||
"version": "7.26.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz",
|
||||
"integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
|
||||
"integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.26.0",
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
"@babel/helper-module-transforms": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -523,12 +523,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
|
||||
"integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
||||
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -538,12 +538,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
|
||||
"integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
||||
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -553,17 +553,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typescript": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.0.tgz",
|
||||
"integrity": "sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz",
|
||||
"integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.0",
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.25.9"
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-create-class-features-plugin": "^7.27.1",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||
"@babel/plugin-syntax-typescript": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -573,17 +573,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-typescript": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.0.tgz",
|
||||
"integrity": "sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
|
||||
"integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.26.5",
|
||||
"@babel/helper-validator-option": "^7.25.9",
|
||||
"@babel/plugin-syntax-jsx": "^7.25.9",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-typescript": "^7.27.0"
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/plugin-syntax-jsx": "^7.27.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
|
||||
"@babel/plugin-transform-typescript": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -593,42 +593,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
|
||||
"integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/types": "^7.27.0"
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
|
||||
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/generator": "^7.27.0",
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/template": "^7.27.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/template": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@@ -637,13 +634,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1242,9 +1239,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz",
|
||||
"integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==",
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5784,9 +5781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.74.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.9.tgz",
|
||||
"integrity": "sha512-qmjXpWyigDw4SfqdSBy24FzRvpBPXlaSbl92N77lcrL+yvVQLQkf0T6bQNbTxl9IEB/SvVFhhVZoIlQvFnNuuw==",
|
||||
"version": "5.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.0.tgz",
|
||||
"integrity": "sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5794,12 +5791,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.74.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.9.tgz",
|
||||
"integrity": "sha512-F8xCXDQRDgsPzLzX9+d6ycNoITAIU2bycc1idZd06bt/GjN1quEJDjHvEDWZGoVn0A/ZmntVrYv6TE0kR7c7LA==",
|
||||
"version": "5.75.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.1.tgz",
|
||||
"integrity": "sha512-tN+gG+eXCHYm+VpmdXUP1rfE9LUrRzgYozTkBZtJV1/WFM3vwWNKQC8G6b2RKcs+2cPg+hdToZHZfjL3bF4yIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.74.9"
|
||||
"@tanstack/query-core": "5.75.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6091,9 +6088,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
|
||||
"integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
|
||||
"version": "19.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz",
|
||||
"integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -7427,9 +7424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001715",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
|
||||
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
|
||||
"version": "1.0.30001716",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz",
|
||||
"integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7930,9 +7927,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.41.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz",
|
||||
"integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==",
|
||||
"version": "3.42.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz",
|
||||
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -8175,9 +8172,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
|
||||
"integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
|
||||
"integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -8384,9 +8381,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.144",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.144.tgz",
|
||||
"integrity": "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ==",
|
||||
"version": "1.5.149",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.149.tgz",
|
||||
"integrity": "sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -9842,13 +9839,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.9.2",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.9.2.tgz",
|
||||
"integrity": "sha512-R0O3Jdqbfwywpm45obP+8sTgafmdEcUoShQTAV+rB5pi+Y1Px/FYL5qLLRe5tPtBdN1J4jos7M+xN2VV2oEAbQ==",
|
||||
"version": "12.9.4",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.9.4.tgz",
|
||||
"integrity": "sha512-yaeGDmGQ3eCQEwZ95/pRQMaSh/Q4E2CK6JYOclG/PdjyQad0MULJ+JFVV8911Fl5a6tF6o0wgW8Dpl5Qx4Adjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.9.1",
|
||||
"motion-utils": "^12.8.3",
|
||||
"motion-dom": "^12.9.4",
|
||||
"motion-utils": "^12.9.4",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -10588,9 +10585,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.5.tgz",
|
||||
"integrity": "sha512-OstebRKqKiQw8xEvQF5aRyUujsCatanj7Q9eo5iiH2gJpoXGZ7483ol3sVBwfqbobTQPNH1J+NAyJ1aCQoEC+w==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz",
|
||||
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
@@ -11987,9 +11984,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.503.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.503.0.tgz",
|
||||
"integrity": "sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==",
|
||||
"version": "0.506.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.506.0.tgz",
|
||||
"integrity": "sha512-/2znFFzlXcZKu0ANFCnxUOBV5I2e08m19PGtb6X+BcByRj8ODlGAl3wpe4LNVrDMLJ263JoIkZn7MOGK/5sXtw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -13125,18 +13122,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.9.1",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.9.1.tgz",
|
||||
"integrity": "sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==",
|
||||
"version": "12.9.4",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.9.4.tgz",
|
||||
"integrity": "sha512-25TWkQPj5I18m+qVjXGtCsxboY11DaRC5HMjd29tHKExazW4Zf4XtAagBdLpyKsVuAxEQ6cx5/E4AB21PFpLnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.8.3"
|
||||
"motion-utils": "^12.9.4"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.8.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.8.3.tgz",
|
||||
"integrity": "sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==",
|
||||
"version": "12.9.4",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.9.4.tgz",
|
||||
"integrity": "sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
@@ -14138,9 +14135,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.237.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.237.0.tgz",
|
||||
"integrity": "sha512-DyZfwDRz405cKKskL22CXvc9EpkBmuM9lCOYsZO3L1/zXu7IGiP9nNlLaxlzy7K/8mHxQ3szoy/DBSw/zXL1pw==",
|
||||
"version": "1.239.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.239.0.tgz",
|
||||
"integrity": "sha512-d8WTXGHmVO1FQV7wvEIan/MlN+gzdR42GHVOSoP3jWH2eiyCHCK4tX48uLZfvaEabDfuJCExdlmelWuYPAjJFw==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
@@ -14871,12 +14868,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||
@@ -16148,17 +16139,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.0.0.tgz",
|
||||
"integrity": "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA==",
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.1.0.tgz",
|
||||
"integrity": "sha512-MLDiniPTHqcfIT3anyBPmOEcaiDhYa7/jRaNypQ3Rt2SJnayQZBvVbFghIziUCZdltGAndm/ZxVOSw6uuSCDig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": ">=8.1.0",
|
||||
"qs": "^6.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=12.x.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
@@ -17178,9 +17176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
|
||||
"integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==",
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.35.0",
|
||||
"version": "0.36.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -16,22 +16,22 @@
|
||||
"@reduxjs/toolkit": "^2.7.0",
|
||||
"@stripe/react-stripe-js": "^3.6.0",
|
||||
"@stripe/stripe-js": "^7.2.0",
|
||||
"@tanstack/react-query": "^5.74.9",
|
||||
"@tanstack/react-query": "^5.75.1",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.9.2",
|
||||
"framer-motion": "^12.9.4",
|
||||
"i18next": "^25.0.2",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.27",
|
||||
"jose": "^6.0.10",
|
||||
"lucide-react": "^0.503.0",
|
||||
"lucide-react": "^0.506.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.237.0",
|
||||
"posthog-js": "^1.239.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"vite": "^6.3.3",
|
||||
"vite": "^6.3.4",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
@@ -77,8 +77,8 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"@babel/traverse": "^7.27.0",
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
@@ -91,7 +91,7 @@
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
@@ -116,7 +116,7 @@
|
||||
"msw": "^2.6.6",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier": "^3.5.3",
|
||||
"stripe": "^18.0.0",
|
||||
"stripe": "^18.1.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
|
||||
/**
|
||||
* Retrieves repositories where OpenHands Github App has been installed
|
||||
* @param installationIndex Pagination cursor position for app installation IDs
|
||||
* @param installations Collection of all App installation IDs for OpenHands Github App
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
export const retrieveGitHubAppRepositories = async (
|
||||
installationIndex: number,
|
||||
installations: number[],
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) => {
|
||||
const installationId = installations[installationIndex];
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
installation_id: installationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const link =
|
||||
response.data.length > 0 && response.data[0].link_header
|
||||
? response.data[0].link_header
|
||||
: "";
|
||||
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
let nextInstallation: number | null;
|
||||
|
||||
if (nextPage) {
|
||||
nextInstallation = installationIndex;
|
||||
} else if (installationIndex + 1 < installations.length) {
|
||||
nextInstallation = installationIndex + 1;
|
||||
} else {
|
||||
nextInstallation = null;
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
nextPage,
|
||||
installationIndex: nextInstallation,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a PAT, retrieves the repositories of the user
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
export const retrieveUserGitRepositories = async () => {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Check if any provider has more results
|
||||
const link =
|
||||
response.data.length > 0 && response.data[0].link_header
|
||||
? response.data[0].link_header
|
||||
: "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
|
||||
return { data: response.data, nextPage };
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ConversationTrigger,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
import { GitUser, GitRepository } from "#/types/git";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
|
||||
@@ -152,7 +152,8 @@ class OpenHands {
|
||||
|
||||
static async createConversation(
|
||||
conversation_trigger: ConversationTrigger = "gui",
|
||||
selectedRepository?: GitRepository,
|
||||
selectedRepository?: string,
|
||||
git_provider?: Provider,
|
||||
initialUserMsg?: string,
|
||||
imageUrls?: string[],
|
||||
replayJson?: string,
|
||||
@@ -160,7 +161,8 @@ class OpenHands {
|
||||
): Promise<Conversation> {
|
||||
const body = {
|
||||
conversation_trigger,
|
||||
selected_repository: selectedRepository,
|
||||
repository: selectedRepository,
|
||||
git_provider,
|
||||
selected_branch: undefined,
|
||||
initial_user_msg: initialUserMsg,
|
||||
image_urls: imageUrls,
|
||||
@@ -297,6 +299,23 @@ class OpenHands {
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a PAT, retrieves the repositories of the user
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
static async retrieveUserGitRepositories() {
|
||||
const { data } = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { BaseModal } from "../../shared/modals/base-modal/base-modal";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { selectSystemMessage } from "#/state/chat-slice";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
@@ -117,7 +118,10 @@ export function ConversationCard({
|
||||
const data = await response.json();
|
||||
|
||||
if (data.vscode_url) {
|
||||
window.open(data.vscode_url, "_blank");
|
||||
const transformedUrl = transformVSCodeUrl(data.vscode_url);
|
||||
if (transformedUrl) {
|
||||
window.open(transformedUrl, "_blank");
|
||||
}
|
||||
}
|
||||
// VS Code URL not available
|
||||
} catch (error) {
|
||||
|
||||
@@ -37,9 +37,6 @@ export function GitRepositoriesSuggestionBox({
|
||||
|
||||
const isLoading = isUserReposLoading || isSearchReposLoading;
|
||||
|
||||
const repositories =
|
||||
userRepositories?.pages.flatMap((page) => page.data) || [];
|
||||
|
||||
const handleConnectToGitHub = () => {
|
||||
if (gitHubAuthUrl) {
|
||||
window.location.href = gitHubAuthUrl;
|
||||
@@ -59,7 +56,7 @@ export function GitRepositoriesSuggestionBox({
|
||||
onInputChange={setSearchQuery}
|
||||
onSelect={handleSubmit}
|
||||
publicRepositories={searchedRepos || []}
|
||||
userRepositories={repositories}
|
||||
userRepositories={userRepositories || []}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -11,7 +11,7 @@ const mockUseAuth = vi.fn();
|
||||
|
||||
// Setup default mock returns
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: { pages: [{ data: [] }] },
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
@@ -88,26 +88,20 @@ describe("RepositorySelectionForm", () => {
|
||||
test("shows dropdown when repositories are loaded", () => {
|
||||
// Setup loaded repositories
|
||||
mockUseUserRepositories.mockReturnValue({
|
||||
data: {
|
||||
pages: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "user/repo1",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "user/repo2",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "user/repo1",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "user/repo2",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
@@ -87,14 +87,13 @@ export function RepositorySelectionForm({
|
||||
const isCreatingConversation =
|
||||
isPending || isSuccess || isCreatingConversationElsewhere;
|
||||
|
||||
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
|
||||
const repositoriesItems = repositoriesList?.map((repo) => ({
|
||||
const repositoriesItems = repositories?.map((repo) => ({
|
||||
key: repo.id,
|
||||
label: repo.full_name,
|
||||
}));
|
||||
|
||||
const handleRepoSelection = (key: React.Key | null) => {
|
||||
const selectedRepo = repositoriesList?.find(
|
||||
const selectedRepo = repositories?.find(
|
||||
(repo) => repo.id.toString() === key,
|
||||
);
|
||||
|
||||
|
||||
@@ -27,8 +27,7 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getRepo = (repo: string, git_provider: Provider) => {
|
||||
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
|
||||
const selectedRepo = repositoriesList?.find(
|
||||
const selectedRepo = repositories?.find(
|
||||
(repository) =>
|
||||
repository.full_name === repo &&
|
||||
repository.git_provider === git_provider,
|
||||
|
||||
@@ -24,13 +24,19 @@ export const useCreateConversation = () => {
|
||||
conversation_trigger: ConversationTrigger;
|
||||
q?: string;
|
||||
selectedRepository?: GitRepository | null;
|
||||
|
||||
suggested_task?: SuggestedTask;
|
||||
}) => {
|
||||
if (variables.q) dispatch(setInitialPrompt(variables.q));
|
||||
|
||||
return OpenHands.createConversation(
|
||||
variables.conversation_trigger,
|
||||
variables.selectedRepository || undefined,
|
||||
variables.selectedRepository
|
||||
? variables.selectedRepository.full_name
|
||||
: undefined,
|
||||
variables.selectedRepository
|
||||
? variables.selectedRepository.git_provider
|
||||
: undefined,
|
||||
variables.q,
|
||||
files,
|
||||
replayJson || undefined,
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveUserGitRepositories } from "#/api/git";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useUserRepositories = () => {
|
||||
const { providerTokensSet, providersAreSet } = useAuth();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
return useQuery({
|
||||
queryKey: ["repositories", providerTokensSet],
|
||||
queryFn: async () => retrieveUserGitRepositories(),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
queryFn: OpenHands.retrieveUserGitRepositories,
|
||||
enabled: providersAreSet,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
|
||||
// (nextui autocomplete doesn't support onEndReached nor is it compatible for extending)
|
||||
const { isSuccess, isFetchingNextPage, hasNextPage, fetchNextPage } = repos;
|
||||
React.useEffect(() => {
|
||||
if (!isFetchingNextPage && isSuccess && hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [isFetchingNextPage, isSuccess, hasNextPage, fetchNextPage]);
|
||||
|
||||
return repos;
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
@@ -159,7 +160,12 @@ function AppContent() {
|
||||
);
|
||||
const data = await response.json();
|
||||
if (data.vscode_url) {
|
||||
window.open(data.vscode_url, "_blank");
|
||||
const transformedUrl = transformVSCodeUrl(
|
||||
data.vscode_url,
|
||||
);
|
||||
if (transformedUrl) {
|
||||
window.open(transformedUrl, "_blank");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle the error
|
||||
|
||||
@@ -51,8 +51,8 @@ function GitSettingsScreen() {
|
||||
saveSettings(
|
||||
{
|
||||
provider_tokens: {
|
||||
github: githubToken,
|
||||
gitlab: gitlabToken,
|
||||
github: { token: githubToken },
|
||||
gitlab: { token: gitlabToken },
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useConversation } from "#/context/conversation-context";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
|
||||
function VSCodeTab() {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,7 +28,8 @@ function VSCodeTab() {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.vscode_url) {
|
||||
setVsCodeUrl(data.vscode_url);
|
||||
const transformedUrl = transformVSCodeUrl(data.vscode_url);
|
||||
setVsCodeUrl(transformedUrl);
|
||||
} else {
|
||||
setError(t(I18nKey.VSCODE$URL_NOT_AVAILABLE));
|
||||
}
|
||||
|
||||
@@ -152,6 +152,22 @@ export function handleAssistantMessage(message: Record<string, unknown>) {
|
||||
handleObservationMessage(message as unknown as ObservationMessage);
|
||||
} else if (message.status_update) {
|
||||
handleStatusMessage(message as unknown as StatusMessage);
|
||||
} else if (message.error) {
|
||||
// Handle error messages from the server
|
||||
const errorMessage =
|
||||
typeof message.message === "string"
|
||||
? message.message
|
||||
: String(message.message || "Unknown error");
|
||||
trackError({
|
||||
message: errorMessage,
|
||||
source: "websocket",
|
||||
metadata: { raw_message: message },
|
||||
});
|
||||
store.dispatch(
|
||||
addErrorMessage({
|
||||
message: errorMessage,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const errorMsg = "Unknown message type received";
|
||||
trackError({
|
||||
|
||||
@@ -16,8 +16,8 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
PROVIDER_TOKENS: {
|
||||
github: "",
|
||||
gitlab: "",
|
||||
github: { token: "" },
|
||||
gitlab: { token: "" },
|
||||
},
|
||||
IS_NEW_USER: true,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,10 @@ export const ProviderOptions = {
|
||||
|
||||
export type Provider = keyof typeof ProviderOptions;
|
||||
|
||||
export type ProviderToken = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
LLM_MODEL: string;
|
||||
LLM_BASE_URL: string;
|
||||
@@ -18,7 +22,7 @@ export type Settings = {
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
ENABLE_SOUND_NOTIFICATIONS: boolean;
|
||||
USER_CONSENTS_TO_ANALYTICS: boolean | null;
|
||||
PROVIDER_TOKENS: Record<Provider, string>;
|
||||
PROVIDER_TOKENS: Record<Provider, ProviderToken>;
|
||||
IS_NEW_USER?: boolean;
|
||||
};
|
||||
|
||||
@@ -35,17 +39,17 @@ export type ApiSettings = {
|
||||
enable_default_condenser: boolean;
|
||||
enable_sound_notifications: boolean;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens: Record<Provider, ProviderToken>;
|
||||
provider_tokens_set: Partial<Record<Provider, string | null>>;
|
||||
};
|
||||
|
||||
export type PostSettings = Settings & {
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens: Record<Provider, ProviderToken>;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
llm_api_key?: string | null;
|
||||
};
|
||||
|
||||
export type PostApiSettings = ApiSettings & {
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens: Record<Provider, ProviderToken>;
|
||||
user_consents_to_analytics: boolean | null;
|
||||
};
|
||||
|
||||
61
frontend/src/utils/__tests__/vscode-url-helper.test.ts
Normal file
61
frontend/src/utils/__tests__/vscode-url-helper.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { transformVSCodeUrl } from "../vscode-url-helper";
|
||||
|
||||
describe("transformVSCodeUrl", () => {
|
||||
const originalWindowLocation = window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hostname: "example.com",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore window.location
|
||||
Object.defineProperty(window, "location", {
|
||||
value: originalWindowLocation,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null if input is null", () => {
|
||||
expect(transformVSCodeUrl(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("should replace localhost with current hostname when they differ", () => {
|
||||
const input = "http://localhost:8080/?tkn=abc123&folder=/workspace";
|
||||
const expected = "http://example.com:8080/?tkn=abc123&folder=/workspace";
|
||||
|
||||
expect(transformVSCodeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should not modify URL if hostname is not localhost", () => {
|
||||
const input = "http://otherhost:8080/?tkn=abc123&folder=/workspace";
|
||||
|
||||
expect(transformVSCodeUrl(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("should not modify URL if current hostname is also localhost", () => {
|
||||
// Change the mocked hostname to localhost
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hostname: "localhost",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const input = "http://localhost:8080/?tkn=abc123&folder=/workspace";
|
||||
|
||||
expect(transformVSCodeUrl(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("should handle invalid URLs gracefully", () => {
|
||||
const input = "not-a-valid-url";
|
||||
|
||||
expect(transformVSCodeUrl(input)).toBe(input);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Settings } from "#/types/settings";
|
||||
import { Provider, ProviderToken, Settings } from "#/types/settings";
|
||||
|
||||
const extractBasicFormData = (formData: FormData) => {
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
@@ -64,14 +64,14 @@ export const extractSettings = (
|
||||
// Extract provider tokens
|
||||
const githubToken = formData.get("github-token")?.toString();
|
||||
const gitlabToken = formData.get("gitlab-token")?.toString();
|
||||
const providerTokens: Record<string, string> = {};
|
||||
|
||||
if (githubToken) {
|
||||
providerTokens.github = githubToken;
|
||||
}
|
||||
if (gitlabToken) {
|
||||
providerTokens.gitlab = gitlabToken;
|
||||
}
|
||||
const providerTokens: Record<Provider, ProviderToken> = {
|
||||
github: {
|
||||
token: githubToken || "",
|
||||
},
|
||||
gitlab: {
|
||||
token: gitlabToken || "",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
|
||||
|
||||
31
frontend/src/utils/vscode-url-helper.ts
Normal file
31
frontend/src/utils/vscode-url-helper.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Helper function to transform VS Code URLs
|
||||
*
|
||||
* This function checks if a VS Code URL points to localhost and replaces it with
|
||||
* the current window's hostname if they don't match.
|
||||
*
|
||||
* @param vsCodeUrl The original VS Code URL from the backend
|
||||
* @returns The transformed URL with the correct hostname
|
||||
*/
|
||||
export function transformVSCodeUrl(vsCodeUrl: string | null): string | null {
|
||||
if (!vsCodeUrl) return null;
|
||||
|
||||
try {
|
||||
const url = new URL(vsCodeUrl);
|
||||
|
||||
// Check if the URL points to localhost
|
||||
if (
|
||||
url.hostname === "localhost" &&
|
||||
window.location.hostname !== "localhost"
|
||||
) {
|
||||
// Replace localhost with the current hostname
|
||||
url.hostname = window.location.hostname;
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return vsCodeUrl;
|
||||
} catch (error) {
|
||||
// Silently handle the error and return the original URL
|
||||
return vsCodeUrl;
|
||||
}
|
||||
}
|
||||
@@ -177,38 +177,30 @@ class CodeActAgent(Agent):
|
||||
}
|
||||
params['tools'] = self.tools
|
||||
|
||||
if self.mcp_tools:
|
||||
# Only add tools with unique names
|
||||
existing_names = {tool['function']['name'] for tool in params['tools']}
|
||||
unique_mcp_tools = [
|
||||
tool
|
||||
for tool in self.mcp_tools
|
||||
if tool['function']['name'] not in existing_names
|
||||
]
|
||||
|
||||
if self.llm.config.model == 'gemini-2.5-pro-preview-03-25':
|
||||
logger.info(
|
||||
f'Removing the default fields from the MCP tools for {self.llm.config.model} '
|
||||
"since it doesn't support them and the request would crash."
|
||||
)
|
||||
# prevent mutation of input tools
|
||||
unique_mcp_tools = copy.deepcopy(unique_mcp_tools)
|
||||
# Strip off default fields that cause errors with gemini-preview
|
||||
for tool in unique_mcp_tools:
|
||||
if 'function' in tool and 'parameters' in tool['function']:
|
||||
if 'properties' in tool['function']['parameters']:
|
||||
for prop_name, prop in tool['function']['parameters'][
|
||||
'properties'
|
||||
].items():
|
||||
if 'default' in prop:
|
||||
del prop['default']
|
||||
|
||||
params['tools'] += unique_mcp_tools
|
||||
# Special handling for Gemini model which doesn't support default fields
|
||||
if self.llm.config.model == 'gemini-2.5-pro-preview-03-25':
|
||||
logger.info(
|
||||
f'Removing the default fields from tools for {self.llm.config.model} '
|
||||
"since it doesn't support them and the request would crash."
|
||||
)
|
||||
# prevent mutation of input tools
|
||||
params['tools'] = copy.deepcopy(params['tools'])
|
||||
# Strip off default fields that cause errors with gemini-preview
|
||||
for tool in params['tools']:
|
||||
if 'function' in tool and 'parameters' in tool['function']:
|
||||
if 'properties' in tool['function']['parameters']:
|
||||
for prop_name, prop in tool['function']['parameters'][
|
||||
'properties'
|
||||
].items():
|
||||
if 'default' in prop:
|
||||
del prop['default']
|
||||
# log to litellm proxy if possible
|
||||
params['extra_body'] = {'metadata': state.to_llm_metadata(agent_name=self.name)}
|
||||
response = self.llm.completion(**params)
|
||||
logger.debug(f'Response from LLM: {response}')
|
||||
actions = self.response_to_actions_fn(response)
|
||||
actions = self.response_to_actions_fn(
|
||||
response, mcp_tool_names=list(self.mcp_tools.keys())
|
||||
)
|
||||
logger.debug(f'Actions after response_to_actions: {actions}')
|
||||
for action in actions:
|
||||
self.pending_actions.append(action)
|
||||
|
||||
@@ -37,10 +37,9 @@ from openhands.events.action import (
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
from openhands.mcp import MCPClientTool
|
||||
|
||||
|
||||
def combine_thought(action: Action, thought: str) -> Action:
|
||||
@@ -53,7 +52,9 @@ def combine_thought(action: Action, thought: str) -> Action:
|
||||
return action
|
||||
|
||||
|
||||
def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
def response_to_actions(
|
||||
response: ModelResponse, mcp_tool_names: list[str] | None = None
|
||||
) -> list[Action]:
|
||||
actions: list[Action] = []
|
||||
assert len(response.choices) == 1, 'Only one choice is supported for now'
|
||||
choice = response.choices[0]
|
||||
@@ -75,7 +76,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
try:
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise RuntimeError(
|
||||
raise FunctionCallValidationError(
|
||||
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
|
||||
) from e
|
||||
|
||||
@@ -195,12 +196,12 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
action = BrowseURLAction(url=arguments['url'])
|
||||
|
||||
# ================================================
|
||||
# McpAction (MCP)
|
||||
# MCPAction (MCP)
|
||||
# ================================================
|
||||
elif tool_call.function.name.endswith(MCPClientTool.postfix()):
|
||||
action = McpAction(
|
||||
name=tool_call.function.name.removesuffix(MCPClientTool.postfix()),
|
||||
arguments=tool_call.function.arguments,
|
||||
elif mcp_tool_names and tool_call.function.name in mcp_tool_names:
|
||||
action = MCPAction(
|
||||
name=tool_call.function.name,
|
||||
arguments=arguments,
|
||||
)
|
||||
else:
|
||||
raise FunctionCallNotExistsError(
|
||||
|
||||
@@ -36,6 +36,7 @@ from openhands.events.action import (
|
||||
BrowseURLAction,
|
||||
CmdRunAction,
|
||||
FileReadAction,
|
||||
MCPAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.event import FileReadSource
|
||||
@@ -102,7 +103,9 @@ def glob_to_cmdrun(pattern: str, path: str = '.') -> str:
|
||||
return echo_cmd + complete_cmd
|
||||
|
||||
|
||||
def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
def response_to_actions(
|
||||
response: ModelResponse, mcp_tool_names: list[str] | None = None
|
||||
) -> list[Action]:
|
||||
actions: list[Action] = []
|
||||
assert len(response.choices) == 1, 'Only one choice is supported for now'
|
||||
choice = response.choices[0]
|
||||
@@ -124,7 +127,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
try:
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise RuntimeError(
|
||||
raise FunctionCallValidationError(
|
||||
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
|
||||
) from e
|
||||
|
||||
@@ -198,6 +201,15 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
)
|
||||
action = BrowseURLAction(url=arguments['url'])
|
||||
|
||||
# ================================================
|
||||
# MCPAction (MCP)
|
||||
# ================================================
|
||||
elif mcp_tool_names and tool_call.function.name in mcp_tool_names:
|
||||
action = MCPAction(
|
||||
name=tool_call.function.name,
|
||||
arguments=arguments,
|
||||
)
|
||||
|
||||
else:
|
||||
raise FunctionCallNotExistsError(
|
||||
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
|
||||
|
||||
@@ -8,6 +8,8 @@ if TYPE_CHECKING:
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.events.action import Action
|
||||
from openhands.events.action.message import SystemMessageAction
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
from openhands.core.exceptions import (
|
||||
AgentAlreadyRegisteredError,
|
||||
AgentNotRegisteredError,
|
||||
@@ -42,7 +44,7 @@ class Agent(ABC):
|
||||
self.config = config
|
||||
self._complete = False
|
||||
self.prompt_manager: 'PromptManager' | None = None
|
||||
self.mcp_tools: list[dict] = []
|
||||
self.mcp_tools: dict[str, ChatCompletionToolParam] = {}
|
||||
self.tools: list = []
|
||||
|
||||
def get_system_message(self) -> 'SystemMessageAction | None':
|
||||
@@ -160,4 +162,18 @@ class Agent(ABC):
|
||||
Args:
|
||||
- mcp_tools (list[dict]): The list of MCP tools.
|
||||
"""
|
||||
self.mcp_tools = mcp_tools
|
||||
logger.info(
|
||||
f"Setting {len(mcp_tools)} MCP tools for agent {self.name}: {[tool['function']['name'] for tool in mcp_tools]}"
|
||||
)
|
||||
for tool in mcp_tools:
|
||||
_tool = ChatCompletionToolParam(**tool)
|
||||
if _tool['function']['name'] in self.mcp_tools:
|
||||
logger.warning(
|
||||
f"Tool {_tool['function']['name']} already exists, skipping"
|
||||
)
|
||||
continue
|
||||
self.mcp_tools[_tool['function']['name']] = _tool
|
||||
self.tools.append(_tool)
|
||||
logger.info(
|
||||
f"Tools updated for agent {self.name}, total {len(self.tools)}: {[tool['function']['name'] for tool in self.tools]}"
|
||||
)
|
||||
|
||||
@@ -741,10 +741,6 @@ class AgentController:
|
||||
content = (
|
||||
f'{self.delegate.agent.name} finishes task with {formatted_output}'
|
||||
)
|
||||
|
||||
# emit the delegate result observation
|
||||
obs = AgentDelegateObservation(outputs=delegate_outputs, content=content)
|
||||
self.event_stream.add_event(obs, EventSource.AGENT)
|
||||
else:
|
||||
# delegate state is ERROR
|
||||
# emit AgentDelegateObservation with error content
|
||||
@@ -755,13 +751,22 @@ class AgentController:
|
||||
f'{self.delegate.agent.name} encountered an error during execution.'
|
||||
)
|
||||
|
||||
# emit the delegate result observation
|
||||
obs = AgentDelegateObservation(outputs=delegate_outputs, content=content)
|
||||
self.event_stream.add_event(obs, EventSource.AGENT)
|
||||
content = f'Delegated agent finished with result:\n\n{content}'
|
||||
|
||||
# emit the delegate result observation
|
||||
obs = AgentDelegateObservation(outputs=delegate_outputs, content=content)
|
||||
|
||||
# associate the delegate action with the initiating tool call
|
||||
for event in reversed(self.state.history):
|
||||
if isinstance(event, AgentDelegateAction):
|
||||
delegate_action = event
|
||||
obs.tool_call_metadata = delegate_action.tool_call_metadata
|
||||
break
|
||||
|
||||
self.event_stream.add_event(obs, EventSource.AGENT)
|
||||
|
||||
# unset delegate so parent can resume normal handling
|
||||
self.delegate = None
|
||||
self.delegateAction = None
|
||||
|
||||
async def _step(self) -> None:
|
||||
"""Executes a single step of the parent or delegate agent. Detects stuck agents and limits on the number of iterations and the task budget."""
|
||||
|
||||
@@ -54,7 +54,7 @@ from openhands.events.observation import (
|
||||
AgentStateChangedObservation,
|
||||
)
|
||||
from openhands.io import read_task
|
||||
from openhands.mcp import fetch_mcp_tools_from_config
|
||||
from openhands.mcp import add_mcp_tools_to_agent
|
||||
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
|
||||
LLMSummarizingCondenserConfig,
|
||||
)
|
||||
@@ -101,7 +101,7 @@ async def run_session(
|
||||
|
||||
sid = str(uuid4())
|
||||
is_loaded = asyncio.Event()
|
||||
is_paused = asyncio.Event()
|
||||
is_paused = asyncio.Event() # Event to track agent pause requests
|
||||
|
||||
# Show runtime initialization message
|
||||
display_runtime_initialization_message(config.runtime)
|
||||
@@ -112,8 +112,6 @@ async def run_session(
|
||||
)
|
||||
|
||||
agent = create_agent(config)
|
||||
mcp_tools = await fetch_mcp_tools_from_config(config.mcp)
|
||||
agent.set_mcp_tools(mcp_tools)
|
||||
runtime = create_runtime(
|
||||
config,
|
||||
sid=sid,
|
||||
@@ -159,20 +157,15 @@ async def run_session(
|
||||
display_event(event, config)
|
||||
update_usage_metrics(event, usage_metrics)
|
||||
|
||||
# Pause the agent if the pause event is set (if Ctrl-P is pressed)
|
||||
if is_paused.is_set():
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.PAUSED),
|
||||
EventSource.USER,
|
||||
)
|
||||
is_paused.clear()
|
||||
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state in [
|
||||
AgentState.AWAITING_USER_INPUT,
|
||||
AgentState.FINISHED,
|
||||
AgentState.PAUSED,
|
||||
]:
|
||||
# If the agent is paused, do not prompt for input as it's already handled by PAUSED state change
|
||||
if is_paused.is_set():
|
||||
return
|
||||
|
||||
# Reload microagents after initialization of repo.md
|
||||
if reload_microagents:
|
||||
microagents: list[BaseMicroagent] = (
|
||||
@@ -183,25 +176,32 @@ async def run_session(
|
||||
await prompt_for_next_task(event.agent_state)
|
||||
|
||||
if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
|
||||
# Only display the confirmation prompt if the agent is not paused
|
||||
if not is_paused.is_set():
|
||||
user_confirmed = await read_confirmation_input()
|
||||
if user_confirmed:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_CONFIRMED),
|
||||
EventSource.USER,
|
||||
)
|
||||
else:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_REJECTED),
|
||||
EventSource.USER,
|
||||
)
|
||||
# If the agent is paused, do not prompt for confirmation
|
||||
# The confirmation step will re-run after the agent has been resumed
|
||||
if is_paused.is_set():
|
||||
return
|
||||
|
||||
user_confirmed = await read_confirmation_input()
|
||||
if user_confirmed:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_CONFIRMED),
|
||||
EventSource.USER,
|
||||
)
|
||||
else:
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.USER_REJECTED),
|
||||
EventSource.USER,
|
||||
)
|
||||
|
||||
if event.agent_state == AgentState.PAUSED:
|
||||
is_paused.clear() # Revert the event state before prompting for user input
|
||||
await prompt_for_next_task(event.agent_state)
|
||||
|
||||
if event.agent_state == AgentState.RUNNING:
|
||||
# Enable pause/resume functionality only if the confirmation mode is disabled
|
||||
if not config.security.confirmation_mode:
|
||||
display_agent_running_message()
|
||||
loop.create_task(process_agent_pause(is_paused))
|
||||
display_agent_running_message()
|
||||
loop.create_task(
|
||||
process_agent_pause(is_paused, event_stream)
|
||||
) # Create a task to track agent pause requests from the user
|
||||
|
||||
def on_event(event: Event) -> None:
|
||||
loop.create_task(on_event_async(event))
|
||||
@@ -209,6 +209,7 @@ async def run_session(
|
||||
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, str(uuid4()))
|
||||
|
||||
await runtime.connect()
|
||||
await add_mcp_tools_to_agent(agent, runtime, config.mcp)
|
||||
|
||||
# Initialize repository if needed
|
||||
repo_directory = None
|
||||
|
||||
@@ -25,10 +25,11 @@ from prompt_toolkit.widgets import Frame, TextArea
|
||||
from openhands import __version__
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events import EventSource
|
||||
from openhands.events import EventSource, EventStream
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
ChangeAgentStateAction,
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
MessageAction,
|
||||
@@ -60,7 +61,7 @@ COMMANDS = {
|
||||
'/status': 'Display session details and usage metrics',
|
||||
'/new': 'Create a new session',
|
||||
'/settings': 'Display and modify current settings',
|
||||
'/resume': 'Resume the agent',
|
||||
'/resume': 'Resume the agent when paused',
|
||||
}
|
||||
|
||||
|
||||
@@ -396,7 +397,7 @@ def display_status(usage_metrics: UsageMetrics, session_id: str):
|
||||
def display_agent_running_message():
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
HTML('<gold>Agent running...</gold> <grey>(Ctrl-P to pause)</grey>')
|
||||
HTML('<gold>Agent running...</gold> <grey>(Press Ctrl-P to pause)</grey>')
|
||||
)
|
||||
|
||||
|
||||
@@ -405,7 +406,7 @@ def display_agent_paused_message(agent_state: str):
|
||||
return
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
HTML('<gold>Agent paused</gold> <grey>(type /resume to resume)</grey>')
|
||||
HTML('<gold>Agent paused...</gold> <grey>(Enter /resume to continue)</grey>')
|
||||
)
|
||||
|
||||
|
||||
@@ -430,7 +431,7 @@ class CommandCompleter(Completer):
|
||||
command,
|
||||
start_position=-len(text),
|
||||
display_meta=description,
|
||||
style='bg:ansidarkgray fg:ansiwhite',
|
||||
style='bg:ansidarkgray fg:gold',
|
||||
)
|
||||
|
||||
|
||||
@@ -488,7 +489,7 @@ async def read_confirmation_input() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def process_agent_pause(done: asyncio.Event) -> None:
|
||||
async def process_agent_pause(done: asyncio.Event, event_stream: EventStream) -> None:
|
||||
input = create_input()
|
||||
|
||||
def keys_ready():
|
||||
@@ -496,6 +497,10 @@ async def process_agent_pause(done: asyncio.Event) -> None:
|
||||
if key_press.key == Keys.ControlP:
|
||||
print_formatted_text('')
|
||||
print_formatted_text(HTML('<gold>Pausing the agent...</gold>'))
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.PAUSED),
|
||||
EventSource.USER,
|
||||
)
|
||||
done.set()
|
||||
|
||||
with input.raw_mode():
|
||||
|
||||
@@ -7,6 +7,7 @@ from openhands.core.config.config_utils import (
|
||||
)
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.config.mcp_config import MCPConfig
|
||||
from openhands.core.config.sandbox_config import SandboxConfig
|
||||
from openhands.core.config.security_config import SecurityConfig
|
||||
from openhands.core.config.utils import (
|
||||
@@ -26,6 +27,7 @@ __all__ = [
|
||||
'OH_MAX_ITERATIONS',
|
||||
'AgentConfig',
|
||||
'AppConfig',
|
||||
'MCPConfig',
|
||||
'LLMConfig',
|
||||
'SandboxConfig',
|
||||
'SecurityConfig',
|
||||
|
||||
@@ -63,10 +63,14 @@ class AppConfig(BaseModel):
|
||||
save_trajectory_path: str | None = Field(default=None)
|
||||
save_screenshots_in_trajectory: bool = Field(default=False)
|
||||
replay_trajectory_path: str | None = Field(default=None)
|
||||
workspace_base: str | None = Field(default=None)
|
||||
workspace_mount_path: str | None = Field(default=None)
|
||||
workspace_mount_path_in_sandbox: str = Field(default='/workspace')
|
||||
workspace_mount_rewrite: str | None = Field(default=None)
|
||||
# New mount parameter that replaces the workspace_* parameters
|
||||
runtime_mount: str | None = Field(default=None, description="Mount specification in the format 'host_path:container_path:mode', e.g. '/my/host/dir:/workspace:rw'")
|
||||
|
||||
# Deprecated parameters - will be removed in a future version
|
||||
workspace_base: str | None = Field(default=None, deprecated=True)
|
||||
workspace_mount_path: str | None = Field(default=None, deprecated=True)
|
||||
workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True)
|
||||
workspace_mount_rewrite: str | None = Field(default=None, deprecated=True)
|
||||
cache_dir: str = Field(default='/tmp/cache')
|
||||
run_as_openhands: bool = Field(default=True)
|
||||
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
|
||||
|
||||
@@ -1,28 +1,59 @@
|
||||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
|
||||
class MCPSSEServerConfig(BaseModel):
|
||||
"""Configuration for a single MCP server.
|
||||
|
||||
Attributes:
|
||||
url: The server URL
|
||||
api_key: Optional API key for authentication
|
||||
"""
|
||||
|
||||
url: str
|
||||
api_key: str | None = None
|
||||
|
||||
|
||||
class MCPStdioServerConfig(BaseModel):
|
||||
"""Configuration for a MCP server that uses stdio.
|
||||
|
||||
Attributes:
|
||||
name: The name of the server
|
||||
command: The command to run the server
|
||||
args: The arguments to pass to the server
|
||||
env: The environment variables to set for the server
|
||||
"""
|
||||
|
||||
name: str
|
||||
command: str
|
||||
args: list[str] = Field(default_factory=list)
|
||||
env: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class MCPConfig(BaseModel):
|
||||
"""Configuration for MCP (Message Control Protocol) settings.
|
||||
|
||||
Attributes:
|
||||
mcp_servers: List of MCP SSE (Server-Sent Events) server URLs.
|
||||
sse_servers: List of MCP SSE server configs
|
||||
stdio_servers: List of MCP stdio server configs. These servers will be added to the MCP Router running inside runtime container.
|
||||
"""
|
||||
|
||||
mcp_servers: List[str] = Field(default_factory=list)
|
||||
sse_servers: list[MCPSSEServerConfig] = Field(default_factory=list)
|
||||
stdio_servers: list[MCPStdioServerConfig] = Field(default_factory=list)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
def validate_servers(self) -> None:
|
||||
"""Validate that server URLs are valid and unique."""
|
||||
urls = [server.url for server in self.sse_servers]
|
||||
|
||||
# Check for duplicate server URLs
|
||||
if len(set(self.mcp_servers)) != len(self.mcp_servers):
|
||||
if len(set(urls)) != len(urls):
|
||||
raise ValueError('Duplicate MCP server URLs are not allowed')
|
||||
|
||||
# Validate URLs
|
||||
for url in self.mcp_servers:
|
||||
for url in urls:
|
||||
try:
|
||||
result = urlparse(url)
|
||||
if not all([result.scheme, result.netloc]):
|
||||
@@ -44,11 +75,32 @@ class MCPConfig(BaseModel):
|
||||
mcp_mapping: dict[str, MCPConfig] = {}
|
||||
|
||||
try:
|
||||
# Convert all entries in sse_servers to MCPSSEServerConfig objects
|
||||
if 'sse_servers' in data:
|
||||
servers = []
|
||||
for server in data['sse_servers']:
|
||||
if isinstance(server, dict):
|
||||
servers.append(MCPSSEServerConfig(**server))
|
||||
else:
|
||||
# Convert string URLs to MCPSSEServerConfig objects with no API key
|
||||
servers.append(MCPSSEServerConfig(url=server))
|
||||
data['sse_servers'] = servers
|
||||
|
||||
# Convert all entries in stdio_servers to MCPStdioServerConfig objects
|
||||
if 'stdio_servers' in data:
|
||||
servers = []
|
||||
for server in data['stdio_servers']:
|
||||
servers.append(MCPStdioServerConfig(**server))
|
||||
data['stdio_servers'] = servers
|
||||
|
||||
# Create SSE config if present
|
||||
mcp_config = MCPConfig.model_validate(data)
|
||||
mcp_config.validate_servers()
|
||||
# Create the main MCP config
|
||||
mcp_mapping['mcp'] = cls(mcp_servers=mcp_config.mcp_servers)
|
||||
mcp_mapping['mcp'] = cls(
|
||||
sse_servers=mcp_config.sse_servers,
|
||||
stdio_servers=mcp_config.stdio_servers,
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise ValueError(f'Invalid MCP configuration: {e}')
|
||||
|
||||
|
||||
@@ -294,10 +294,39 @@ def get_or_create_jwt_secret(file_store: FileStore) -> str:
|
||||
|
||||
def finalize_config(cfg: AppConfig) -> None:
|
||||
"""More tweaks to the config after it's been loaded."""
|
||||
if cfg.workspace_base is not None:
|
||||
cfg.workspace_base = os.path.abspath(cfg.workspace_base)
|
||||
if cfg.workspace_mount_path is None:
|
||||
cfg.workspace_mount_path = cfg.workspace_base
|
||||
# Handle the new runtime_mount parameter
|
||||
if cfg.runtime_mount is not None:
|
||||
# Parse the runtime_mount parameter
|
||||
parts = cfg.runtime_mount.split(':')
|
||||
if len(parts) < 2 or len(parts) > 3:
|
||||
raise ValueError(
|
||||
f"Invalid runtime_mount format: {cfg.runtime_mount}. "
|
||||
f"Expected format: 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'"
|
||||
)
|
||||
|
||||
host_path = os.path.abspath(parts[0])
|
||||
container_path = parts[1]
|
||||
# Default mode is 'rw' if not specified
|
||||
mode = parts[2] if len(parts) > 2 else 'rw'
|
||||
|
||||
# Set the workspace_mount_path and workspace_mount_path_in_sandbox for backward compatibility
|
||||
cfg.workspace_mount_path = host_path
|
||||
cfg.workspace_mount_path_in_sandbox = container_path
|
||||
|
||||
# Also set workspace_base for backward compatibility
|
||||
cfg.workspace_base = host_path
|
||||
|
||||
# Handle the deprecated workspace_* parameters
|
||||
elif cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
|
||||
logger.openhands_logger.warning(
|
||||
"DEPRECATED: The WORKSPACE_BASE and WORKSPACE_MOUNT_PATH environment variables are deprecated. "
|
||||
"Please use RUNTIME_MOUNT instead, e.g. 'RUNTIME_MOUNT=/my/host/dir:/workspace:rw'"
|
||||
)
|
||||
|
||||
if cfg.workspace_base is not None:
|
||||
cfg.workspace_base = os.path.abspath(cfg.workspace_base)
|
||||
if cfg.workspace_mount_path is None:
|
||||
cfg.workspace_mount_path = cfg.workspace_base
|
||||
|
||||
if cfg.workspace_mount_rewrite:
|
||||
base = cfg.workspace_base or os.getcwd()
|
||||
|
||||
@@ -30,7 +30,7 @@ from openhands.events.action.action import Action
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import AgentStateChangedObservation
|
||||
from openhands.io import read_input, read_task
|
||||
from openhands.mcp import fetch_mcp_tools_from_config
|
||||
from openhands.mcp import add_mcp_tools_to_agent
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
@@ -96,8 +96,6 @@ async def run_controller(
|
||||
|
||||
if agent is None:
|
||||
agent = create_agent(config)
|
||||
mcp_tools = await fetch_mcp_tools_from_config(config.mcp)
|
||||
agent.set_mcp_tools(mcp_tools)
|
||||
|
||||
# when the runtime is created, it will be connected and clone the selected repository
|
||||
repo_directory = None
|
||||
@@ -118,6 +116,8 @@ async def run_controller(
|
||||
selected_repository=config.sandbox.selected_repo,
|
||||
)
|
||||
|
||||
await add_mcp_tools_to_agent(agent, runtime, config.mcp)
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
|
||||
# when memory is created, it will load the microagents from the selected repository
|
||||
|
||||
@@ -85,40 +85,41 @@ def create_runtime(
|
||||
|
||||
|
||||
def initialize_repository_for_runtime(
|
||||
runtime: Runtime,
|
||||
selected_repository: str | None = None,
|
||||
github_token: SecretStr | None = None,
|
||||
runtime: Runtime, selected_repository: str | None = None
|
||||
) -> str | None:
|
||||
"""Initialize the repository for the runtime.
|
||||
|
||||
Args:
|
||||
runtime: The runtime to initialize the repository for.
|
||||
selected_repository: (optional) The GitHub repository to use.
|
||||
github_token: (optional) The GitHub token to use.
|
||||
|
||||
Returns:
|
||||
The repository directory path if a repository was cloned, None otherwise.
|
||||
"""
|
||||
# clone selected repository if provided
|
||||
if github_token is None and 'GITHUB_TOKEN' in os.environ:
|
||||
provider_tokens = {}
|
||||
if 'GITHUB_TOKEN' in os.environ:
|
||||
github_token = SecretStr(os.environ['GITHUB_TOKEN'])
|
||||
provider_tokens[ProviderType.GITHUB] = ProviderToken(
|
||||
token=SecretStr(github_token)
|
||||
)
|
||||
|
||||
if 'GITLAB_TOKEN' in os.environ:
|
||||
gitlab_token = SecretStr(os.environ['GITLAB_TOKEN'])
|
||||
provider_tokens[ProviderType.GITLAB] = ProviderToken(
|
||||
token=SecretStr(gitlab_token)
|
||||
)
|
||||
|
||||
secret_store = (
|
||||
SecretStore(
|
||||
provider_tokens={
|
||||
ProviderType.GITHUB: ProviderToken(token=SecretStr(github_token))
|
||||
}
|
||||
)
|
||||
if github_token
|
||||
else None
|
||||
SecretStore(provider_tokens=provider_tokens) if provider_tokens else None
|
||||
)
|
||||
provider_tokens = secret_store.provider_tokens if secret_store else None
|
||||
immutable_provider_tokens = secret_store.provider_tokens if secret_store else None
|
||||
|
||||
logger.debug(f'Selected repository {selected_repository}.')
|
||||
repo_directory = call_async_from_sync(
|
||||
runtime.clone_or_init_repo,
|
||||
GENERAL_TIMEOUT,
|
||||
provider_tokens,
|
||||
immutable_provider_tokens,
|
||||
selected_repository,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from openhands.events.action.files import (
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
)
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.action.message import MessageAction, SystemMessageAction
|
||||
|
||||
__all__ = [
|
||||
@@ -37,5 +37,5 @@ __all__ = [
|
||||
'ActionConfirmationStatus',
|
||||
'AgentThinkAction',
|
||||
'RecallAction',
|
||||
'McpAction',
|
||||
'MCPAction',
|
||||
]
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from openhands.core.schema import ActionType
|
||||
from openhands.events.action.action import Action, ActionSecurityRisk
|
||||
|
||||
|
||||
@dataclass
|
||||
class McpAction(Action):
|
||||
class MCPAction(Action):
|
||||
name: str
|
||||
arguments: str | None = None
|
||||
arguments: dict[str, Any] = field(default_factory=dict)
|
||||
thought: str = ''
|
||||
action: str = ActionType.MCP
|
||||
runnable: ClassVar[bool] = True
|
||||
@@ -24,7 +24,7 @@ class McpAction(Action):
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
ret = '**McpAction**\n'
|
||||
ret = '**MCPAction**\n'
|
||||
if self.thought:
|
||||
ret += f'THOUGHT: {self.thought}\n'
|
||||
ret += f'NAME: {self.name}\n'
|
||||
|
||||
@@ -21,6 +21,7 @@ from openhands.events.observation.files import (
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
)
|
||||
from openhands.events.observation.mcp import MCPObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.events.observation.reject import UserRejectObservation
|
||||
from openhands.events.observation.success import SuccessObservation
|
||||
|
||||
@@ -22,7 +22,7 @@ from openhands.events.action.files import (
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
)
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.action.message import MessageAction, SystemMessageAction
|
||||
|
||||
actions = (
|
||||
@@ -43,7 +43,7 @@ actions = (
|
||||
MessageAction,
|
||||
SystemMessageAction,
|
||||
CondensationAction,
|
||||
McpAction,
|
||||
MCPAction,
|
||||
)
|
||||
|
||||
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
|
||||
|
||||
@@ -390,6 +390,20 @@ class GitHubService(BaseGitService, GitService):
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def get_repository_details_from_repo_name(
|
||||
self, repository: str
|
||||
) -> Repository:
|
||||
url = f'{self.BASE_URL}/repos/{repository}'
|
||||
repo, _ = await self._make_request(url)
|
||||
|
||||
return Repository(
|
||||
id=repo.get('id'),
|
||||
full_name=repo.get('full_name'),
|
||||
stargazers_count=repo.get('stargazers_count'),
|
||||
git_provider=ProviderType.GITHUB,
|
||||
is_public=not repo.get('private', True),
|
||||
)
|
||||
|
||||
|
||||
github_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITHUB_SERVICE_CLS',
|
||||
|
||||
@@ -382,6 +382,22 @@ class GitLabService(BaseGitService, GitService):
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def get_repository_details_from_repo_name(
|
||||
self, repository: str
|
||||
) -> Repository:
|
||||
encoded_name = repository.replace('/', '%2F')
|
||||
|
||||
url = f'{self.BASE_URL}/projects/{encoded_name}'
|
||||
repo, _ = await self._make_request(url)
|
||||
|
||||
return Repository(
|
||||
id=repo.get('id'),
|
||||
full_name=repo.get('path_with_namespace'),
|
||||
stargazers_count=repo.get('star_count'),
|
||||
git_provider=ProviderType.GITLAB,
|
||||
is_public=repo.get('visibility') == 'public',
|
||||
)
|
||||
|
||||
|
||||
gitlab_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITLAB_SERVICE_CLS',
|
||||
|
||||
@@ -397,3 +397,22 @@ class ProviderHandler:
|
||||
Map ProviderType value to the environment variable name in the runtime
|
||||
"""
|
||||
return f'{provider.value}_token'.lower()
|
||||
|
||||
async def verify_repo_provider(
|
||||
self, repository: str, specified_provider: ProviderType | None = None
|
||||
):
|
||||
if specified_provider:
|
||||
try:
|
||||
service = self._get_service(specified_provider)
|
||||
return await service.get_repository_details_from_repo_name(repository)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for provider in self.provider_tokens:
|
||||
try:
|
||||
service = self._get_service(provider)
|
||||
return await service.get_repository_details_from_repo_name(repository)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise AuthenticationError(f'Unable to access repo {repository}')
|
||||
|
||||
@@ -206,3 +206,8 @@ class GitService(Protocol):
|
||||
async def get_suggested_tasks(self) -> list[SuggestedTask]:
|
||||
"""Get suggested tasks for the authenticated user across all repositories"""
|
||||
...
|
||||
|
||||
async def get_repository_details_from_repo_name(
|
||||
self, repository: str
|
||||
) -> Repository:
|
||||
"""Gets all repository details from repository name"""
|
||||
|
||||
@@ -3,4 +3,4 @@ Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retriev
|
||||
Check out the branch from that {{ requestVerb }} and look at the diff versus the base branch of the {{ requestTypeShort }} to understand the {{ requestTypeShort }}'s intention.
|
||||
Then use the {{ apiName }} to look at the {{ ciSystem }} that are failing on the most recent commit. Try and reproduce the failure locally.
|
||||
Get things working locally, then push your changes. Sleep for 30 seconds at a time until the {{ ciProvider }} {{ ciSystem.lower() }} have run again.
|
||||
If they are still failing, repeat the process.
|
||||
If they are still failing, repeat the process.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You are working on {{ requestType }} #{{ issue_number }} in repository {{ repo }}. You need to fix the merge conflicts.
|
||||
Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retrieve the {{ requestTypeShort }} details.
|
||||
Check out the branch from that {{ requestVerb }} and look at the diff versus the base branch of the {{ requestTypeShort }} to understand the {{ requestTypeShort }}'s intention.
|
||||
Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly.
|
||||
Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You are working on Issue #{{ issue_number }} in repository {{ repo }}. Your goal is to fix the issue.
|
||||
Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retrieve the issue details and any comments on the issue.
|
||||
Then check out a new branch and investigate what changes will need to be made.
|
||||
Finally, make the required changes and open up a {{ requestVerb }}. Be sure to reference the issue in the {{ requestTypeShort }} description.
|
||||
Finally, make the required changes and open up a {{ requestVerb }}. Be sure to reference the issue in the {{ requestTypeShort }} description.
|
||||
|
||||
@@ -2,4 +2,4 @@ You are working on {{ requestType }} #{{ issue_number }} in repository {{ repo }
|
||||
Use the {{ apiName }} with the {{ tokenEnvVar }} environment variable to retrieve the {{ requestTypeShort }} details.
|
||||
Check out the branch from that {{ requestVerb }} and look at the diff versus the base branch of the {{ requestTypeShort }} to understand the {{ requestTypeShort }}'s intention.
|
||||
Then use the {{ apiName }} to retrieve all the feedback on the {{ requestTypeShort }} so far.
|
||||
If anything hasn't been addressed, address it and commit your changes back to the same branch.
|
||||
If anything hasn't been addressed, address it and commit your changes back to the same branch.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from openhands.mcp.client import MCPClient
|
||||
from openhands.mcp.tool import (
|
||||
BaseTool,
|
||||
MCPClientTool,
|
||||
)
|
||||
from openhands.mcp.tool import MCPClientTool
|
||||
from openhands.mcp.utils import (
|
||||
add_mcp_tools_to_agent,
|
||||
call_tool_mcp,
|
||||
convert_mcp_clients_to_tools,
|
||||
create_mcp_clients,
|
||||
@@ -14,8 +12,8 @@ __all__ = [
|
||||
'MCPClient',
|
||||
'convert_mcp_clients_to_tools',
|
||||
'create_mcp_clients',
|
||||
'BaseTool',
|
||||
'MCPClientTool',
|
||||
'fetch_mcp_tools_from_config',
|
||||
'call_tool_mcp',
|
||||
'add_mcp_tools_to_agent',
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ from mcp.client.sse import sse_client
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.mcp.tool import BaseTool, MCPClientTool
|
||||
from openhands.mcp.tool import MCPClientTool
|
||||
|
||||
|
||||
class MCPClient(BaseModel):
|
||||
@@ -18,13 +18,15 @@ class MCPClient(BaseModel):
|
||||
session: Optional[ClientSession] = None
|
||||
exit_stack: AsyncExitStack = AsyncExitStack()
|
||||
description: str = 'MCP client tools for server interaction'
|
||||
tools: List[BaseTool] = Field(default_factory=list)
|
||||
tool_map: Dict[str, BaseTool] = Field(default_factory=dict)
|
||||
tools: List[MCPClientTool] = Field(default_factory=list)
|
||||
tool_map: Dict[str, MCPClientTool] = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def connect_sse(self, server_url: str, timeout: float = 30.0) -> None:
|
||||
async def connect_sse(
|
||||
self, server_url: str, api_key: str | None = None, timeout: float = 30.0
|
||||
) -> None:
|
||||
"""Connect to an MCP server using SSE transport.
|
||||
|
||||
Args:
|
||||
@@ -41,7 +43,8 @@ class MCPClient(BaseModel):
|
||||
async def connect_with_timeout():
|
||||
streams_context = sse_client(
|
||||
url=server_url,
|
||||
timeout=timeout, # Pass the timeout to sse_client
|
||||
headers={'Authorization': f'Bearer {api_key}'} if api_key else None,
|
||||
timeout=timeout,
|
||||
)
|
||||
streams = await self.exit_stack.enter_async_context(streams_context)
|
||||
self.session = await self.exit_stack.enter_async_context(
|
||||
@@ -92,7 +95,10 @@ class MCPClient(BaseModel):
|
||||
"""Call a tool on the MCP server."""
|
||||
if tool_name not in self.tool_map:
|
||||
raise ValueError(f'Tool {tool_name} not found.')
|
||||
return await self.tool_map[tool_name].execute(**args)
|
||||
# The MCPClientTool is primarily for metadata; use the session to call the actual tool.
|
||||
if not self.session:
|
||||
raise RuntimeError('Client session is not available.')
|
||||
return await self.session.call_tool(name=tool_name, arguments=args)
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the MCP server and clean up resources."""
|
||||
|
||||
@@ -1,54 +1,26 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.types import CallToolResult, TextContent, Tool
|
||||
from mcp.types import Tool
|
||||
|
||||
|
||||
class BaseTool(ABC, Tool):
|
||||
@classmethod
|
||||
def postfix(cls) -> str:
|
||||
return '_mcp_tool_call'
|
||||
class MCPClientTool(Tool):
|
||||
"""
|
||||
Represents a tool proxy that can be called on the MCP server from the client side.
|
||||
|
||||
This version doesn't store a session reference, as sessions are created on-demand
|
||||
by the MCPClient for each operation.
|
||||
"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, **kwargs) -> CallToolResult:
|
||||
"""Execute the tool with given parameters."""
|
||||
|
||||
def to_param(self) -> Dict:
|
||||
"""Convert tool to function call format."""
|
||||
return {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': self.name + self.postfix(),
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'parameters': self.inputSchema,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class MCPClientTool(BaseTool):
|
||||
"""Represents a tool proxy that can be called on the MCP server from the client side."""
|
||||
|
||||
session: Optional[ClientSession] = None
|
||||
|
||||
async def execute(self, **kwargs) -> CallToolResult:
|
||||
"""Execute the tool by making a remote call to the MCP server."""
|
||||
if not self.session:
|
||||
return CallToolResult(
|
||||
content=[TextContent(text='Not connected to MCP server', type='text')],
|
||||
isError=True,
|
||||
)
|
||||
|
||||
try:
|
||||
result = await self.session.call_tool(self.name, kwargs)
|
||||
return result
|
||||
except Exception as e:
|
||||
return CallToolResult(
|
||||
content=[
|
||||
TextContent(text=f'Error executing tool: {str(e)}', type='text')
|
||||
],
|
||||
isError=True,
|
||||
)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from openhands.core.config.mcp_config import MCPConfig
|
||||
if TYPE_CHECKING:
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
from openhands.core.config.mcp_config import MCPConfig, MCPSSEServerConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.observation.mcp import MCPObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.mcp.client import MCPClient
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[dict]:
|
||||
@@ -38,19 +43,19 @@ def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[di
|
||||
|
||||
|
||||
async def create_mcp_clients(
|
||||
mcp_servers: list[str],
|
||||
sse_servers: list[MCPSSEServerConfig],
|
||||
) -> list[MCPClient]:
|
||||
mcp_clients: list[MCPClient] = []
|
||||
# Initialize SSE connections
|
||||
if mcp_servers:
|
||||
for server_url in mcp_servers:
|
||||
if sse_servers:
|
||||
for server_url in sse_servers:
|
||||
logger.info(
|
||||
f'Initializing MCP agent for {server_url} with SSE connection...'
|
||||
)
|
||||
|
||||
client = MCPClient()
|
||||
try:
|
||||
await client.connect_sse(server_url)
|
||||
await client.connect_sse(server_url.url, api_key=server_url.api_key)
|
||||
# Only add the client to the list after a successful connection
|
||||
mcp_clients.append(client)
|
||||
logger.info(f'Connected to MCP server {server_url} via SSE')
|
||||
@@ -77,14 +82,16 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]:
|
||||
mcp_tools = []
|
||||
try:
|
||||
logger.debug(f'Creating MCP clients with config: {mcp_config}')
|
||||
# Create clients - this will fetch tools but not maintain active connections
|
||||
mcp_clients = await create_mcp_clients(
|
||||
mcp_config.mcp_servers,
|
||||
mcp_config.sse_servers,
|
||||
)
|
||||
|
||||
if not mcp_clients:
|
||||
logger.debug('No MCP clients were successfully connected')
|
||||
return []
|
||||
|
||||
# Convert tools to the format expected by the agent
|
||||
mcp_tools = convert_mcp_clients_to_tools(mcp_clients)
|
||||
|
||||
# Always disconnect clients to clean up resources
|
||||
@@ -93,6 +100,7 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]:
|
||||
await mcp_client.disconnect()
|
||||
except Exception as disconnect_error:
|
||||
logger.error(f'Error disconnecting MCP client: {str(disconnect_error)}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching MCP tools: {str(e)}')
|
||||
return []
|
||||
@@ -101,13 +109,13 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]:
|
||||
return mcp_tools
|
||||
|
||||
|
||||
async def call_tool_mcp(mcp_clients: list[MCPClient], action: McpAction) -> Observation:
|
||||
async def call_tool_mcp(mcp_clients: list[MCPClient], action: MCPAction) -> Observation:
|
||||
"""
|
||||
Call a tool on an MCP server and return the observation.
|
||||
|
||||
Args:
|
||||
mcp_clients: The list of MCP clients to execute the action on
|
||||
action: The MCP action to execute
|
||||
sse_mcp_servers: List of SSE MCP server URLs
|
||||
|
||||
Returns:
|
||||
The observation from the MCP server
|
||||
@@ -116,20 +124,55 @@ async def call_tool_mcp(mcp_clients: list[MCPClient], action: McpAction) -> Obse
|
||||
raise ValueError('No MCP clients found')
|
||||
|
||||
logger.debug(f'MCP action received: {action}')
|
||||
# Find the MCP agent that has the matching tool name
|
||||
|
||||
# Find the MCP client that has the matching tool name
|
||||
matching_client = None
|
||||
logger.debug(f'MCP clients: {mcp_clients}')
|
||||
logger.debug(f'MCP action name: {action.name}')
|
||||
|
||||
for client in mcp_clients:
|
||||
logger.debug(f'MCP client tools: {client.tools}')
|
||||
if action.name in [tool.name for tool in client.tools]:
|
||||
matching_client = client
|
||||
break
|
||||
|
||||
if matching_client is None:
|
||||
raise ValueError(f'No matching MCP agent found for tool name: {action.name}')
|
||||
|
||||
logger.debug(f'Matching client: {matching_client}')
|
||||
args_dict = json.loads(action.arguments) if action.arguments else {}
|
||||
response = await matching_client.call_tool(action.name, args_dict)
|
||||
|
||||
# Call the tool - this will create a new connection internally
|
||||
response = await matching_client.call_tool(action.name, action.arguments)
|
||||
logger.debug(f'MCP response: {response}')
|
||||
|
||||
return MCPObservation(content=f'MCP result:{response.model_dump(mode="json")}')
|
||||
return MCPObservation(content=json.dumps(response.model_dump(mode='json')))
|
||||
|
||||
|
||||
async def add_mcp_tools_to_agent(
|
||||
agent: 'Agent', runtime: Runtime, mcp_config: MCPConfig
|
||||
):
|
||||
"""
|
||||
Add MCP tools to an agent.
|
||||
"""
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient, # inline import to avoid circular import
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
runtime, ActionExecutionClient
|
||||
), 'Runtime must be an instance of ActionExecutionClient'
|
||||
assert (
|
||||
runtime.runtime_initialized
|
||||
), 'Runtime must be initialized before adding MCP tools'
|
||||
|
||||
# Add the runtime as another MCP server
|
||||
updated_mcp_config = runtime.get_updated_mcp_config()
|
||||
# Fetch the MCP tools
|
||||
mcp_tools = await fetch_mcp_tools_from_config(updated_mcp_config)
|
||||
|
||||
logger.info(
|
||||
f"Loaded {len(mcp_tools)} MCP tools: {[tool['function']['name'] for tool in mcp_tools]}"
|
||||
)
|
||||
|
||||
# Set the MCP tools on the agent
|
||||
agent.set_mcp_tools(mcp_tools)
|
||||
|
||||
@@ -19,7 +19,7 @@ from openhands.events.action import (
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.action.message import SystemMessageAction
|
||||
from openhands.events.event import Event, RecallType
|
||||
from openhands.events.observation import (
|
||||
@@ -184,7 +184,7 @@ class ConversationMemory:
|
||||
- BrowseInteractiveAction: For browsing the web
|
||||
- AgentFinishAction: For ending the interaction
|
||||
- MessageAction: For sending messages
|
||||
- McpAction: For interacting with the MCP server
|
||||
- MCPAction: For interacting with the MCP server
|
||||
pending_tool_call_action_messages: Dictionary mapping response IDs to their corresponding messages.
|
||||
Used in function calling mode to track tool calls that are waiting for their results.
|
||||
|
||||
@@ -210,7 +210,7 @@ class ConversationMemory:
|
||||
FileReadAction,
|
||||
BrowseInteractiveAction,
|
||||
BrowseURLAction,
|
||||
McpAction,
|
||||
MCPAction,
|
||||
),
|
||||
) or (isinstance(action, CmdRunAction) and action.source == 'agent'):
|
||||
tool_metadata = action.tool_call_metadata
|
||||
@@ -412,7 +412,7 @@ class ConversationMemory:
|
||||
logger.debug('Vision disabled for browsing, showing text')
|
||||
elif isinstance(obs, AgentDelegateObservation):
|
||||
text = truncate_content(
|
||||
obs.outputs['content'] if 'content' in obs.outputs else '',
|
||||
obs.outputs.get('content', obs.content),
|
||||
max_message_chars,
|
||||
)
|
||||
message = Message(role='user', content=[TextContent(text=text)])
|
||||
|
||||
@@ -8,6 +8,8 @@ NOTE: this will be executed inside the docker sandbox.
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
@@ -23,6 +25,8 @@ from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.security import APIKeyHeader
|
||||
from mcpm import MCPRouter, RouterConfig
|
||||
from mcpm.router.router import logger as mcp_router_logger
|
||||
from openhands_aci.editor.editor import OHEditor
|
||||
from openhands_aci.editor.exceptions import ToolError
|
||||
from openhands_aci.editor.results import ToolResult
|
||||
@@ -68,6 +72,9 @@ from openhands.runtime.utils.runtime_init import init_user_and_working_directory
|
||||
from openhands.runtime.utils.system_stats import get_system_stats
|
||||
from openhands.utils.async_utils import call_sync_from_async, wait_all
|
||||
|
||||
# Set MCP router logger to the same level as the main logger
|
||||
mcp_router_logger.setLevel(logger.getEffectiveLevel())
|
||||
|
||||
|
||||
class ActionRequest(BaseModel):
|
||||
action: dict
|
||||
@@ -572,10 +579,15 @@ if __name__ == '__main__':
|
||||
plugins_to_load.append(ALL_PLUGINS[plugin]()) # type: ignore
|
||||
|
||||
client: ActionExecutor | None = None
|
||||
mcp_router: MCPRouter | None = None
|
||||
MCP_ROUTER_PROFILE_PATH = os.path.join(
|
||||
os.path.dirname(__file__), 'mcp', 'config.json'
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global client
|
||||
global client, mcp_router
|
||||
logger.info('Initializing ActionExecutor...')
|
||||
client = ActionExecutor(
|
||||
plugins_to_load,
|
||||
work_dir=args.working_dir,
|
||||
@@ -584,9 +596,70 @@ if __name__ == '__main__':
|
||||
browsergym_eval_env=args.browsergym_eval_env,
|
||||
)
|
||||
await client.ainit()
|
||||
logger.info('ActionExecutor initialized.')
|
||||
|
||||
# Initialize and mount MCP Router
|
||||
logger.info('Initializing MCP Router...')
|
||||
mcp_router = MCPRouter(
|
||||
profile_path=MCP_ROUTER_PROFILE_PATH,
|
||||
router_config=RouterConfig(
|
||||
api_key=SESSION_API_KEY,
|
||||
auth_enabled=bool(SESSION_API_KEY),
|
||||
),
|
||||
)
|
||||
allowed_origins = ['*']
|
||||
sse_app = await mcp_router.get_sse_server_app(
|
||||
allow_origins=allowed_origins, include_lifespan=False
|
||||
)
|
||||
|
||||
# Check for route conflicts before mounting
|
||||
main_app_routes = {route.path for route in app.routes}
|
||||
sse_app_routes = {route.path for route in sse_app.routes}
|
||||
conflicting_routes = main_app_routes.intersection(sse_app_routes)
|
||||
|
||||
if conflicting_routes:
|
||||
logger.error(f'Route conflicts detected: {conflicting_routes}')
|
||||
raise RuntimeError(
|
||||
f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}'
|
||||
)
|
||||
|
||||
app.mount('/', sse_app)
|
||||
logger.info(
|
||||
f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}'
|
||||
)
|
||||
|
||||
# Additional debug logging
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug('Main app routes:')
|
||||
for route in main_app_routes:
|
||||
logger.debug(f' {route}')
|
||||
logger.debug('MCP SSE server app routes:')
|
||||
for route in sse_app_routes:
|
||||
logger.debug(f' {route}')
|
||||
|
||||
yield
|
||||
|
||||
# Clean up & release the resources
|
||||
client.close()
|
||||
logger.info('Shutting down MCP Router...')
|
||||
if mcp_router:
|
||||
try:
|
||||
await mcp_router.shutdown()
|
||||
logger.info('MCP Router shutdown successfully.')
|
||||
except Exception as e:
|
||||
logger.error(f'Error shutting down MCP Router: {e}', exc_info=True)
|
||||
else:
|
||||
logger.info('MCP Router instance not found for shutdown.')
|
||||
|
||||
logger.info('Closing ActionExecutor...')
|
||||
if client:
|
||||
try:
|
||||
client.close()
|
||||
logger.info('ActionExecutor closed successfully.')
|
||||
except Exception as e:
|
||||
logger.error(f'Error closing ActionExecutor: {e}', exc_info=True)
|
||||
else:
|
||||
logger.info('ActionExecutor instance not found for closing.')
|
||||
logger.info('Shutdown complete.')
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
@@ -663,6 +736,51 @@ if __name__ == '__main__':
|
||||
detail=traceback.format_exc(),
|
||||
)
|
||||
|
||||
@app.post('/update_mcp_server')
|
||||
async def update_mcp_server(request: Request):
|
||||
assert mcp_router is not None
|
||||
assert os.path.exists(MCP_ROUTER_PROFILE_PATH)
|
||||
|
||||
# Use synchronous file operations outside of async function
|
||||
def read_profile():
|
||||
with open(MCP_ROUTER_PROFILE_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
current_profile = read_profile()
|
||||
assert 'default' in current_profile
|
||||
assert isinstance(current_profile['default'], list)
|
||||
|
||||
# Get the request body
|
||||
mcp_tools_to_sync = await request.json()
|
||||
if not isinstance(mcp_tools_to_sync, list):
|
||||
raise HTTPException(
|
||||
status_code=400, detail='Request must be a list of MCP tools to sync'
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'Updating MCP server to: {json.dumps(mcp_tools_to_sync, indent=2)}.\nPrevious profile: {json.dumps(current_profile, indent=2)}'
|
||||
)
|
||||
current_profile['default'] = mcp_tools_to_sync
|
||||
|
||||
# Use synchronous file operations outside of async function
|
||||
def write_profile(profile):
|
||||
with open(MCP_ROUTER_PROFILE_PATH, 'w') as f:
|
||||
json.dump(profile, f)
|
||||
|
||||
write_profile(current_profile)
|
||||
|
||||
# Manually reload the profile and update the servers
|
||||
mcp_router.profile_manager.reload()
|
||||
servers_wait_for_update = mcp_router.get_unique_servers()
|
||||
await mcp_router.update_servers(servers_wait_for_update)
|
||||
logger.info(
|
||||
f'MCP router updated successfully with unique servers: {servers_wait_for_update}'
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200, content={'detail': 'MCP server updated successfully'}
|
||||
)
|
||||
|
||||
@app.post('/upload_file')
|
||||
async def upload_file(
|
||||
file: UploadFile, destination: str = '/', recursive: bool = False
|
||||
|
||||
@@ -30,7 +30,7 @@ from openhands.events.action import (
|
||||
FileWriteAction,
|
||||
IPythonRunCellAction,
|
||||
)
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import (
|
||||
AgentThinkObservation,
|
||||
@@ -47,7 +47,7 @@ from openhands.integrations.provider import (
|
||||
ProviderHandler,
|
||||
ProviderType,
|
||||
)
|
||||
from openhands.integrations.service_types import Repository
|
||||
from openhands.integrations.service_types import AuthenticationError
|
||||
from openhands.microagent import (
|
||||
BaseMicroagent,
|
||||
load_microagents_from_dir,
|
||||
@@ -282,9 +282,8 @@ class Runtime(FileEditRuntimeMixin):
|
||||
assert event.timeout is not None
|
||||
try:
|
||||
await self._export_latest_git_provider_tokens(event)
|
||||
if isinstance(event, McpAction):
|
||||
# we don't call call_tool_mcp impl directly because there can be other action ActionExecutionClient
|
||||
observation: Observation = await getattr(self, McpAction.action)(event)
|
||||
if isinstance(event, MCPAction):
|
||||
observation: Observation = await self.call_tool_mcp(event)
|
||||
else:
|
||||
observation = await call_sync_from_async(self.run_action, event)
|
||||
except Exception as e:
|
||||
@@ -312,10 +311,23 @@ class Runtime(FileEditRuntimeMixin):
|
||||
async def clone_or_init_repo(
|
||||
self,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
|
||||
selected_repository: str | Repository | None,
|
||||
selected_repository: str | None,
|
||||
selected_branch: str | None,
|
||||
repository_provider: ProviderType = ProviderType.GITHUB,
|
||||
) -> str:
|
||||
repository = None
|
||||
if selected_repository: # Determine provider from repo name
|
||||
try:
|
||||
provider_handler = ProviderHandler(
|
||||
git_provider_tokens or MappingProxyType({})
|
||||
)
|
||||
repository = await provider_handler.verify_repo_provider(
|
||||
selected_repository
|
||||
)
|
||||
except AuthenticationError:
|
||||
raise RuntimeError(
|
||||
'Git provider authentication issue when cloning repo'
|
||||
)
|
||||
|
||||
if not selected_repository:
|
||||
# In SaaS mode (indicated by user_id being set), always run git init
|
||||
# In OSS mode, only run git init if workspace_base is not set
|
||||
@@ -333,36 +345,30 @@ class Runtime(FileEditRuntimeMixin):
|
||||
)
|
||||
return ''
|
||||
|
||||
# This satisfies mypy because param is optional, but `verify_repo_provider` guarentees this gets populated
|
||||
if not repository:
|
||||
return ''
|
||||
|
||||
provider = repository.git_provider
|
||||
provider_domains = {
|
||||
ProviderType.GITHUB: 'github.com',
|
||||
ProviderType.GITLAB: 'gitlab.com',
|
||||
}
|
||||
|
||||
chosen_provider = (
|
||||
repository_provider
|
||||
if isinstance(selected_repository, str)
|
||||
else selected_repository.git_provider
|
||||
)
|
||||
|
||||
domain = provider_domains[chosen_provider]
|
||||
repository = (
|
||||
selected_repository
|
||||
if isinstance(selected_repository, str)
|
||||
else selected_repository.full_name
|
||||
)
|
||||
domain = provider_domains[provider]
|
||||
|
||||
# Try to use token if available, otherwise use public URL
|
||||
if git_provider_tokens and chosen_provider in git_provider_tokens:
|
||||
git_token = git_provider_tokens[chosen_provider].token
|
||||
if git_provider_tokens and provider in git_provider_tokens:
|
||||
git_token = git_provider_tokens[provider].token
|
||||
if git_token:
|
||||
if chosen_provider == ProviderType.GITLAB:
|
||||
remote_repo_url = f'https://oauth2:{git_token.get_secret_value()}@{domain}/{repository}.git'
|
||||
if provider == ProviderType.GITLAB:
|
||||
remote_repo_url = f'https://oauth2:{git_token.get_secret_value()}@{domain}/{selected_repository}.git'
|
||||
else:
|
||||
remote_repo_url = f'https://{git_token.get_secret_value()}@{domain}/{repository}.git'
|
||||
remote_repo_url = f'https://{git_token.get_secret_value()}@{domain}/{selected_repository}.git'
|
||||
else:
|
||||
remote_repo_url = f'https://{domain}/{repository}.git'
|
||||
remote_repo_url = f'https://{domain}/{selected_repository}.git'
|
||||
else:
|
||||
remote_repo_url = f'https://{domain}/{repository}.git'
|
||||
remote_repo_url = f'https://{domain}/{selected_repository}.git'
|
||||
|
||||
if not remote_repo_url:
|
||||
raise ValueError('Missing either Git token or valid repository')
|
||||
@@ -372,7 +378,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
'info', 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...'
|
||||
)
|
||||
|
||||
dir_name = repository.split('/')[-1]
|
||||
dir_name = selected_repository.split('/')[-1]
|
||||
|
||||
# Generate a random branch name to avoid conflicts
|
||||
random_str = ''.join(
|
||||
@@ -571,7 +577,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def call_tool_mcp(self, action: McpAction) -> Observation:
|
||||
async def call_tool_mcp(self, action: MCPAction) -> Observation:
|
||||
pass
|
||||
|
||||
# ====================================================================
|
||||
|
||||
@@ -10,6 +10,7 @@ import httpx
|
||||
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.core.config.mcp_config import MCPConfig, MCPSSEServerConfig
|
||||
from openhands.core.exceptions import (
|
||||
AgentRuntimeTimeoutError,
|
||||
)
|
||||
@@ -27,7 +28,7 @@ from openhands.events.action import (
|
||||
)
|
||||
from openhands.events.action.action import Action
|
||||
from openhands.events.action.files import FileEditSource
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.observation import (
|
||||
AgentThinkObservation,
|
||||
ErrorObservation,
|
||||
@@ -38,12 +39,9 @@ from openhands.events.observation import (
|
||||
from openhands.events.serialization import event_to_dict, observation_from_dict
|
||||
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.mcp import MCPClient, create_mcp_clients
|
||||
from openhands.mcp import call_tool_mcp as call_tool_mcp_handler
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.utils.request import send_request
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
from openhands.utils.http_session import HttpSession
|
||||
from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
@@ -79,7 +77,6 @@ class ActionExecutionClient(Runtime):
|
||||
self._runtime_initialized: bool = False
|
||||
self._runtime_closed: bool = False
|
||||
self._vscode_token: str | None = None # initial dummy value
|
||||
self.mcp_clients: list[MCPClient] | None = None
|
||||
super().__init__(
|
||||
config,
|
||||
event_stream,
|
||||
@@ -329,19 +326,60 @@ class ActionExecutionClient(Runtime):
|
||||
def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
|
||||
return self.send_action_for_execution(action)
|
||||
|
||||
async def call_tool_mcp(self, action: McpAction) -> Observation:
|
||||
if self.mcp_clients is None:
|
||||
self.log(
|
||||
'debug',
|
||||
f'Creating MCP clients with servers: {self.config.mcp.mcp_servers}',
|
||||
)
|
||||
self.mcp_clients = await create_mcp_clients(self.config.mcp.mcp_servers)
|
||||
return await call_tool_mcp_handler(self.mcp_clients, action)
|
||||
def get_updated_mcp_config(self) -> MCPConfig:
|
||||
# Add the runtime as another MCP server
|
||||
updated_mcp_config = self.config.mcp.model_copy()
|
||||
# Send a request to the action execution server to updated MCP config
|
||||
stdio_tools = [
|
||||
server.model_dump(mode='json')
|
||||
for server in updated_mcp_config.stdio_servers
|
||||
]
|
||||
self.log('debug', f'Updating MCP server to: {stdio_tools}')
|
||||
response = self._send_action_server_request(
|
||||
'POST',
|
||||
f'{self.action_execution_server_url}/update_mcp_server',
|
||||
json=stdio_tools,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(f'Failed to update MCP server: {response.text}')
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self.mcp_clients:
|
||||
for client in self.mcp_clients:
|
||||
await client.disconnect()
|
||||
# No API key by default. Child runtime can override this when appropriate
|
||||
updated_mcp_config.sse_servers.append(
|
||||
MCPSSEServerConfig(
|
||||
url=self.action_execution_server_url.rstrip('/') + '/sse', api_key=None
|
||||
)
|
||||
)
|
||||
self.log(
|
||||
'debug',
|
||||
f'Updated MCP config by adding runtime as another server: {updated_mcp_config}',
|
||||
)
|
||||
return updated_mcp_config
|
||||
|
||||
async def call_tool_mcp(self, action: MCPAction) -> Observation:
|
||||
# Import here to avoid circular imports
|
||||
from openhands.mcp.utils import call_tool_mcp as call_tool_mcp_handler
|
||||
from openhands.mcp.utils import create_mcp_clients
|
||||
|
||||
# Get the updated MCP config
|
||||
updated_mcp_config = self.get_updated_mcp_config()
|
||||
self.log(
|
||||
'debug',
|
||||
f'Creating MCP clients with servers: {updated_mcp_config.sse_servers}',
|
||||
)
|
||||
|
||||
# Create clients for this specific operation
|
||||
mcp_clients = await create_mcp_clients(updated_mcp_config.sse_servers)
|
||||
|
||||
# Call the tool and return the result
|
||||
# No need for try/finally since disconnect() is now just resetting state
|
||||
result = await call_tool_mcp_handler(mcp_clients, action)
|
||||
|
||||
# Reset client state (no active connections to worry about)
|
||||
for client in mcp_clients:
|
||||
await client.disconnect()
|
||||
|
||||
return result
|
||||
|
||||
def close(self) -> None:
|
||||
# Make sure we don't close the session multiple times
|
||||
@@ -350,4 +388,3 @@ class ActionExecutionClient(Runtime):
|
||||
return
|
||||
self._runtime_closed = True
|
||||
self.session.close()
|
||||
call_async_from_sync(self.aclose)
|
||||
|
||||
@@ -272,14 +272,23 @@ class DockerRuntime(ActionExecutionClient):
|
||||
self.config.workspace_mount_path is not None
|
||||
and self.config.workspace_mount_path_in_sandbox is not None
|
||||
):
|
||||
# Determine the mount mode
|
||||
mount_mode = 'rw' # Default mode
|
||||
|
||||
# If runtime_mount is set, extract the mode from it
|
||||
if self.config.runtime_mount is not None:
|
||||
parts = self.config.runtime_mount.split(':')
|
||||
if len(parts) > 2:
|
||||
mount_mode = parts[2]
|
||||
|
||||
# e.g. result would be: {"/home/user/openhands/workspace": {'bind': "/workspace", 'mode': 'rw'}}
|
||||
volumes = {
|
||||
self.config.workspace_mount_path: {
|
||||
'bind': self.config.workspace_mount_path_in_sandbox,
|
||||
'mode': 'rw',
|
||||
'mode': mount_mode,
|
||||
}
|
||||
}
|
||||
logger.debug(f'Mount dir: {self.config.workspace_mount_path}')
|
||||
logger.debug(f'Mount dir: {self.config.workspace_mount_path} with mode: {mount_mode}')
|
||||
else:
|
||||
logger.debug(
|
||||
'Mount dir is not set, will not mount the workspace directory to the container'
|
||||
|
||||
3
openhands/runtime/mcp/config.json
Normal file
3
openhands/runtime/mcp/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"default": []
|
||||
}
|
||||
@@ -34,10 +34,12 @@ RUN apt-get update && \
|
||||
{% if 'ubuntu' in base_image %}
|
||||
RUN ln -s "$(dirname $(which node))/corepack" /usr/local/bin/corepack && \
|
||||
npm install -g corepack && corepack enable yarn && \
|
||||
curl -fsSL --compressed https://install.python-poetry.org | python - && \
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
curl -fsSL --compressed https://install.python-poetry.org | python -
|
||||
{% endif %}
|
||||
|
||||
# Install uv (required by MCP)
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts
|
||||
RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \
|
||||
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi)
|
||||
|
||||
@@ -10,8 +10,8 @@ from openhands.events.event_store import EventStore
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.monitoring import MonitoringListener
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ from openhands.server.monitoring import MonitoringListener
|
||||
from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.server.session.session import ROOM_KEY, Session
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync, wait_all
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
@@ -12,8 +12,13 @@ from openhands.events.event import EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import (
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
ProviderHandler,
|
||||
)
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
ProviderType,
|
||||
SuggestedTask,
|
||||
)
|
||||
from openhands.integrations.service_types import Repository, SuggestedTask
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.server.data_models.conversation_info import ConversationInfo
|
||||
from openhands.server.data_models.conversation_info_result_set import (
|
||||
@@ -29,9 +34,11 @@ from openhands.server.shared import (
|
||||
)
|
||||
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
|
||||
from openhands.server.user_auth import (
|
||||
get_auth_type,
|
||||
get_provider_tokens,
|
||||
get_user_id,
|
||||
)
|
||||
from openhands.server.user_auth.user_auth import AuthType
|
||||
from openhands.server.utils import get_conversation_store
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.storage.data_models.conversation_metadata import (
|
||||
@@ -42,24 +49,24 @@ from openhands.storage.data_models.conversation_status import ConversationStatus
|
||||
from openhands.utils.async_utils import wait_all
|
||||
from openhands.utils.conversation_summary import generate_conversation_title
|
||||
|
||||
|
||||
app = APIRouter(prefix='/api')
|
||||
|
||||
|
||||
class InitSessionRequest(BaseModel):
|
||||
conversation_trigger: ConversationTrigger = ConversationTrigger.GUI
|
||||
selected_repository: Repository | None = None
|
||||
repository: str | None = None
|
||||
git_provider: ProviderType | None = None
|
||||
selected_branch: str | None = None
|
||||
initial_user_msg: str | None = None
|
||||
image_urls: list[str] | None = None
|
||||
replay_json: str | None = None
|
||||
suggested_task: SuggestedTask | None = None
|
||||
|
||||
|
||||
|
||||
async def _create_new_conversation(
|
||||
user_id: str | None,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
|
||||
selected_repository: Repository | None,
|
||||
selected_repository: str | None,
|
||||
selected_branch: str | None,
|
||||
initial_user_msg: str | None,
|
||||
image_urls: list[str] | None,
|
||||
@@ -67,10 +74,13 @@ async def _create_new_conversation(
|
||||
conversation_trigger: ConversationTrigger = ConversationTrigger.GUI,
|
||||
attach_convo_id: bool = False,
|
||||
):
|
||||
print("trigger", conversation_trigger)
|
||||
logger.info(
|
||||
'Creating conversation',
|
||||
extra={'signal': 'create_conversation', 'user_id': user_id, 'trigger': conversation_trigger.value},
|
||||
extra={
|
||||
'signal': 'create_conversation',
|
||||
'user_id': user_id,
|
||||
'trigger': conversation_trigger.value,
|
||||
},
|
||||
)
|
||||
logger.info('Loading settings')
|
||||
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
|
||||
@@ -122,9 +132,7 @@ async def _create_new_conversation(
|
||||
title=conversation_title,
|
||||
user_id=user_id,
|
||||
github_user_id=None,
|
||||
selected_repository=selected_repository.full_name
|
||||
if selected_repository
|
||||
else selected_repository,
|
||||
selected_repository=selected_repository,
|
||||
selected_branch=selected_branch,
|
||||
)
|
||||
)
|
||||
@@ -161,6 +169,7 @@ async def new_conversation(
|
||||
data: InitSessionRequest,
|
||||
user_id: str = Depends(get_user_id),
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
|
||||
auth_type: AuthType | None = Depends(get_auth_type),
|
||||
):
|
||||
"""Initialize a new session or join an existing one.
|
||||
|
||||
@@ -168,29 +177,38 @@ async def new_conversation(
|
||||
using the returned conversation ID.
|
||||
"""
|
||||
logger.info('Initializing new conversation')
|
||||
selected_repository = data.selected_repository
|
||||
repository = data.repository
|
||||
selected_branch = data.selected_branch
|
||||
initial_user_msg = data.initial_user_msg
|
||||
image_urls = data.image_urls or []
|
||||
replay_json = data.replay_json
|
||||
suggested_task = data.suggested_task
|
||||
conversation_trigger = data.conversation_trigger
|
||||
git_provider = data.git_provider
|
||||
|
||||
if suggested_task:
|
||||
initial_user_msg = suggested_task.get_prompt_for_task()
|
||||
conversation_trigger = ConversationTrigger.SUGGESTED_TASK
|
||||
|
||||
if auth_type == AuthType.BEARER:
|
||||
conversation_trigger = ConversationTrigger.REMOTE_API_KEY
|
||||
|
||||
try:
|
||||
if repository:
|
||||
provider_handler = ProviderHandler(provider_tokens)
|
||||
# Check against git_provider, otherwise check all provider apis
|
||||
await provider_handler.verify_repo_provider(repository, git_provider)
|
||||
|
||||
# Create conversation with initial message
|
||||
conversation_id = await _create_new_conversation(
|
||||
user_id=user_id,
|
||||
git_provider_tokens=provider_tokens,
|
||||
selected_repository=selected_repository,
|
||||
selected_repository=repository,
|
||||
selected_branch=selected_branch,
|
||||
initial_user_msg=initial_user_msg,
|
||||
image_urls=image_urls,
|
||||
replay_json=replay_json,
|
||||
conversation_trigger=conversation_trigger
|
||||
conversation_trigger=conversation_trigger,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
@@ -216,6 +234,16 @@ async def new_conversation(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
except AuthenticationError as e:
|
||||
return JSONResponse(
|
||||
content={
|
||||
'status': 'error',
|
||||
'message': str(e),
|
||||
'msg_id': 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR',
|
||||
},
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@app.get('/conversations')
|
||||
async def search_conversations(
|
||||
|
||||
@@ -2,9 +2,8 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from openhands.security.options import SecurityAnalyzers
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.security.options import SecurityAnalyzers
|
||||
from openhands.server.shared import config, server_config
|
||||
from openhands.utils.llm import get_supported_llm_models
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import (
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
ProviderToken,
|
||||
ProviderType,
|
||||
SecretStore,
|
||||
)
|
||||
@@ -17,12 +15,12 @@ from openhands.server.settings import (
|
||||
POSTSettingsModel,
|
||||
)
|
||||
from openhands.server.shared import config
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.server.user_auth import (
|
||||
get_provider_tokens,
|
||||
get_user_settings,
|
||||
get_user_settings_store,
|
||||
)
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.storage.settings.settings_store import SettingsStore
|
||||
|
||||
app = APIRouter(prefix='/api')
|
||||
@@ -40,7 +38,7 @@ async def load_settings(
|
||||
content={'error': 'Settings not found'},
|
||||
)
|
||||
|
||||
provider_tokens_set: dict[ProviderType, str | None] = {}
|
||||
provider_tokens_set: dict[ProviderType, str | None] = {}
|
||||
if provider_tokens:
|
||||
for provider_type, provider_token in provider_tokens.items():
|
||||
if provider_token.token or provider_token.user_id:
|
||||
@@ -210,20 +208,14 @@ async def reset_settings() -> JSONResponse:
|
||||
|
||||
async def check_provider_tokens(settings: POSTSettingsModel) -> str:
|
||||
if settings.provider_tokens:
|
||||
# Remove extraneous token types
|
||||
provider_types = [provider.value for provider in ProviderType]
|
||||
settings.provider_tokens = {
|
||||
k: v for k, v in settings.provider_tokens.items() if k in provider_types
|
||||
}
|
||||
|
||||
# Determine whether tokens are valid
|
||||
for token_type, token_value in settings.provider_tokens.items():
|
||||
if token_value:
|
||||
for provider_type, provider_token in settings.provider_tokens.items():
|
||||
if provider_token.token:
|
||||
confirmed_token_type = await validate_provider_token(
|
||||
SecretStr(token_value)
|
||||
provider_token.token
|
||||
)
|
||||
if not confirmed_token_type or confirmed_token_type.value != token_type:
|
||||
return f'Invalid token. Please make sure it is a valid {token_type} token.'
|
||||
if not confirmed_token_type or confirmed_token_type != provider_type:
|
||||
return f'Invalid token. Please make sure it is a valid {provider_type.value} token.'
|
||||
|
||||
return ''
|
||||
|
||||
@@ -233,32 +225,25 @@ async def store_provider_tokens(
|
||||
):
|
||||
existing_settings = await settings_store.load()
|
||||
if existing_settings:
|
||||
if settings.provider_tokens:
|
||||
if existing_settings.secrets_store:
|
||||
existing_providers = [
|
||||
provider.value
|
||||
for provider in existing_settings.secrets_store.provider_tokens
|
||||
]
|
||||
if existing_settings.secrets_store:
|
||||
existing_providers = [
|
||||
provider for provider in existing_settings.secrets_store.provider_tokens
|
||||
]
|
||||
|
||||
# Merge incoming settings store with the existing one
|
||||
for provider, token_value in list(settings.provider_tokens.items()):
|
||||
if provider in existing_providers and not token_value:
|
||||
provider_type = ProviderType(provider)
|
||||
existing_token = (
|
||||
existing_settings.secrets_store.provider_tokens.get(
|
||||
provider_type
|
||||
)
|
||||
# Merge incoming settings store with the existing one
|
||||
for provider_type, provider_value in list(settings.provider_tokens.items()):
|
||||
if provider_type in existing_providers and not provider_value.token:
|
||||
existing_token = (
|
||||
existing_settings.secrets_store.provider_tokens.get(
|
||||
provider_type
|
||||
)
|
||||
if existing_token and existing_token.token:
|
||||
settings.provider_tokens[provider] = (
|
||||
existing_token.token.get_secret_value()
|
||||
)
|
||||
)
|
||||
if existing_token and existing_token.token:
|
||||
settings.provider_tokens[provider_type] = existing_token
|
||||
|
||||
else: # nothing passed in means keep current settings
|
||||
provider_tokens = existing_settings.secrets_store.provider_tokens
|
||||
settings.provider_tokens = {
|
||||
provider.value: data.token.get_secret_value() if data.token else None
|
||||
for provider, data in provider_tokens.items()
|
||||
}
|
||||
provider_tokens = dict(existing_settings.secrets_store.provider_tokens)
|
||||
settings.provider_tokens = provider_tokens
|
||||
|
||||
return settings
|
||||
|
||||
@@ -347,17 +332,12 @@ def convert_to_settings(settings_with_token_data: POSTSettingsModel) -> Settings
|
||||
|
||||
# Create new provider tokens immutably
|
||||
if settings_with_token_data.provider_tokens:
|
||||
tokens = {}
|
||||
for token_type, token_value in settings_with_token_data.provider_tokens.items():
|
||||
if token_value:
|
||||
provider = ProviderType(token_type)
|
||||
tokens[provider] = ProviderToken(
|
||||
token=SecretStr(token_value), user_id=None
|
||||
)
|
||||
|
||||
# Create new SecretStore with tokens
|
||||
settings = settings.model_copy(
|
||||
update={'secrets_store': SecretStore(provider_tokens=tokens)}
|
||||
update={
|
||||
'secrets_store': SecretStore(
|
||||
provider_tokens=settings_with_token_data.provider_tokens
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return settings
|
||||
|
||||
@@ -17,7 +17,7 @@ from openhands.events.action import ChangeAgentStateAction, MessageAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
|
||||
from openhands.integrations.service_types import Repository
|
||||
from openhands.mcp import add_mcp_tools_to_agent
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroagent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
@@ -85,7 +85,7 @@ class AgentSession:
|
||||
max_budget_per_task: float | None = None,
|
||||
agent_to_llm_config: dict[str, LLMConfig] | None = None,
|
||||
agent_configs: dict[str, AgentConfig] | None = None,
|
||||
selected_repository: Repository | None = None,
|
||||
selected_repository: str | None = None,
|
||||
selected_branch: str | None = None,
|
||||
initial_message: MessageAction | None = None,
|
||||
replay_json: str | None = None,
|
||||
@@ -124,6 +124,11 @@ class AgentSession:
|
||||
selected_branch=selected_branch,
|
||||
)
|
||||
|
||||
# NOTE: this needs to happen before controller is created
|
||||
# so MCP tools can be included into the SystemMessageAction
|
||||
if self.runtime and runtime_connected:
|
||||
await add_mcp_tools_to_agent(agent, self.runtime, config.mcp)
|
||||
|
||||
if replay_json:
|
||||
initial_message = self._run_replay(
|
||||
initial_message,
|
||||
@@ -147,7 +152,8 @@ class AgentSession:
|
||||
|
||||
repo_directory = None
|
||||
if self.runtime and runtime_connected and selected_repository:
|
||||
repo_directory = selected_repository.full_name.split('/')[-1]
|
||||
repo_directory = selected_repository.split('/')[-1]
|
||||
|
||||
self.memory = await self._create_memory(
|
||||
selected_repository=selected_repository,
|
||||
repo_directory=repo_directory,
|
||||
@@ -258,7 +264,7 @@ class AgentSession:
|
||||
config: AppConfig,
|
||||
agent: Agent,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
selected_repository: Repository | None = None,
|
||||
selected_repository: str | None = None,
|
||||
selected_branch: str | None = None,
|
||||
) -> bool:
|
||||
"""Creates a runtime instance
|
||||
@@ -393,7 +399,7 @@ class AgentSession:
|
||||
return controller
|
||||
|
||||
async def _create_memory(
|
||||
self, selected_repository: Repository | None, repo_directory: str | None
|
||||
self, selected_repository: str | None, repo_directory: str | None
|
||||
) -> Memory:
|
||||
memory = Memory(
|
||||
event_stream=self.event_stream,
|
||||
@@ -408,14 +414,12 @@ class AgentSession:
|
||||
# loads microagents from repo/.openhands/microagents
|
||||
microagents: list[BaseMicroagent] = await call_sync_from_async(
|
||||
self.runtime.get_microagents_from_selected_repo,
|
||||
selected_repository.full_name if selected_repository else None,
|
||||
selected_repository or None,
|
||||
)
|
||||
memory.load_user_workspace_microagents(microagents)
|
||||
|
||||
if selected_repository and repo_directory:
|
||||
memory.set_repository_info(
|
||||
selected_repository.full_name, repo_directory
|
||||
)
|
||||
memory.set_repository_info(selected_repository, repo_directory)
|
||||
return memory
|
||||
|
||||
def _maybe_restore_state(self) -> State | None:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from pydantic import Field
|
||||
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.integrations.service_types import Repository
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
|
||||
|
||||
@@ -11,7 +10,7 @@ class ConversationInitData(Settings):
|
||||
"""
|
||||
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = Field(default=None, frozen=True)
|
||||
selected_repository: Repository | None = Field(default=None)
|
||||
selected_repository: str | None = Field(default=None)
|
||||
replay_json: str | None = Field(default=None)
|
||||
selected_branch: str | None = Field(default=None)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user