diff --git a/README.md b/README.md index cdc8694815a6..150263e67e71 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [ ## ⚡ Quick Start The easiest way to run OpenHands is in Docker. -See the [Installation](https://docs.all-hands.dev/modules/usage/installation) guide for +See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) guide for system requirements and more information. ```bash @@ -69,7 +69,7 @@ run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action). -Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions. +Visit [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions. > [!CAUTION] > OpenHands is meant to be run by a single user on their local workstation. diff --git a/config.template.toml b/config.template.toml index ccb7b1159747..7f256898b347 100644 --- a/config.template.toml +++ b/config.template.toml @@ -39,6 +39,11 @@ workspace_base = "./workspace" # If it's a folder, the session id will be used as the file name #save_trajectory_path="./trajectories" +# Path to replay a trajectory, must be a file path +# If provided, trajectory will be loaded and replayed before the +# agent responds to any user instruction +#replay_trajectory_path = "" + # File store path #file_store_path = "/tmp/file_store" diff --git a/docs/modules/usage/configuration-options.md b/docs/modules/usage/configuration-options.md index a3c11de52ed8..ff0aa5674cc8 100644 --- a/docs/modules/usage/configuration-options.md +++ b/docs/modules/usage/configuration-options.md @@ -55,6 +55,11 @@ The core configuration options are defined in the `[core]` section of the `confi - Default: `"./trajectories"` - Description: Path to store trajectories (can be a folder or a file). If it's a folder, the trajectories will be saved in a file named with the session id name and .json extension, in that folder. +- `replay_trajectory_path` + - Type: `str` + - Default: `""` + - Description: Path to load a trajectory and replay. If given, must be a path to the trajectory file in JSON format. The actions in the trajectory file would be replayed first before any user instruction is executed. + ### File Store - `file_store_path` - Type: `str` diff --git a/docs/modules/usage/getting-started.mdx b/docs/modules/usage/getting-started.mdx index 0a7140b91dd6..b01c4d147686 100644 --- a/docs/modules/usage/getting-started.mdx +++ b/docs/modules/usage/getting-started.mdx @@ -1,6 +1,6 @@ # Getting Started with OpenHands -So you've [installed OpenHands](./installation) and have +So you've [run OpenHands](./installation) and have [set up your LLM](./installation#setup). Now what? OpenHands can help you tackle a wide variety of engineering tasks. But the technology diff --git a/docs/modules/usage/installation.mdx b/docs/modules/usage/installation.mdx index d35eeb409fa8..d559440fac3b 100644 --- a/docs/modules/usage/installation.mdx +++ b/docs/modules/usage/installation.mdx @@ -1,12 +1,51 @@ -# Installation +# Running OpenHands ## System Requirements -- Docker version 26.0.0+ or Docker Desktop 4.31.0+. -- You must be using Linux or Mac OS. - - If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). +- MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements) +- Linux +- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements) -## Start the app +## Prerequisites + +
+ MacOS + ### Docker Desktop + + 1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install). + 2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled. +
+ +
+ Linux + + :::note + Tested with Ubuntu 22.04. + ::: + + ### Docker Desktop + + 1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/). + +
+ +
+ Windows + ### WSL + + 1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install). + 2. Run `wsl --version` in powershell and confirm `Default Version: 2`. + + ### Docker Desktop + + 1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install). + 2. Open Docker Desktop, go to `Settings` and confirm the following: + - General: `Use the WSL 2 based engine` is enabled. + - Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled. + +
+ +## Start the App The easiest way to run OpenHands is in Docker. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index b7def32dd68f..a8d88d9dfca1 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -5,7 +5,7 @@ const sidebars: SidebarsConfig = { docsSidebar: [ { type: 'doc', - label: 'Installation', + label: 'Running OpenHands', id: 'usage/installation', }, { diff --git a/evaluation/utils/shared.py b/evaluation/utils/shared.py index 5eafac1db61e..0f8ac8fa8332 100644 --- a/evaluation/utils/shared.py +++ b/evaluation/utils/shared.py @@ -355,7 +355,9 @@ def _process_instance_wrapper( ) # e is likely an EvalException, so we can't directly infer it from type # but rather check if it's a fatal error - if is_fatal_runtime_error(str(e)): + # But it can also be AgentRuntime**Error (e.g., swe_bench/eval_infer.py) + _error_str = type(e).__name__ + ': ' + str(e) + if is_fatal_runtime_error(_error_str): runtime_failure_count += 1 msg += f'Runtime disconnected error detected for instance {instance.instance_id}, runtime failure count: {runtime_failure_count}' msg += '\n' + '-' * 10 + '\n' @@ -531,6 +533,7 @@ def is_fatal_runtime_error(error: str | None) -> bool: return False FATAL_RUNTIME_ERRORS = [ + AgentRuntimeTimeoutError, AgentRuntimeUnavailableError, AgentRuntimeDisconnectedError, AgentRuntimeNotFoundError, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 48bb85cd4799..70068c558317 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -43,7 +43,7 @@ "sirv-cli": "^3.0.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.6.0", - "vite": "^6.0.7", + "vite": "^5.4.11", "web-vitals": "^3.5.2", "ws": "^8.18.0" }, @@ -826,378 +826,371 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -8340,42 +8333,41 @@ } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -16720,19 +16712,20 @@ } }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -16741,25 +16734,19 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "terser": "^5.4.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "jiti": { - "optional": true - }, "less": { "optional": true }, @@ -16780,12 +16767,6 @@ }, "terser": { "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, diff --git a/frontend/package.json b/frontend/package.json index 14566f3be94b..e9c61b2d8432 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,7 @@ "sirv-cli": "^3.0.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^2.6.0", - "vite": "^6.0.7", + "vite": "^5.4.11", "web-vitals": "^3.5.2", "ws": "^8.18.0" }, diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index 521968c88d12..87bdd08173b9 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -12,6 +12,7 @@ ) from openhands.controller.agent import Agent +from openhands.controller.replay import ReplayManager from openhands.controller.state.state import State, TrafficControlState from openhands.controller.stuck import StuckDetector from openhands.core.config import AgentConfig, LLMConfig @@ -90,6 +91,7 @@ def __init__( is_delegate: bool = False, headless_mode: bool = True, status_callback: Callable | None = None, + replay_events: list[Event] | None = None, ): """Initializes a new instance of the AgentController class. @@ -108,6 +110,7 @@ def __init__( is_delegate: Whether this controller is a delegate. headless_mode: Whether the agent is run in headless mode. status_callback: Optional callback function to handle status updates. + replay_events: A list of logs to replay. """ self.id = sid self.agent = agent @@ -139,6 +142,9 @@ def __init__( self._stuck_detector = StuckDetector(self.state) self.status_callback = status_callback + # replay-related + self._replay_manager = ReplayManager(replay_events) + async def close(self) -> None: """Closes the agent controller, canceling any ongoing tasks and unsubscribing from the event stream. @@ -234,6 +240,11 @@ async def _step_with_exception_handling(self): await self._react_to_exception(reported) def should_step(self, event: Event) -> bool: + """ + Whether the agent should take a step based on an event. In general, + the agent should take a step if it receives a message from the user, + or observes something in the environment (after acting). + """ # it might be the delegate's day in the sun if self.delegate is not None: return False @@ -641,42 +652,50 @@ async def _step(self) -> None: self.update_state_before_step() action: Action = NullAction() - try: - action = self.agent.step(self.state) - if action is None: - raise LLMNoActionError('No action was returned') - except ( - LLMMalformedActionError, - LLMNoActionError, - LLMResponseError, - FunctionCallValidationError, - FunctionCallNotExistsError, - ) as e: - self.event_stream.add_event( - ErrorObservation( - content=str(e), - ), - EventSource.AGENT, - ) - return - except (ContextWindowExceededError, BadRequestError) as e: - # FIXME: this is a hack until a litellm fix is confirmed - # Check if this is a nested context window error - error_str = str(e).lower() - if ( - 'contextwindowexceedederror' in error_str - or 'prompt is too long' in error_str - or isinstance(e, ContextWindowExceededError) - ): - # When context window is exceeded, keep roughly half of agent interactions - self.state.history = self._apply_conversation_window(self.state.history) - # Save the ID of the first event in our truncated history for future reloading - if self.state.history: - self.state.start_id = self.state.history[0].id - # Don't add error event - let the agent retry with reduced context + if self._replay_manager.should_replay(): + # in replay mode, we don't let the agent to proceed + # instead, we replay the action from the replay trajectory + action = self._replay_manager.step() + else: + try: + action = self.agent.step(self.state) + if action is None: + raise LLMNoActionError('No action was returned') + except ( + LLMMalformedActionError, + LLMNoActionError, + LLMResponseError, + FunctionCallValidationError, + FunctionCallNotExistsError, + ) as e: + self.event_stream.add_event( + ErrorObservation( + content=str(e), + ), + EventSource.AGENT, + ) return - raise + except (ContextWindowExceededError, BadRequestError) as e: + # FIXME: this is a hack until a litellm fix is confirmed + # Check if this is a nested context window error + error_str = str(e).lower() + if ( + 'contextwindowexceedederror' in error_str + or 'prompt is too long' in error_str + or isinstance(e, ContextWindowExceededError) + ): + # When context window is exceeded, keep roughly half of agent interactions + self.state.history = self._apply_conversation_window( + self.state.history + ) + + # Save the ID of the first event in our truncated history for future reloading + if self.state.history: + self.state.start_id = self.state.history[0].id + # Don't add error event - let the agent retry with reduced context + return + raise if action.runnable: if self.state.confirmation_mode and ( diff --git a/openhands/controller/replay.py b/openhands/controller/replay.py new file mode 100644 index 000000000000..c960d4a1fb6d --- /dev/null +++ b/openhands/controller/replay.py @@ -0,0 +1,52 @@ +from openhands.core.logger import openhands_logger as logger +from openhands.events.action.action import Action +from openhands.events.event import Event, EventSource + + +class ReplayManager: + """ReplayManager manages the lifecycle of a replay session of a given trajectory. + + Replay manager keeps track of a list of events, replays actions, and ignore + messages and observations. It could lead to unexpected or even errorneous + results if any action is non-deterministic, or if the initial state before + the replay session is different from the initial state of the trajectory. + """ + + def __init__(self, replay_events: list[Event] | None): + if replay_events: + logger.info(f'Replay logs loaded, events length = {len(replay_events)}') + self.replay_events = replay_events + self.replay_mode = bool(replay_events) + self.replay_index = 0 + + def _replayable(self) -> bool: + return ( + self.replay_events is not None + and self.replay_index < len(self.replay_events) + and isinstance(self.replay_events[self.replay_index], Action) + and self.replay_events[self.replay_index].source != EventSource.USER + ) + + def should_replay(self) -> bool: + """ + Whether the controller is in trajectory replay mode, and the replay + hasn't finished. Note: after the replay is finished, the user and + the agent could continue to message/act. + + This method also moves "replay_index" to the next action, if applicable. + """ + if not self.replay_mode: + return False + + assert self.replay_events is not None + while self.replay_index < len(self.replay_events) and not self._replayable(): + self.replay_index += 1 + + return self._replayable() + + def step(self) -> Action: + assert self.replay_events is not None + event = self.replay_events[self.replay_index] + assert isinstance(event, Action) + self.replay_index += 1 + return event diff --git a/openhands/core/config/app_config.py b/openhands/core/config/app_config.py index 468d37572fe2..8c995d1ee3db 100644 --- a/openhands/core/config/app_config.py +++ b/openhands/core/config/app_config.py @@ -28,6 +28,7 @@ class AppConfig(BaseModel): file_store: Type of file store to use. file_store_path: Path to the file store. save_trajectory_path: Either a folder path to store trajectories with auto-generated filenames, or a designated trajectory file path. + replay_trajectory_path: Path to load trajectory and replay. If provided, trajectory would be replayed first before user's instruction. workspace_base: Base path for the workspace. Defaults to `./workspace` as absolute path. workspace_mount_path: Path to mount the workspace. Defaults to `workspace_base`. workspace_mount_path_in_sandbox: Path to mount the workspace in sandbox. Defaults to `/workspace`. @@ -55,6 +56,7 @@ class AppConfig(BaseModel): file_store: str = Field(default='local') file_store_path: str = Field(default='/tmp/openhands_file_store') save_trajectory_path: str | None = Field(default=None) + 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') diff --git a/openhands/core/main.py b/openhands/core/main.py index 31dd3d7d6ce8..7abee764fccf 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -1,6 +1,8 @@ import asyncio import json import os +import sys +from pathlib import Path from typing import Callable, Protocol import openhands.agenthub # noqa F401 (we import this to get the agents registered) @@ -22,10 +24,11 @@ ) from openhands.core.utils.io import read_input, read_task from openhands.events import EventSource, EventStreamSubscriber -from openhands.events.action import MessageAction +from openhands.events.action import MessageAction, NullAction from openhands.events.action.action import Action from openhands.events.event import Event from openhands.events.observation import AgentStateChangedObservation +from openhands.events.serialization import event_from_dict from openhands.events.serialization.event import event_to_trajectory from openhands.runtime.base import Runtime @@ -75,7 +78,17 @@ async def run_controller( if agent is None: agent = create_agent(runtime, config) - controller, initial_state = create_controller(agent, runtime, config) + replay_events: list[Event] | None = None + if config.replay_trajectory_path: + logger.info('Trajectory replay is enabled') + assert isinstance(initial_user_action, NullAction) + replay_events, initial_user_action = load_replay_log( + config.replay_trajectory_path + ) + + controller, initial_state = create_controller( + agent, runtime, config, replay_events=replay_events + ) assert isinstance( initial_user_action, Action @@ -168,6 +181,40 @@ def auto_continue_response( return message +def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]: + """ + Load trajectory from given path, serialize it to a list of events, and return + two things: + 1) A list of events except the first action + 2) First action (user message, a.k.a. initial task) + """ + try: + path = Path(trajectory_path).resolve() + + if not path.exists(): + raise ValueError(f'Trajectory file not found: {path}') + + if not path.is_file(): + raise ValueError(f'Trajectory path is a directory, not a file: {path}') + + with open(path, 'r', encoding='utf-8') as file: + data = json.load(file) + if not isinstance(data, list): + raise ValueError( + f'Expected a list in {path}, got {type(data).__name__}' + ) + events = [] + for item in data: + event = event_from_dict(item) + # cannot add an event with _id to event stream + event._id = None # type: ignore[attr-defined] + events.append(event) + assert isinstance(events[0], MessageAction) + return events[1:], events[0] + except json.JSONDecodeError as e: + raise ValueError(f'Invalid JSON format in {trajectory_path}: {e}') + + if __name__ == '__main__': args = parse_arguments() @@ -175,6 +222,12 @@ def auto_continue_response( # Read task from file, CLI args, or stdin task_str = read_task(args, config.cli_multiline_input) + + if config.replay_trajectory_path: + if task_str: + raise ValueError( + 'User-specified task is not supported under trajectory replay mode' + ) if not task_str: raise ValueError('No task provided. Please specify a task through -t, -f.') diff --git a/openhands/core/setup.py b/openhands/core/setup.py index 28888478017a..82bdaf0c204b 100644 --- a/openhands/core/setup.py +++ b/openhands/core/setup.py @@ -11,6 +11,7 @@ ) from openhands.core.logger import openhands_logger as logger from openhands.events import EventStream +from openhands.events.event import Event from openhands.llm.llm import LLM from openhands.runtime import get_runtime_cls from openhands.runtime.base import Runtime @@ -78,7 +79,11 @@ def create_agent(runtime: Runtime, config: AppConfig) -> Agent: def create_controller( - agent: Agent, runtime: Runtime, config: AppConfig, headless_mode: bool = True + agent: Agent, + runtime: Runtime, + config: AppConfig, + headless_mode: bool = True, + replay_events: list[Event] | None = None, ) -> Tuple[AgentController, State | None]: event_stream = runtime.event_stream initial_state = None @@ -101,6 +106,7 @@ def create_controller( initial_state=initial_state, headless_mode=headless_mode, confirmation_mode=config.security.confirmation_mode, + replay_events=replay_events, ) return (controller, initial_state) diff --git a/openhands/events/event.py b/openhands/events/event.py index 1bdece59eb75..9d7af19160ca 100644 --- a/openhands/events/event.py +++ b/openhands/events/event.py @@ -24,6 +24,8 @@ class FileReadSource(str, Enum): @dataclass class Event: + INVALID_ID = -1 + @property def message(self) -> str | None: if hasattr(self, '_message'): @@ -34,7 +36,7 @@ def message(self) -> str | None: def id(self) -> int: if hasattr(self, '_id'): return self._id # type: ignore[attr-defined] - return -1 + return Event.INVALID_ID @property def timestamp(self): diff --git a/openhands/events/observation/browse.py b/openhands/events/observation/browse.py index 2ab73f047d8a..bc347d657473 100644 --- a/openhands/events/observation/browse.py +++ b/openhands/events/observation/browse.py @@ -12,7 +12,7 @@ class BrowserOutputObservation(Observation): url: str trigger_by_action: str - screenshot: str = field(repr=False) # don't show in repr + screenshot: str = field(repr=False, default='') # don't show in repr error: bool = False observation: str = ObservationType.BROWSE # do not include in the memory diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 54947720204f..3deb7ba40814 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -60,6 +60,12 @@ def __init__( ) self.session.headers.update({'X-API-Key': self.config.sandbox.api_key}) + if self.config.workspace_base is not None: + self.log( + 'debug', + 'Setting workspace_base is not supported in the remote runtime.', + ) + self.runtime_builder = RemoteRuntimeBuilder( self.config.sandbox.remote_runtime_api_url, self.config.sandbox.api_key, @@ -70,12 +76,6 @@ def __init__( self.available_hosts: dict[str, int] = {} self._runtime_initialized: bool = False - if self.config.workspace_base is not None: - self.log( - 'debug', - 'Setting workspace_base is not supported in the remote runtime.', - ) - def log(self, level: str, message: str) -> None: message = f'[runtime session_id={self.sid} runtime_id={self.runtime_id or "unknown"}] {message}' getattr(logger, level)(message, stacklevel=2) @@ -230,7 +230,7 @@ def _start_runtime(self): f'Runtime started. URL: {self.runtime_url}', ) except requests.HTTPError as e: - self.log('error', f'Unable to start runtime: {e}') + self.log('error', f'Unable to start runtime: {str(e)}') raise AgentRuntimeUnavailableError() from e def _resume_runtime(self): @@ -315,10 +315,11 @@ def _wait_until_alive_impl(self): self.check_if_alive() except requests.HTTPError as e: self.log( - 'warning', f"Runtime /alive failed, but pod says it's ready: {e}" + 'warning', + f"Runtime /alive failed, but pod says it's ready: {str(e)}", ) raise AgentRuntimeNotReadyError( - f'Runtime /alive failed to respond with 200: {e}' + f'Runtime /alive failed to respond with 200: {str(e)}' ) return elif ( @@ -363,6 +364,7 @@ def close(self): ): self.log('debug', 'Runtime stopped.') except Exception as e: + self.log('error', f'Unable to stop runtime: {str(e)}') raise e finally: super().close() @@ -403,8 +405,13 @@ def _send_action_server_request(self, method, url, **kwargs): f'Runtime is temporarily unavailable. This may be due to a restart or network issue, please try again. Original error: {e}' ) from e elif e.response.status_code == 503: - self.log('warning', 'Runtime appears to be paused. Resuming...') - self._resume_runtime() - return super()._send_action_server_request(method, url, **kwargs) + if self.config.sandbox.keep_runtime_alive: + self.log('warning', 'Runtime appears to be paused. Resuming...') + self._resume_runtime() + return super()._send_action_server_request(method, url, **kwargs) + else: + raise AgentRuntimeDisconnectedError( + f'Runtime is temporarily unavailable. This may be due to a restart or network issue, please try again. Original error: {e}' + ) from e else: raise e diff --git a/openhands/server/app.py b/openhands/server/app.py index 1ece8476151a..151e16eb3d56 100644 --- a/openhands/server/app.py +++ b/openhands/server/app.py @@ -9,6 +9,7 @@ ) import openhands.agenthub # noqa F401 (we import this to get the agents registered) +from openhands import __version__ from openhands.server.middleware import ( AttachConversationMiddleware, InMemoryRateLimiter, @@ -36,7 +37,12 @@ async def _lifespan(app: FastAPI): yield -app = FastAPI(lifespan=_lifespan) +app = FastAPI( + title='OpenHands', + description='OpenHands: Code Less, Make More', + version=__version__, + lifespan=_lifespan, +) app.add_middleware( LocalhostCORSMiddleware, allow_credentials=True, diff --git a/openhands/server/settings.py b/openhands/server/settings.py index 6732ea2445d4..19174706e497 100644 --- a/openhands/server/settings.py +++ b/openhands/server/settings.py @@ -1,6 +1,11 @@ +from __future__ import annotations + from pydantic import BaseModel, SecretStr, SerializationInfo, field_serializer from pydantic.json import pydantic_encoder +from openhands.core.config.llm_config import LLMConfig +from openhands.core.config.utils import load_app_config + class Settings(BaseModel): """ @@ -28,3 +33,24 @@ def llm_api_key_serializer(self, llm_api_key: SecretStr, info: SerializationInfo return llm_api_key.get_secret_value() return pydantic_encoder(llm_api_key) + + @staticmethod + def from_config() -> Settings | None: + app_config = load_app_config() + llm_config: LLMConfig = app_config.get_llm_config() + if llm_config.api_key is None: + # If no api key has been set, we take this to mean that there is no reasonable default + return None + security = app_config.security + settings = Settings( + language='en', + agent=app_config.default_agent, + max_iterations=app_config.max_iterations, + security_analyzer=security.security_analyzer, + confirmation_mode=security.confirmation_mode, + llm_model=llm_config.model, + llm_api_key=llm_config.api_key, + llm_base_url=llm_config.base_url, + remote_runtime_resource_factor=app_config.sandbox.remote_runtime_resource_factor, + ) + return settings diff --git a/openhands/storage/settings/file_settings_store.py b/openhands/storage/settings/file_settings_store.py index d3cc08677078..eaf35554d7ae 100644 --- a/openhands/storage/settings/file_settings_store.py +++ b/openhands/storage/settings/file_settings_store.py @@ -23,7 +23,7 @@ async def load(self) -> Settings | None: settings = Settings(**kwargs) return settings except FileNotFoundError: - return None + return Settings.from_config() async def store(self, settings: Settings): json_str = settings.model_dump_json(context={'expose_secrets': True}) diff --git a/poetry.lock b/poetry.lock index 562f082c479b..c84eed65a096 100644 --- a/poetry.lock +++ b/poetry.lock @@ -170,13 +170,13 @@ files = [ [[package]] name = "anthropic" -version = "0.43.0" +version = "0.43.1" description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.8" files = [ - {file = "anthropic-0.43.0-py3-none-any.whl", hash = "sha256:f748a703f77b3244975e1aace3a935840dc653a4714fb6bba644f97cc76847b4"}, - {file = "anthropic-0.43.0.tar.gz", hash = "sha256:06801f01d317a431d883230024318d48981758058bf7e079f33fb11f64b5a5c1"}, + {file = "anthropic-0.43.1-py3-none-any.whl", hash = "sha256:20759c25cd0f4072eb966b0180a41c061c156473bbb674da6a3f1e92e1ad78f8"}, + {file = "anthropic-0.43.1.tar.gz", hash = "sha256:c7f13e4b7b515ac4a3111142310b214527c0fc561485e5bc9b582e49fe3adba2"}, ] [package.dependencies] @@ -552,17 +552,17 @@ files = [ [[package]] name = "boto3" -version = "1.36.1" +version = "1.36.2" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.36.1-py3-none-any.whl", hash = "sha256:eb21380d73fec6645439c0d802210f72a0cdb3295b02953f246ff53f512faa8f"}, - {file = "boto3-1.36.1.tar.gz", hash = "sha256:258ab77225a81d3cf3029c9afe9920cd9dec317689dfadec6f6f0a23130bb60a"}, + {file = "boto3-1.36.2-py3-none-any.whl", hash = "sha256:76cfc9a705be46e8d22607efacc8d688c064f923d785a01c00b28e9a96425d1a"}, + {file = "boto3-1.36.2.tar.gz", hash = "sha256:fde1c29996b77274a60b7bc9f741525afa6267bb1716eb644a764fb7c124a0d2"}, ] [package.dependencies] -botocore = ">=1.36.1,<1.37.0" +botocore = ">=1.36.2,<1.37.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.11.0,<0.12.0" @@ -571,13 +571,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.36.1" +version = "1.36.2" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.36.1-py3-none-any.whl", hash = "sha256:dec513b4eb8a847d79bbefdcdd07040ed9d44c20b0001136f0890a03d595705a"}, - {file = "botocore-1.36.1.tar.gz", hash = "sha256:f789a6f272b5b3d8f8756495019785e33868e5e00dd9662a3ee7959ac939bb12"}, + {file = "botocore-1.36.2-py3-none-any.whl", hash = "sha256:bc3b7e3b573a48af2bd7116b80fe24f9a335b0b67314dcb2697a327d009abf29"}, + {file = "botocore-1.36.2.tar.gz", hash = "sha256:a1fe6610983f0214b0c7655fe6990b6a731746baf305b182976fc7b568fc3cb0"}, ] [package.dependencies] @@ -3707,13 +3707,13 @@ types-tqdm = "*" [[package]] name = "litellm" -version = "1.58.2" +version = "1.59.0" description = "Library to easily interface with LLM API providers" optional = false python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" files = [ - {file = "litellm-1.58.2-py3-none-any.whl", hash = "sha256:51b14b2f5e30d2d41a76fbf926d7d882f1fddbbfda8812358cb4bb27d0d27692"}, - {file = "litellm-1.58.2.tar.gz", hash = "sha256:4e1b7191a86970bbacd30e5315d3b6a0f5fc75a99763c9164116de60c6ac0bf3"}, + {file = "litellm-1.59.0-py3-none-any.whl", hash = "sha256:b0c8bdee556d5dc2f9c703f7dc831574ea2e339d2e762dd626d014c170b8b587"}, + {file = "litellm-1.59.0.tar.gz", hash = "sha256:140eecb47952558414d00f7a259fe303fe5f0d073973a28f488fc6938cc45660"}, ] [package.dependencies] @@ -3731,7 +3731,7 @@ tokenizers = "*" [package.extras] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "resend (>=0.8.0,<0.9.0)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=22.0.0,<23.0.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.22.0,<0.23.0)", "uvloop (>=0.21.0,<0.22.0)"] +proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=22.0.0,<23.0.0)", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)"] [[package]] name = "llama-cloud" @@ -4441,13 +4441,13 @@ files = [ [[package]] name = "minio" -version = "7.2.14" +version = "7.2.15" description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" optional = false python-versions = ">=3.9" files = [ - {file = "minio-7.2.14-py3-none-any.whl", hash = "sha256:868dfe907e1702ce4bec86df1f3ced577a73ca85f344ef898d94fe2b5237f8c1"}, - {file = "minio-7.2.14.tar.gz", hash = "sha256:f5c24bf236fefd2edc567cd4455dc49a11ad8ff7ac984bb031b849d82f01222a"}, + {file = "minio-7.2.15-py3-none-any.whl", hash = "sha256:c06ef7a43e5d67107067f77b6c07ebdd68733e5aa7eed03076472410ca19d876"}, + {file = "minio-7.2.15.tar.gz", hash = "sha256:5247df5d4dca7bfa4c9b20093acd5ad43e82d8710ceb059d79c6eea970f49f79"}, ] [package.dependencies] @@ -4583,12 +4583,12 @@ type = ["mypy (==1.11.2)"] [[package]] name = "modal" -version = "0.72.21" +version = "0.72.33" description = "Python client library for Modal" optional = false python-versions = ">=3.9" files = [ - {file = "modal-0.72.21-py3-none-any.whl", hash = "sha256:16ad3b1f9e5e8a7d2b6aa9746f8e3315c44f8901e5e873679ceacbdd956a7ba1"}, + {file = "modal-0.72.33-py3-none-any.whl", hash = "sha256:ce8ac919c4ae81563ea8a122b1d9cd23c98335b2795b82cb16b9fe96c3d9fd4b"}, ] [package.dependencies] @@ -5076,66 +5076,66 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "2.2.1" +version = "2.2.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440"}, - {file = "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab"}, - {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675"}, - {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308"}, - {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957"}, - {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf"}, - {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2"}, - {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528"}, - {file = "numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95"}, - {file = "numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf"}, - {file = "numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484"}, - {file = "numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7"}, - {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb"}, - {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5"}, - {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73"}, - {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591"}, - {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8"}, - {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0"}, - {file = "numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd"}, - {file = "numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16"}, - {file = "numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab"}, - {file = "numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa"}, - {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315"}, - {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355"}, - {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7"}, - {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d"}, - {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51"}, - {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046"}, - {file = "numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2"}, - {file = "numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8"}, - {file = "numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780"}, - {file = "numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821"}, - {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e"}, - {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348"}, - {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59"}, - {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af"}, - {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51"}, - {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716"}, - {file = "numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e"}, - {file = "numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60"}, - {file = "numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e"}, - {file = "numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712"}, - {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008"}, - {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84"}, - {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631"}, - {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d"}, - {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5"}, - {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71"}, - {file = "numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2"}, - {file = "numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268"}, - {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3"}, - {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964"}, - {file = "numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800"}, - {file = "numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e"}, - {file = "numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"}, + {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"}, + {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"}, + {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"}, + {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"}, + {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"}, + {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"}, + {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"}, + {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"}, + {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"}, + {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"}, + {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"}, ] [[package]] @@ -5364,13 +5364,13 @@ sympy = "*" [[package]] name = "openai" -version = "1.59.7" +version = "1.59.9" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" files = [ - {file = "openai-1.59.7-py3-none-any.whl", hash = "sha256:cfa806556226fa96df7380ab2e29814181d56fea44738c2b0e581b462c268692"}, - {file = "openai-1.59.7.tar.gz", hash = "sha256:043603def78c00befb857df9f0a16ee76a3af5984ba40cb7ee5e2f40db4646bf"}, + {file = "openai-1.59.9-py3-none-any.whl", hash = "sha256:61a0608a1313c08ddf92fe793b6dbd1630675a1fe3866b2f96447ce30050c448"}, + {file = "openai-1.59.9.tar.gz", hash = "sha256:ec1a20b0351b4c3e65c6292db71d8233515437c6065efd4fd50edeb55df5f5d2"}, ] [package.dependencies] diff --git a/tests/unit/test_file_settings_store.py b/tests/unit/test_file_settings_store.py index 347754035aa1..4dd0e1d360a7 100644 --- a/tests/unit/test_file_settings_store.py +++ b/tests/unit/test_file_settings_store.py @@ -20,8 +20,12 @@ def file_settings_store(mock_file_store): @pytest.mark.asyncio async def test_load_nonexistent_data(file_settings_store): - file_settings_store.file_store.read.side_effect = FileNotFoundError() - assert await file_settings_store.load() is None + with patch( + 'openhands.server.settings.load_app_config', + MagicMock(return_value=AppConfig()), + ): + file_settings_store.file_store.read.side_effect = FileNotFoundError() + assert await file_settings_store.load() is None @pytest.mark.asyncio diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py new file mode 100644 index 000000000000..a785a75d08f1 --- /dev/null +++ b/tests/unit/test_settings.py @@ -0,0 +1,67 @@ +from unittest.mock import patch + +from pydantic import SecretStr + +from openhands.core.config.app_config import AppConfig +from openhands.core.config.llm_config import LLMConfig +from openhands.core.config.sandbox_config import SandboxConfig +from openhands.core.config.security_config import SecurityConfig +from openhands.server.settings import Settings + + +def test_settings_from_config(): + # Mock configuration + mock_app_config = AppConfig( + default_agent='test-agent', + max_iterations=100, + security=SecurityConfig( + security_analyzer='test-analyzer', confirmation_mode=True + ), + llms={ + 'llm': LLMConfig( + model='test-model', + api_key=SecretStr('test-key'), + base_url='https://test.example.com', + ) + }, + sandbox=SandboxConfig(remote_runtime_resource_factor=2), + ) + + with patch( + 'openhands.server.settings.load_app_config', return_value=mock_app_config + ): + settings = Settings.from_config() + + assert settings is not None + assert settings.language == 'en' + assert settings.agent == 'test-agent' + assert settings.max_iterations == 100 + assert settings.security_analyzer == 'test-analyzer' + assert settings.confirmation_mode is True + assert settings.llm_model == 'test-model' + assert settings.llm_api_key.get_secret_value() == 'test-key' + assert settings.llm_base_url == 'https://test.example.com' + assert settings.remote_runtime_resource_factor == 2 + + +def test_settings_from_config_no_api_key(): + # Mock configuration without API key + mock_app_config = AppConfig( + default_agent='test-agent', + max_iterations=100, + security=SecurityConfig( + security_analyzer='test-analyzer', confirmation_mode=True + ), + llms={ + 'llm': LLMConfig( + model='test-model', api_key=None, base_url='https://test.example.com' + ) + }, + sandbox=SandboxConfig(remote_runtime_resource_factor=2), + ) + + with patch( + 'openhands.server.settings.load_app_config', return_value=mock_app_config + ): + settings = Settings.from_config() + assert settings is None