mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
8 Commits
pr-6297
...
improve-ag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba79c454b | ||
|
|
fbc06f42aa | ||
|
|
f35ed5e277 | ||
|
|
6787a3adf7 | ||
|
|
fa50e0c9b9 | ||
|
|
f4c5bbda19 | ||
|
|
6562297615 | ||
|
|
0217a7cfbd |
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
@@ -1,11 +1,12 @@
|
||||
**End-user friendly description of the problem this fixes or functionality that this introduces**
|
||||
|
||||
- [ ] Include this change in the Release Notes. If checked, you must provide an **end-user friendly** description for your change below
|
||||
|
||||
---
|
||||
**Give a summary of what the PR does, explaining any non-trivial design decisions**
|
||||
- [ ] 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.**
|
||||
|
||||
|
||||
---
|
||||
**Link of any specific issues this addresses**
|
||||
**Give a summary of what the PR does, explaining any non-trivial design decisions.**
|
||||
|
||||
|
||||
---
|
||||
**Link of any specific issues this addresses.**
|
||||
|
||||
@@ -100,7 +100,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.25-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.26-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -1,4 +1,4 @@
|
||||
SHELL=/bin/bash
|
||||
SHELL=/usr/bin/env bash
|
||||
# Makefile for OpenHands project
|
||||
|
||||
# Variables
|
||||
|
||||
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.26-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -83,9 +83,6 @@ workspace_base = "./workspace"
|
||||
# Runtime environment
|
||||
#runtime = "docker"
|
||||
|
||||
# Runtime executor
|
||||
#runtime_executor = "openhands.runtime.executor:ActionExecutor"
|
||||
|
||||
# Name of the default agent
|
||||
#default_agent = "CodeActAgent"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# Initialize variables with default values
|
||||
|
||||
@@ -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.25-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.26-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -o pipefail
|
||||
|
||||
function get_docker() {
|
||||
|
||||
@@ -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.25-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26 \
|
||||
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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26 \
|
||||
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.25-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.26-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26
|
||||
```
|
||||
|
||||
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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26 \
|
||||
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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26 \
|
||||
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.25-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.26-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26 \
|
||||
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.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26 \
|
||||
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.25-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.26-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-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.25
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.26
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Step 1: Stop all running containers
|
||||
echo "Stopping all running containers..."
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
RESULT_FILE=$1
|
||||
MODEL_CONFIG=$2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
LEVEL=$1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This is ONLY used for pushing docker images created by https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
FOLDER_PATH=$1
|
||||
NEW_FOLDER_PATH=${FOLDER_PATH}.swebench_submission
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PROCESS_FILEPATH=$1
|
||||
if [ -z "$PROCESS_FILEPATH" ]; then
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
INPUT_FILE=$1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ~/.bashrc
|
||||
SWEUTIL_DIR=/swe_util
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
EVAL_WORKSPACE="evaluation/benchmarks/swe_bench/eval_workspace"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##################################################################################################
|
||||
# Adapted from https://github.com/TheAgentCompany/TheAgentCompany/blob/main/evaluation/run_eval.sh
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
echo "hello world"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
# API base URL
|
||||
|
||||
@@ -65,6 +65,12 @@ describe("extractModelAndProvider", () => {
|
||||
separator: "/",
|
||||
});
|
||||
|
||||
expect(extractModelAndProvider("claude-3-7-sonnet-20250219")).toEqual({
|
||||
provider: "anthropic",
|
||||
model: "claude-3-7-sonnet-20250219",
|
||||
separator: "/",
|
||||
});
|
||||
|
||||
expect(extractModelAndProvider("claude-3-haiku-20240307")).toEqual({
|
||||
provider: "anthropic",
|
||||
model: "claude-3-haiku-20240307",
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.6.14",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.25.0",
|
||||
"version": "0.26.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,7 @@ export const VERIFIED_PROVIDERS = ["openai", "azure", "anthropic", "deepseek"];
|
||||
export const VERIFIED_MODELS = [
|
||||
"o3-mini-2025-01-31",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"deepseek-chat",
|
||||
];
|
||||
|
||||
@@ -31,4 +32,5 @@ export const VERIFIED_ANTHROPIC_MODELS = [
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-7-sonnet-20250219",
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ from openhands.core.config.config_utils import (
|
||||
OH_MAX_ITERATIONS,
|
||||
get_field_info,
|
||||
)
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.config.sandbox_config import SandboxConfig
|
||||
from openhands.core.config.security_config import SecurityConfig
|
||||
@@ -28,6 +29,7 @@ __all__ = [
|
||||
'LLMConfig',
|
||||
'SandboxConfig',
|
||||
'SecurityConfig',
|
||||
'ExtendedConfig',
|
||||
'load_app_config',
|
||||
'load_from_env',
|
||||
'load_from_toml',
|
||||
|
||||
@@ -9,6 +9,7 @@ from openhands.core.config.config_utils import (
|
||||
OH_MAX_ITERATIONS,
|
||||
model_defaults_to_dict,
|
||||
)
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.config.sandbox_config import SandboxConfig
|
||||
from openhands.core.config.security_config import SecurityConfig
|
||||
@@ -52,6 +53,7 @@ class AppConfig(BaseModel):
|
||||
default_agent: str = Field(default=OH_DEFAULT_AGENT)
|
||||
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
|
||||
security: SecurityConfig = Field(default_factory=SecurityConfig)
|
||||
extended: ExtendedConfig = Field(default_factory=lambda: ExtendedConfig({}))
|
||||
runtime: str = Field(default='docker')
|
||||
file_store: str = Field(default='local')
|
||||
file_store_path: str = Field(default='/tmp/openhands_file_store')
|
||||
|
||||
40
openhands/core/config/extended_config.py
Normal file
40
openhands/core/config/extended_config.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from pydantic import RootModel
|
||||
|
||||
|
||||
class ExtendedConfig(RootModel[dict]):
|
||||
"""Configuration for extended functionalities.
|
||||
|
||||
This is implemented as a root model so that the entire input is stored
|
||||
as the root value. This allows arbitrary keys to be stored and later
|
||||
accessed via attribute or dictionary-style access.
|
||||
"""
|
||||
|
||||
@property
|
||||
def root(self) -> dict: # type annotation to help mypy
|
||||
return super().root
|
||||
|
||||
def __str__(self) -> str:
|
||||
# Use the root dict to build a string representation.
|
||||
attr_str = [f'{k}={repr(v)}' for k, v in self.root.items()]
|
||||
return f"ExtendedConfig({', '.join(attr_str)})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> 'ExtendedConfig':
|
||||
# Create an instance directly by wrapping the input dict.
|
||||
return cls(data)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
# Provide dictionary-like access via the root dict.
|
||||
return self.root[key]
|
||||
|
||||
def __getattr__(self, key: str) -> object:
|
||||
# Fallback for attribute access using the root dict.
|
||||
try:
|
||||
return self.root[key]
|
||||
except KeyError as e:
|
||||
raise AttributeError(
|
||||
f"'ExtendedConfig' object has no attribute '{key}'"
|
||||
) from e
|
||||
@@ -53,11 +53,11 @@ class SandboxConfig(BaseModel):
|
||||
remote_runtime_api_timeout: int = Field(default=10)
|
||||
remote_runtime_enable_retries: bool = Field(default=False)
|
||||
remote_runtime_class: str | None = Field(
|
||||
default='sysbox'
|
||||
default=None
|
||||
) # can be "None" (default to gvisor) or "sysbox" (support docker inside runtime + more stable)
|
||||
enable_auto_lint: bool = Field(
|
||||
default=False # once enabled, OpenHands would lint files after editing
|
||||
)
|
||||
default=False
|
||||
) # once enabled, OpenHands would lint files after editing
|
||||
use_host_network: bool = Field(default=False)
|
||||
runtime_extra_build_args: list[str] | None = Field(default=None)
|
||||
initialize_plugins: bool = Field(default=True)
|
||||
|
||||
@@ -19,6 +19,7 @@ from openhands.core.config.config_utils import (
|
||||
OH_DEFAULT_AGENT,
|
||||
OH_MAX_ITERATIONS,
|
||||
)
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.config.sandbox_config import SandboxConfig
|
||||
from openhands.core.config.security_config import SecurityConfig
|
||||
@@ -134,6 +135,10 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
|
||||
for key, value in toml_config.items():
|
||||
if isinstance(value, dict):
|
||||
try:
|
||||
if key.lower() == 'extended':
|
||||
# For ExtendedConfig (RootModel), pass the entire dict as the root value
|
||||
cfg.extended = ExtendedConfig(value)
|
||||
continue
|
||||
if key is not None and key.lower() == 'agent':
|
||||
# Every entry here is either a field for the default `agent` config group, or itself a group
|
||||
# The best way to tell the difference is to try to parse it as an AgentConfig object
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any, Callable
|
||||
import requests
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.utils.ensure_httpx_close import EnsureHttpxClose
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
@@ -43,6 +42,7 @@ LLM_RETRY_EXCEPTIONS: tuple[type[Exception], ...] = (RateLimitError,)
|
||||
# cache prompt supporting models
|
||||
# remove this when we gemini and deepseek are supported
|
||||
CACHE_PROMPT_SUPPORTED_MODELS = [
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-sonnet-20240620',
|
||||
'claude-3-5-haiku-20241022',
|
||||
@@ -52,6 +52,7 @@ CACHE_PROMPT_SUPPORTED_MODELS = [
|
||||
|
||||
# function calling supporting models
|
||||
FUNCTION_CALLING_SUPPORTED_MODELS = [
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'claude-3-5-sonnet',
|
||||
'claude-3-5-sonnet-20240620',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
@@ -231,9 +232,9 @@ class LLM(RetryMixin, DebugMixin):
|
||||
|
||||
# Record start time for latency measurement
|
||||
start_time = time.time()
|
||||
with EnsureHttpxClose():
|
||||
# we don't support streaming here, thus we get a ModelResponse
|
||||
resp: ModelResponse = self._completion_unwrapped(*args, **kwargs)
|
||||
|
||||
# we don't support streaming here, thus we get a ModelResponse
|
||||
resp: ModelResponse = self._completion_unwrapped(*args, **kwargs)
|
||||
|
||||
# Calculate and record latency
|
||||
latency = time.time() - start_time
|
||||
@@ -288,11 +289,7 @@ class LLM(RetryMixin, DebugMixin):
|
||||
'messages': messages,
|
||||
'response': resp,
|
||||
'args': args,
|
||||
'kwargs': {
|
||||
k: v
|
||||
for k, v in kwargs.items()
|
||||
if k not in ('messages', 'client')
|
||||
},
|
||||
'kwargs': {k: v for k, v in kwargs.items() if k != 'messages'},
|
||||
'timestamp': time.time(),
|
||||
'cost': cost,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import tempfile
|
||||
import time
|
||||
import traceback
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Type
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
|
||||
@@ -60,6 +60,7 @@ from openhands.runtime.utils.files import insert_lines, read_lines
|
||||
from openhands.runtime.utils.memory_monitor import MemoryMonitor
|
||||
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
|
||||
|
||||
|
||||
class ActionRequest(BaseModel):
|
||||
@@ -67,6 +68,7 @@ class ActionRequest(BaseModel):
|
||||
|
||||
|
||||
ROOT_GID = 0
|
||||
|
||||
SESSION_API_KEY = os.environ.get('SESSION_API_KEY')
|
||||
api_key_header = APIKeyHeader(name='X-Session-API-Key', auto_error=False)
|
||||
|
||||
@@ -133,6 +135,7 @@ class ActionExecutor:
|
||||
"""ActionExecutor is running inside docker sandbox.
|
||||
It is responsible for executing actions received from OpenHands backend and producing observations.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugins_to_load: list[Plugin],
|
||||
@@ -460,7 +463,6 @@ class ActionExecutor:
|
||||
if self.bash_session is not None:
|
||||
self.bash_session.close()
|
||||
self.browser.close()
|
||||
>>>>>>> origin/main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -478,12 +480,6 @@ if __name__ == '__main__':
|
||||
help='BrowserGym environment used for browser evaluation',
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--executor-class',
|
||||
type=str,
|
||||
default='openhands.runtime.executor:ActionExecutor',
|
||||
help='Action executor class to use (format: module.path:ClassName)',
|
||||
)
|
||||
# example: python client.py 8000 --working-dir /workspace --plugins JupyterRequirement
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -494,13 +490,12 @@ if __name__ == '__main__':
|
||||
raise ValueError(f'Plugin {plugin} not found')
|
||||
plugins_to_load.append(ALL_PLUGINS[plugin]()) # type: ignore
|
||||
|
||||
executor_class = get_action_executor_class(args.executor_class)
|
||||
client: RuntimeExecutor | None = None
|
||||
client: ActionExecutor | None = None
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global client
|
||||
client = executor_class(
|
||||
client = ActionExecutor(
|
||||
plugins_to_load,
|
||||
work_dir=args.working_dir,
|
||||
username=args.username,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from .base import RuntimeExecutor
|
||||
from .action_executor import ActionExecutor, BaseActionExecutor
|
||||
|
||||
__all__ = ['ActionExecutor', 'BaseActionExecutor', 'RuntimeExecutor']
|
||||
@@ -1,267 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from openhands_aci.utils.diff import get_diff
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
|
||||
from openhands.events.action.commands import IPythonRunCellAction
|
||||
from openhands.events.action.files import FileReadAction, FileWriteAction
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
from openhands.events.observation.commands import (
|
||||
IPythonRunCellObservation,
|
||||
)
|
||||
from openhands.events.observation.error import ErrorObservation
|
||||
from openhands.events.observation.files import (
|
||||
FileEditObservation,
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
)
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.runtime.browser import browse
|
||||
from openhands.runtime.executor.base import RuntimeExecutor
|
||||
from openhands.runtime.plugins.jupyter import JupyterPlugin
|
||||
from openhands.runtime.utils.files import insert_lines, read_lines
|
||||
|
||||
|
||||
class BaseActionExecutor(RuntimeExecutor):
|
||||
"""Runtime executor that dynamically dispatches actions to the appropriate method based on their name."""
|
||||
|
||||
async def run_action(self, action) -> Observation:
|
||||
async with self.lock:
|
||||
action_type = action.action
|
||||
logger.debug(f'Running action:\n{action}')
|
||||
observation = await getattr(self, action_type)(action)
|
||||
logger.debug(f'Action output:\n{observation}')
|
||||
return observation
|
||||
|
||||
|
||||
class ActionExecutor(BaseActionExecutor):
|
||||
"""ActionExecutor runs inside docker sandbox.
|
||||
It is responsible for executing actions received from OpenHands backend and producing observations.
|
||||
It is a BaseActionExectuor that provides a default implementation for all of the built-in actions.
|
||||
"""
|
||||
|
||||
async def run_ipython(self, action: IPythonRunCellAction) -> Observation:
|
||||
assert self.bash_session is not None
|
||||
if 'jupyter' in self.plugins:
|
||||
_jupyter_plugin: JupyterPlugin = self.plugins['jupyter'] # type: ignore
|
||||
# This is used to make AgentSkills in Jupyter aware of the
|
||||
# current working directory in Bash
|
||||
jupyter_cwd = getattr(self, '_jupyter_cwd', None)
|
||||
if self.bash_session.cwd != jupyter_cwd:
|
||||
logger.debug(
|
||||
f'{self.bash_session.cwd} != {jupyter_cwd} -> reset Jupyter PWD'
|
||||
)
|
||||
reset_jupyter_cwd_code = (
|
||||
f'import os; os.chdir("{self.bash_session.cwd}")'
|
||||
)
|
||||
_aux_action = IPythonRunCellAction(code=reset_jupyter_cwd_code)
|
||||
_reset_obs: IPythonRunCellObservation = await _jupyter_plugin.run(
|
||||
_aux_action
|
||||
)
|
||||
logger.debug(
|
||||
f'Changed working directory in IPython to: {self.bash_session.cwd}. Output: {_reset_obs}'
|
||||
)
|
||||
self._jupyter_cwd = self.bash_session.cwd
|
||||
|
||||
obs: IPythonRunCellObservation = await _jupyter_plugin.run(action)
|
||||
obs.content = obs.content.rstrip()
|
||||
matches = re.findall(
|
||||
r'<oh_aci_output_[0-9a-f]{32}>(.*?)</oh_aci_output_[0-9a-f]{32}>',
|
||||
obs.content,
|
||||
re.DOTALL,
|
||||
)
|
||||
if matches:
|
||||
results: list[str] = []
|
||||
if len(matches) == 1:
|
||||
# Use specific actions/observations types
|
||||
match = matches[0]
|
||||
try:
|
||||
result_dict = json.loads(match)
|
||||
if result_dict.get('path'): # Successful output
|
||||
if (
|
||||
result_dict['new_content'] is not None
|
||||
): # File edit commands
|
||||
diff = get_diff(
|
||||
old_contents=result_dict['old_content']
|
||||
or '', # old_content is None when file is created
|
||||
new_contents=result_dict['new_content'],
|
||||
filepath=result_dict['path'],
|
||||
)
|
||||
return FileEditObservation(
|
||||
content=diff,
|
||||
path=result_dict['path'],
|
||||
old_content=result_dict['old_content'],
|
||||
new_content=result_dict['new_content'],
|
||||
prev_exist=result_dict['prev_exist'],
|
||||
impl_source=FileEditSource.OH_ACI,
|
||||
formatted_output_and_error=result_dict[
|
||||
'formatted_output_and_error'
|
||||
],
|
||||
)
|
||||
else: # File view commands
|
||||
return FileReadObservation(
|
||||
content=result_dict['formatted_output_and_error'],
|
||||
path=result_dict['path'],
|
||||
impl_source=FileReadSource.OH_ACI,
|
||||
)
|
||||
else: # Error output
|
||||
results.append(result_dict['formatted_output_and_error'])
|
||||
except json.JSONDecodeError:
|
||||
# Handle JSON decoding errors if necessary
|
||||
results.append(
|
||||
f"Invalid JSON in 'openhands-aci' output: {match}"
|
||||
)
|
||||
else:
|
||||
for match in matches:
|
||||
try:
|
||||
result_dict = json.loads(match)
|
||||
results.append(result_dict['formatted_output_and_error'])
|
||||
except json.JSONDecodeError:
|
||||
# Handle JSON decoding errors if necessary
|
||||
results.append(
|
||||
f"Invalid JSON in 'openhands-aci' output: {match}"
|
||||
)
|
||||
|
||||
# Combine the results (e.g., join them) or handle them as required
|
||||
obs.content = '\n'.join(str(result) for result in results)
|
||||
|
||||
if action.include_extra:
|
||||
obs.content += (
|
||||
f'\n[Jupyter current working directory: {self.bash_session.cwd}]'
|
||||
)
|
||||
obs.content += f'\n[Jupyter Python interpreter: {_jupyter_plugin.python_interpreter_path}]'
|
||||
return obs
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'JupyterRequirement not found. Unable to run IPython action.'
|
||||
)
|
||||
|
||||
def _resolve_path(self, path: str, working_dir: str) -> str:
|
||||
filepath = Path(path)
|
||||
if not filepath.is_absolute():
|
||||
return str(Path(working_dir) / filepath)
|
||||
return str(filepath)
|
||||
|
||||
async def read(self, action: FileReadAction) -> Observation:
|
||||
assert self.bash_session is not None
|
||||
if action.impl_source == FileReadSource.OH_ACI:
|
||||
return await self.run_ipython(
|
||||
IPythonRunCellAction(
|
||||
code=action.translated_ipython_code,
|
||||
include_extra=False,
|
||||
)
|
||||
)
|
||||
|
||||
# NOTE: the client code is running inside the sandbox,
|
||||
# so there's no need to check permission
|
||||
working_dir = self.bash_session.cwd
|
||||
filepath = self._resolve_path(action.path, working_dir)
|
||||
try:
|
||||
if filepath.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
||||
with open(filepath, 'rb') as file:
|
||||
image_data = file.read()
|
||||
encoded_image = base64.b64encode(image_data).decode('utf-8')
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
if mime_type is None:
|
||||
mime_type = 'image/png' # default to PNG if mime type cannot be determined
|
||||
encoded_image = f'data:{mime_type};base64,{encoded_image}'
|
||||
|
||||
return FileReadObservation(path=filepath, content=encoded_image)
|
||||
elif filepath.lower().endswith('.pdf'):
|
||||
with open(filepath, 'rb') as file:
|
||||
pdf_data = file.read()
|
||||
encoded_pdf = base64.b64encode(pdf_data).decode('utf-8')
|
||||
encoded_pdf = f'data:application/pdf;base64,{encoded_pdf}'
|
||||
return FileReadObservation(path=filepath, content=encoded_pdf)
|
||||
elif filepath.lower().endswith(('.mp4', '.webm', '.ogg')):
|
||||
with open(filepath, 'rb') as file:
|
||||
video_data = file.read()
|
||||
encoded_video = base64.b64encode(video_data).decode('utf-8')
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
if mime_type is None:
|
||||
mime_type = 'video/mp4' # default to MP4 if MIME type cannot be determined
|
||||
encoded_video = f'data:{mime_type};base64,{encoded_video}'
|
||||
|
||||
return FileReadObservation(path=filepath, content=encoded_video)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as file:
|
||||
lines = read_lines(file.readlines(), action.start, action.end)
|
||||
except FileNotFoundError:
|
||||
return ErrorObservation(
|
||||
f'File not found: {filepath}. Your current working directory is {working_dir}.'
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
return ErrorObservation(f'File could not be decoded as utf-8: {filepath}.')
|
||||
except IsADirectoryError:
|
||||
return ErrorObservation(
|
||||
f'Path is a directory: {filepath}. You can only read files'
|
||||
)
|
||||
|
||||
code_view = ''.join(lines)
|
||||
return FileReadObservation(path=filepath, content=code_view)
|
||||
|
||||
async def write(self, action: FileWriteAction) -> Observation:
|
||||
assert self.bash_session is not None
|
||||
working_dir = self.bash_session.cwd
|
||||
filepath = self._resolve_path(action.path, working_dir)
|
||||
|
||||
insert = action.content.split('\n')
|
||||
try:
|
||||
if not os.path.exists(os.path.dirname(filepath)):
|
||||
os.makedirs(os.path.dirname(filepath))
|
||||
|
||||
file_exists = os.path.exists(filepath)
|
||||
if file_exists:
|
||||
file_stat = os.stat(filepath)
|
||||
else:
|
||||
file_stat = None
|
||||
|
||||
mode = 'w' if not file_exists else 'r+'
|
||||
try:
|
||||
with open(filepath, mode, encoding='utf-8') as file:
|
||||
if mode != 'w':
|
||||
all_lines = file.readlines()
|
||||
new_file = insert_lines(
|
||||
insert, all_lines, action.start, action.end
|
||||
)
|
||||
else:
|
||||
new_file = [i + '\n' for i in insert]
|
||||
|
||||
file.seek(0)
|
||||
file.writelines(new_file)
|
||||
file.truncate()
|
||||
|
||||
# Handle file permissions
|
||||
if file_exists:
|
||||
assert file_stat is not None
|
||||
# restore the original file permissions if the file already exists
|
||||
os.chmod(filepath, file_stat.st_mode)
|
||||
os.chown(filepath, file_stat.st_uid, file_stat.st_gid)
|
||||
else:
|
||||
# set the new file permissions if the file is new
|
||||
os.chmod(filepath, 0o664)
|
||||
os.chown(filepath, self.user_id, self.user_id)
|
||||
|
||||
except FileNotFoundError:
|
||||
return ErrorObservation(f'File not found: {filepath}')
|
||||
except IsADirectoryError:
|
||||
return ErrorObservation(
|
||||
f'Path is a directory: {filepath}. You can only write to files'
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
return ErrorObservation(
|
||||
f'File could not be decoded as utf-8: {filepath}'
|
||||
)
|
||||
except PermissionError:
|
||||
return ErrorObservation(f'Malformed paths not permitted: {filepath}')
|
||||
return FileWriteObservation(content='', path=filepath)
|
||||
|
||||
async def browse(self, action: BrowseURLAction) -> Observation:
|
||||
return await browse(action, self.browser)
|
||||
|
||||
async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
|
||||
return await browse(action, self.browser)
|
||||
@@ -1,126 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.commands import CmdRunAction, IPythonRunCellAction
|
||||
from openhands.events.observation.commands import CmdOutputObservation
|
||||
from openhands.events.observation.error import ErrorObservation
|
||||
from openhands.runtime.browser.browser_env import BrowserEnv
|
||||
from openhands.runtime.plugins.jupyter import JupyterPlugin
|
||||
from openhands.runtime.plugins.requirement import Plugin
|
||||
from openhands.runtime.utils.bash import BashSession
|
||||
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
|
||||
from openhands.utils.async_utils import call_sync_from_async, wait_all
|
||||
|
||||
|
||||
ROOT_GID = 0
|
||||
INIT_COMMANDS = [
|
||||
'git config --global user.name "openhands" && git config --global user.email "openhands@all-hands.dev" && alias git="git --no-pager"',
|
||||
]
|
||||
|
||||
|
||||
class RuntimeExecutor:
|
||||
"""RuntimeExecutor for running inside docker sandbox.
|
||||
It provides a minimal base class that handles initialization of the executor, and provides a run method to execute bash commands.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugins_to_load: list[Plugin],
|
||||
work_dir: str,
|
||||
username: str,
|
||||
user_id: int,
|
||||
browsergym_eval_env: str | None,
|
||||
) -> None:
|
||||
self.plugins_to_load = plugins_to_load
|
||||
self._initial_cwd = work_dir
|
||||
self.username = username
|
||||
self.user_id = user_id
|
||||
_updated_user_id = init_user_and_working_directory(
|
||||
username=username, user_id=self.user_id, initial_cwd=work_dir
|
||||
)
|
||||
if _updated_user_id is not None:
|
||||
self.user_id = _updated_user_id
|
||||
|
||||
self.bash_session: BashSession | None = None
|
||||
self.lock = asyncio.Lock()
|
||||
self.plugins: dict[str, Plugin] = {}
|
||||
self.browser = BrowserEnv(browsergym_eval_env)
|
||||
self.start_time = time.time()
|
||||
self.last_execution_time = self.start_time
|
||||
self._initialized = False
|
||||
|
||||
@property
|
||||
def initial_cwd(self):
|
||||
return self._initial_cwd
|
||||
|
||||
async def ainit(self):
|
||||
# bash needs to be initialized first
|
||||
self.bash_session = BashSession(
|
||||
work_dir=self._initial_cwd,
|
||||
username=self.username,
|
||||
)
|
||||
self.bash_session.initialize()
|
||||
await wait_all(
|
||||
(self._init_plugin(plugin) for plugin in self.plugins_to_load),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# This is a temporary workaround
|
||||
# TODO: refactor AgentSkills to be part of JupyterPlugin
|
||||
# AFTER ServerRuntime is deprecated
|
||||
if 'agent_skills' in self.plugins and 'jupyter' in self.plugins:
|
||||
obs = await self.run_ipython(
|
||||
IPythonRunCellAction(
|
||||
code='from openhands.runtime.plugins.agent_skills.agentskills import *\n'
|
||||
)
|
||||
)
|
||||
logger.debug(f'AgentSkills initialized: {obs}')
|
||||
|
||||
await self._init_bash_commands()
|
||||
logger.debug('Runtime client initialized.')
|
||||
|
||||
self._initialized = True
|
||||
|
||||
@property
|
||||
def initialized(self) -> bool:
|
||||
return self._initialized
|
||||
|
||||
async def _init_plugin(self, plugin: Plugin):
|
||||
assert self.bash_session is not None
|
||||
await plugin.initialize(self.username)
|
||||
self.plugins[plugin.name] = plugin
|
||||
logger.debug(f'Initializing plugin: {plugin.name}')
|
||||
|
||||
if isinstance(plugin, JupyterPlugin):
|
||||
await self.run_ipython(
|
||||
IPythonRunCellAction(
|
||||
code=f'import os; os.chdir("{self.bash_session.cwd}")'
|
||||
)
|
||||
)
|
||||
|
||||
async def _init_bash_commands(self):
|
||||
logger.debug(f'Initializing by running {len(INIT_COMMANDS)} bash commands...')
|
||||
for command in INIT_COMMANDS:
|
||||
action = CmdRunAction(command=command)
|
||||
action.timeout = 300
|
||||
logger.debug(f'Executing init command: {command}')
|
||||
obs = await self.run(action)
|
||||
assert isinstance(obs, CmdOutputObservation)
|
||||
logger.debug(
|
||||
f'Init command outputs (exit code: {obs.exit_code}): {obs.content}'
|
||||
)
|
||||
assert obs.exit_code == 0
|
||||
|
||||
logger.debug('Bash init commands completed')
|
||||
|
||||
async def run(
|
||||
self, action: CmdRunAction
|
||||
) -> CmdOutputObservation | ErrorObservation:
|
||||
assert self.bash_session is not None
|
||||
obs = await call_sync_from_async(self.bash_session.execute, action)
|
||||
return obs
|
||||
|
||||
def close(self):
|
||||
if self.bash_session is not None:
|
||||
self.bash_session.close()
|
||||
self.browser.close()
|
||||
@@ -55,8 +55,6 @@ def get_action_execution_server_startup_command(
|
||||
'--user-id',
|
||||
str(user_id),
|
||||
*browsergym_args,
|
||||
'--executor-class',
|
||||
app_config.runtime_executor,
|
||||
]
|
||||
|
||||
return base_cmd
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""
|
||||
LiteLLM currently have an issue where HttpHandlers are being created but not
|
||||
closed. We have submitted a PR to them, (https://github.com/BerriAI/litellm/pull/8711)
|
||||
and their dev team say they are in the process of a refactor that will fix this, but
|
||||
in the meantime, we need to manage the lifecycle of the httpx.Client manually.
|
||||
|
||||
We can't simply pass in our own client object, because all the different implementations use
|
||||
different types of client object.
|
||||
|
||||
So we monkey patch the httpx.Client class to track newly created instances and close these
|
||||
when the operations complete. (This is relatively safe, as if the client is reused after this
|
||||
then is will transparently reopen)
|
||||
|
||||
Hopefully, this will be fixed soon and we can remove this abomination.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
|
||||
from httpx import Client
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnsureHttpxClose:
|
||||
clients: list[Client] = field(default_factory=list)
|
||||
original_init: Callable | None = None
|
||||
|
||||
def __enter__(self):
|
||||
self.original_init = Client.__init__
|
||||
|
||||
@wraps(Client.__init__)
|
||||
def init_wrapper(*args, **kwargs):
|
||||
self.clients.append(args[0])
|
||||
return self.original_init(*args, **kwargs) # type: ignore
|
||||
|
||||
Client.__init__ = init_wrapper
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
Client.__init__ = self.original_init
|
||||
while self.clients:
|
||||
client = self.clients.pop()
|
||||
client.close()
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.25.0"
|
||||
version = "0.26.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -109,7 +109,6 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -138,7 +137,6 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
169
tests/unit/test_config_extended.py
Normal file
169
tests/unit/test_config_extended.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.core.config.app_config import AppConfig
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.utils import load_from_toml
|
||||
|
||||
|
||||
def test_extended_config_from_dict():
|
||||
"""
|
||||
Test that ExtendedConfig.from_dict successfully creates an instance
|
||||
from a dictionary containing arbitrary extra keys.
|
||||
"""
|
||||
data = {'foo': 'bar', 'baz': 123, 'flag': True}
|
||||
ext_cfg = ExtendedConfig.from_dict(data)
|
||||
|
||||
# Check that the keys are accessible both as attributes and via __getitem__
|
||||
assert ext_cfg.foo == 'bar'
|
||||
assert ext_cfg['baz'] == 123
|
||||
assert ext_cfg.flag is True
|
||||
# Verify the root dictionary contains all keys
|
||||
assert ext_cfg.root == data
|
||||
|
||||
|
||||
def test_extended_config_empty():
|
||||
"""
|
||||
Test that an empty ExtendedConfig can be created and accessed.
|
||||
"""
|
||||
ext_cfg = ExtendedConfig.from_dict({})
|
||||
assert ext_cfg.root == {}
|
||||
|
||||
# Creating directly should also work
|
||||
ext_cfg2 = ExtendedConfig({})
|
||||
assert ext_cfg2.root == {}
|
||||
|
||||
|
||||
def test_extended_config_str_and_repr():
|
||||
"""
|
||||
Test that __str__ and __repr__ return the correct string representations
|
||||
of the ExtendedConfig instance.
|
||||
"""
|
||||
data = {'alpha': 'test', 'beta': 42}
|
||||
ext_cfg = ExtendedConfig.from_dict(data)
|
||||
string_repr = str(ext_cfg)
|
||||
repr_str = repr(ext_cfg)
|
||||
|
||||
# Ensure the representations include our key/value pairs
|
||||
assert "alpha='test'" in string_repr
|
||||
assert 'beta=42' in string_repr
|
||||
|
||||
# __repr__ should match __str__
|
||||
assert string_repr == repr_str
|
||||
|
||||
|
||||
def test_extended_config_getitem_and_getattr():
|
||||
"""
|
||||
Test that __getitem__ and __getattr__ can be used to access values
|
||||
in the ExtendedConfig instance.
|
||||
"""
|
||||
data = {'key1': 'value1', 'key2': 2}
|
||||
ext_cfg = ExtendedConfig.from_dict(data)
|
||||
|
||||
# Attribute access
|
||||
assert ext_cfg.key1 == 'value1'
|
||||
# Dictionary-style access
|
||||
assert ext_cfg['key2'] == 2
|
||||
|
||||
|
||||
def test_extended_config_invalid_key():
|
||||
"""
|
||||
Test that accessing a non-existent key via attribute access raises AttributeError.
|
||||
"""
|
||||
data = {'existing': 'yes'}
|
||||
ext_cfg = ExtendedConfig.from_dict(data)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
_ = ext_cfg.nonexistent
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
_ = ext_cfg['nonexistent']
|
||||
|
||||
|
||||
def test_app_config_extended_from_toml(tmp_path: os.PathLike) -> None:
|
||||
"""
|
||||
Test that the [extended] section in a TOML file is correctly loaded into
|
||||
AppConfig.extended and that it accepts arbitrary keys.
|
||||
"""
|
||||
# Create a temporary TOML file with multiple sections including [extended]
|
||||
config_content = """
|
||||
[core]
|
||||
workspace_base = "/tmp/workspace"
|
||||
|
||||
[llm]
|
||||
model = "test-model"
|
||||
api_key = "toml-api-key"
|
||||
|
||||
[extended]
|
||||
custom1 = "custom_value"
|
||||
custom2 = 42
|
||||
llm = "overridden" # even a key like 'llm' is accepted in extended
|
||||
|
||||
[agent]
|
||||
memory_enabled = true
|
||||
"""
|
||||
config_file = tmp_path / 'config.toml'
|
||||
config_file.write_text(config_content)
|
||||
|
||||
# Load the TOML into the AppConfig instance
|
||||
config = AppConfig()
|
||||
load_from_toml(config, str(config_file))
|
||||
|
||||
# Verify that extended section is applied
|
||||
assert config.extended.custom1 == 'custom_value'
|
||||
assert config.extended.custom2 == 42
|
||||
# Even though 'llm' is defined in extended, it should not affect the main llm config.
|
||||
assert config.get_llm_config().model == 'test-model'
|
||||
|
||||
|
||||
def test_app_config_extended_default(tmp_path: os.PathLike) -> None:
|
||||
"""
|
||||
Test that if there is no [extended] section in the TOML file,
|
||||
AppConfig.extended remains its default (empty) ExtendedConfig.
|
||||
"""
|
||||
config_content = """
|
||||
[core]
|
||||
workspace_base = "/tmp/workspace"
|
||||
|
||||
[llm]
|
||||
model = "test-model"
|
||||
api_key = "toml-api-key"
|
||||
|
||||
[agent]
|
||||
memory_enabled = true
|
||||
"""
|
||||
config_file = tmp_path / 'config.toml'
|
||||
config_file.write_text(config_content)
|
||||
|
||||
config = AppConfig()
|
||||
load_from_toml(config, str(config_file))
|
||||
|
||||
# Extended config should be empty
|
||||
assert config.extended.root == {}
|
||||
|
||||
|
||||
def test_app_config_extended_random_keys(tmp_path: os.PathLike) -> None:
|
||||
"""
|
||||
Test that the extended section accepts arbitrary keys,
|
||||
including ones not defined in any schema.
|
||||
"""
|
||||
config_content = """
|
||||
[core]
|
||||
workspace_base = "/tmp/workspace"
|
||||
|
||||
[extended]
|
||||
random_key = "random_value"
|
||||
another_key = 3.14
|
||||
"""
|
||||
config_file = tmp_path / 'config.toml'
|
||||
config_file.write_text(config_content)
|
||||
|
||||
config = AppConfig()
|
||||
load_from_toml(config, str(config_file))
|
||||
|
||||
# Verify that extended config holds the arbitrary keys with correct values.
|
||||
assert config.extended.random_key == 'random_value'
|
||||
assert config.extended.another_key == 3.14
|
||||
# Verify the root dictionary contains all keys
|
||||
assert config.extended.root == {'random_key': 'random_value', 'another_key': 3.14}
|
||||
@@ -1,84 +0,0 @@
|
||||
from httpx import Client
|
||||
|
||||
from openhands.utils.ensure_httpx_close import EnsureHttpxClose
|
||||
|
||||
|
||||
def test_ensure_httpx_close_basic():
|
||||
"""Test basic functionality of EnsureHttpxClose."""
|
||||
clients = []
|
||||
ctx = EnsureHttpxClose()
|
||||
with ctx:
|
||||
# Create a client - should be tracked
|
||||
client = Client()
|
||||
assert client in ctx.clients
|
||||
assert len(ctx.clients) == 1
|
||||
clients.append(client)
|
||||
|
||||
# After context exit, client should be closed
|
||||
assert client.is_closed
|
||||
|
||||
|
||||
def test_ensure_httpx_close_multiple_clients():
|
||||
"""Test EnsureHttpxClose with multiple clients."""
|
||||
ctx = EnsureHttpxClose()
|
||||
with ctx:
|
||||
client1 = Client()
|
||||
client2 = Client()
|
||||
assert len(ctx.clients) == 2
|
||||
assert client1 in ctx.clients
|
||||
assert client2 in ctx.clients
|
||||
|
||||
assert client1.is_closed
|
||||
assert client2.is_closed
|
||||
|
||||
|
||||
def test_ensure_httpx_close_nested():
|
||||
"""Test nested usage of EnsureHttpxClose."""
|
||||
outer_ctx = EnsureHttpxClose()
|
||||
with outer_ctx:
|
||||
client1 = Client()
|
||||
assert client1 in outer_ctx.clients
|
||||
|
||||
inner_ctx = EnsureHttpxClose()
|
||||
with inner_ctx:
|
||||
client2 = Client()
|
||||
assert client2 in inner_ctx.clients
|
||||
# Since both contexts are using the same monkey-patched __init__,
|
||||
# both contexts will track all clients created while they are active
|
||||
assert client2 in outer_ctx.clients
|
||||
|
||||
# After inner context, client2 should be closed
|
||||
assert client2.is_closed
|
||||
# client1 should still be open since outer context is still active
|
||||
assert not client1.is_closed
|
||||
|
||||
# After outer context, both clients should be closed
|
||||
assert client1.is_closed
|
||||
assert client2.is_closed
|
||||
|
||||
|
||||
def test_ensure_httpx_close_exception():
|
||||
"""Test EnsureHttpxClose when an exception occurs."""
|
||||
client = None
|
||||
ctx = EnsureHttpxClose()
|
||||
try:
|
||||
with ctx:
|
||||
client = Client()
|
||||
raise ValueError('Test exception')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Client should be closed even if an exception occurred
|
||||
assert client is not None
|
||||
assert client.is_closed
|
||||
|
||||
|
||||
def test_ensure_httpx_close_restore_init():
|
||||
"""Test that the original __init__ is restored after context exit."""
|
||||
original_init = Client.__init__
|
||||
ctx = EnsureHttpxClose()
|
||||
with ctx:
|
||||
assert Client.__init__ != original_init
|
||||
|
||||
# Original __init__ should be restored
|
||||
assert Client.__init__ == original_init
|
||||
@@ -1,6 +1,4 @@
|
||||
import copy
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -491,27 +489,3 @@ def test_llm_token_usage(mock_litellm_completion, default_config):
|
||||
assert usage_entry_2['cache_read_tokens'] == 1
|
||||
assert usage_entry_2['cache_write_tokens'] == 3
|
||||
assert usage_entry_2['response_id'] == 'test-response-usage-2'
|
||||
|
||||
|
||||
@patch('openhands.llm.llm.litellm_completion')
|
||||
def test_completion_with_log_completions(mock_litellm_completion, default_config):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
default_config.log_completions = True
|
||||
default_config.log_completions_folder = temp_dir
|
||||
mock_response = {
|
||||
'choices': [{'message': {'content': 'This is a mocked response.'}}]
|
||||
}
|
||||
mock_litellm_completion.return_value = mock_response
|
||||
|
||||
test_llm = LLM(config=default_config)
|
||||
response = test_llm.completion(
|
||||
messages=[{'role': 'user', 'content': 'Hello!'}],
|
||||
stream=False,
|
||||
drop_params=True,
|
||||
)
|
||||
assert (
|
||||
response['choices'][0]['message']['content'] == 'This is a mocked response.'
|
||||
)
|
||||
files = list(Path(temp_dir).iterdir())
|
||||
# Expect a log to be generated
|
||||
assert len(files) == 1
|
||||
|
||||
Reference in New Issue
Block a user