Compare commits

...

20 Commits

Author SHA1 Message Date
openhands
f486028530 Fix circular imports in service_types.py and http.py 2025-06-10 15:53:18 +00:00
openhands
77f2d9b0d4 Fix: Update imports to resolve circular dependencies 2025-05-28 23:36:30 +00:00
openhands
396e4b07f3 Fix: Resolve circular imports in core modules 2025-05-28 23:17:34 +00:00
openhands
7c3c5a2e95 Fix: Update test_core_conversation.py to use create_agent instead of Agent 2025-05-28 23:06:57 +00:00
openhands
09a4c1604e Add Conversation and OpenHands classes for conversation management 2025-05-28 21:57:13 +00:00
Graham Neubig
6491142364 Fix KeyError on router error logging (#8769) 2025-05-28 19:59:18 +00:00
Robert Brennan
205f0234e8 Rename Conversation to ServerConversation and AppConfig to OpenHandsConfig (#8754)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 21:48:34 +02:00
Robert Brennan
c76809a766 Revert "Add username parameter to AsyncBashSession" (#8767) 2025-05-28 14:28:26 -04:00
chuckbutkus
9f86f731a7 Update login (#8743)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 17:53:35 +00:00
sp.wack
6fe5da810b fix(frontend): Handle assistant messages at the top (#8766) 2025-05-28 17:33:05 +00:00
dependabot[bot]
52a1e94335 chore(deps): bump the docusaurus group in /docs with 7 updates (#8758)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 21:24:54 +04:00
sp.wack
3e0532e8b9 fix(frontend): Only clear UI messages on cid change (#8762) 2025-05-28 15:31:34 +00:00
tofarr
90c440d709 Add HTTP FileStore implementation (#8751)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 08:17:26 -06:00
Robert Brennan
82657b7ba1 Add username parameter to AsyncBashSession (#8746)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 10:16:45 -04:00
Engel Nyst
3c51600260 Add vscode rules/ignores to .gitignore (#8755) 2025-05-28 15:42:11 +02:00
sp.wack
b5f2a04ea2 Add refill link to out-of-credits error message (#8737)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 17:23:14 +04:00
sp.wack
155615bbb1 hotfix(frontend): Invalidate and refetch git changes if messages aren't being received (#8752) 2025-05-28 13:22:15 +00:00
Kent Johnson
4b6f2aeb4d docs: Mention dev container in Development.md (#8726) 2025-05-27 18:29:05 -04:00
Rohit Malhotra
0023eb0982 (Hotfix): Handle cases where user secrets store doesn't exist (#8745)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-27 18:26:36 -04:00
Robert Brennan
c3ab4b480b Fix TypeError in list_files endpoint while preserving router_error_log functionality (#8744)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-27 18:25:07 -04:00
140 changed files with 3105 additions and 2543 deletions

15
.gitignore vendored
View File

@@ -161,7 +161,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.cursorignore
# VS Code: Ignore all but certain files that specify repo-specific settings.
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
@@ -171,6 +170,20 @@ cython_debug/
!.vscode/settings.json
!.vscode/tasks.json
# VS Code extensions/forks:
.cursorignore
.rooignore
.clineignore
.windsurfignore
.cursorrules
.roorules
.clinerules
.windsurfrules
.cursor/rules
.roo/rules
.cline/rules
.windsurf/rules
# evaluation
evaluation/evaluation_outputs
evaluation/outputs

View File

@@ -1,8 +1,10 @@
# Development Guide
This guide is for people working on OpenHands and editing the source code.
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project
initially before moving on. Otherwise, you can clone the OpenHands project directly.
If you wish to contribute your changes, check out the
[CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md)
on how to clone and setup the project initially before moving on. Otherwise,
you can clone the OpenHands project directly.
## Start the Server for Development
@@ -19,9 +21,20 @@ initially before moving on. Otherwise, you can clone the OpenHands project direc
Make sure you have all these dependencies installed before moving on to `make build`.
#### Dev container
There is a [dev container](https://containers.dev/) available which provides a
pre-configured environment with all the necessary dependencies installed if you
are using a [supported editor or tool](https://containers.dev/supporting). For
example, if you are using Visual Studio Code (VS Code) with the
[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
extension installed, you can open the project in a dev container by using the
_Dev Container: Reopen in Container_ command from the Command Palette
(Ctrl+Shift+P).
#### Develop without sudo access
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
`conda` or `mamba` to manage the packages for you:
```bash
@@ -37,7 +50,7 @@ mamba install conda-forge::poetry
### 2. Build and Setup The Environment
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
that OpenHands is ready to run on your system:
```bash
@@ -54,11 +67,11 @@ To configure the LM of your choice, run:
make setup-config
```
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
please set the model in the UI.
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
variables in your terminal. The final configurations are set from highest to lowest priority:
Environment variables > config.toml variables > default variables
@@ -77,14 +90,14 @@ make run
#### Option B: Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
components or interface enhancements.
```bash
make start-frontend
@@ -120,7 +133,7 @@ poetry run pytest ./tests/unit/test_*.py
### 9. Use existing Docker image
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
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.39-nikolaik`

3594
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,10 @@
},
"// Note": "The OpenAPI spec is stored in docs/static/openapi.json so it's accessible at /openapi.json in the deployed site",
"dependencies": {
"@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-pages": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.7.0",
"@docusaurus/core": "^3.8.0",
"@docusaurus/plugin-content-pages": "^3.8.0",
"@docusaurus/preset-classic": "^3.8.0",
"@docusaurus/theme-mermaid": "^3.8.0",
"@mdx-js/react": "^3.1.0",
"@node-rs/jieba": "^2.0.1",
"clsx": "^2.0.0",
@@ -33,7 +33,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/tsconfig": "^3.8.0",
"@docusaurus/types": "^3.5.1",
"swagger-cli": "^4.0.4",
"swagger-ui-dist": "^5.22.0",

View File

@@ -17,7 +17,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -59,10 +59,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -39,11 +39,11 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-slim'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),

View File

@@ -24,7 +24,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
load_from_toml,
parse_arguments,
@@ -46,10 +46,10 @@ SKIP_NUM = (
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.11-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),

View File

@@ -22,7 +22,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -55,12 +55,12 @@ FILE_EXT_MAP = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
BIOCODER_BENCH_CONTAINER_IMAGE = 'public.ecr.aws/i5g0m1f6/eval_biocoder:v1.0'
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -70,11 +70,11 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -18,7 +18,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -33,13 +33,13 @@ SUPPORTED_AGENT_CLS = {'CodeActAgent'}
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
assert metadata.max_iterations == 1, (
'max_iterations must be 1 for browsing delegation evaluation.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -101,7 +101,7 @@ def get_instance_docker_image(repo_name: str) -> str:
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
repo_name = instance['repo'].split('/')[1]
base_container_image = get_instance_docker_image(repo_name)
logger.info(
@@ -113,7 +113,7 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,

View File

@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -61,10 +61,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -47,10 +47,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -19,7 +19,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -39,10 +39,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -37,7 +37,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -60,10 +60,10 @@ ACTION_FORMAT = """
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -81,10 +81,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -19,10 +19,10 @@ from evaluation.utils.shared import (
make_metadata,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
load_app_config,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
@@ -34,10 +34,10 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -53,7 +53,7 @@ def get_config(
return config
config = load_app_config()
config = load_openhands_config()
def load_bench_config():

View File

@@ -29,10 +29,10 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
load_app_config,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -44,10 +44,10 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -63,7 +63,7 @@ def get_config(
return config
config = load_app_config()
config = load_openhands_config()
def load_bench_config():

View File

@@ -17,7 +17,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -44,14 +44,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-logic-reasoning:v1.0'
sandbox_config.runtime_extra_deps = (
'$OH_INTERPRETER_PATH -m pip install scitools-pyke'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -54,10 +54,10 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),

View File

@@ -22,7 +22,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -102,14 +102,14 @@ def load_incontext_example(task_name: str, with_tool: bool = True):
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-mint:v1.0'
sandbox_config.runtime_extra_deps = (
f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -4,11 +4,11 @@ import pprint
import tqdm
from openhands.core.config import get_llm_config_arg, get_parser, load_app_config
from openhands.core.config import get_llm_config_arg, get_parser, load_openhands_config
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
config = load_app_config()
config = load_openhands_config()
def extract_test_results(res_file_path: str) -> tuple[list[str], list[str]]:

View File

@@ -33,10 +33,10 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
load_app_config,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -45,7 +45,7 @@ from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
config = load_app_config()
config = load_openhands_config()
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -76,10 +76,10 @@ ID2CONDA = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -28,8 +28,8 @@ from evaluation.utils.shared import (
run_evaluation,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
@@ -73,7 +73,7 @@ def process_git_patch(patch):
return patch
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -87,7 +87,7 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,

View File

@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -314,7 +314,7 @@ def get_instance_docker_image(instance: pd.Series):
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
SWE_BENCH_CONTAINER_IMAGE = 'ghcr.io/opendevin/eval-swe-bench:full-v1.2.1'
if USE_INSTANCE_IMAGE:
# We use a different instance image for the each instance of swe-bench eval
@@ -340,7 +340,7 @@ def get_config(
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,

View File

@@ -20,7 +20,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -58,12 +58,12 @@ def format_task_dict(example, use_knowledge):
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = (
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),

View File

@@ -24,8 +24,8 @@ from evaluation.utils.shared import (
run_evaluation,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
@@ -69,7 +69,7 @@ def process_git_patch(patch):
return patch
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -83,7 +83,7 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,

View File

@@ -40,12 +40,12 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.critic import AgentFinishedCritic
@@ -220,7 +220,7 @@ def get_instance_docker_image(
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
use_swebench_official_image = 'swe-gym' not in metadata.dataset.lower()
base_container_image = get_instance_docker_image(
@@ -244,7 +244,7 @@ def get_config(
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -721,15 +721,16 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
# repos for the swe-bench instances:
# ['astropy/astropy', 'django/django', 'matplotlib/matplotlib', 'mwaskom/seaborn', 'pallets/flask', 'psf/requests', 'pydata/xarray', 'pylint-dev/pylint', 'pytest-dev/pytest', 'scikit-learn/scikit-learn', 'sphinx-doc/sphinx', 'sympy/sympy']
selected_repos = data['selected_repos']
if isinstance(selected_repos, str): selected_repos = [selected_repos]
if isinstance(selected_repos, str):
selected_repos = [selected_repos]
assert isinstance(selected_repos, list)
logger.info(
f'Filtering {selected_repos} tasks from "selected_repos"...'
)
subset = dataset[dataset["repo"].isin(selected_repos)]
subset = dataset[dataset['repo'].isin(selected_repos)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
@@ -806,7 +807,9 @@ if __name__ == '__main__':
else:
# If no specific condenser config is provided via env var, default to NoOpCondenser
condenser_config = NoOpCondenserConfig()
logger.debug('No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.')
logger.debug(
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
)
details = {'mode': args.mode}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)

View File

@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -58,7 +58,7 @@ def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
_get_swebench_workspace_dir_name(instance)
instruction = f"""
Consider the following issue description:
@@ -168,7 +168,7 @@ def get_instance_docker_image(instance_id: str, official_image: bool = False) ->
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
@@ -197,7 +197,7 @@ def get_config(
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
}
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -348,13 +348,13 @@ def initialize_runtime(
# Check if an existing graph index file is available
graph_index_file_path = os.path.join(
INDEX_BASE_DIR, 'graph_index_v2.3', f"{instance['instance_id']}.pkl"
INDEX_BASE_DIR, 'graph_index_v2.3', f'{instance["instance_id"]}.pkl'
)
if INDEX_BASE_DIR and os.path.exists(graph_index_file_path):
logger.info(
f"Copying graph index from {graph_index_file_path} to /workspace/{workspace_dir_name}/_index_data/graph_index_v2.3"
f'Copying graph index from {graph_index_file_path} to /workspace/{workspace_dir_name}/_index_data/graph_index_v2.3'
)
runtime.copy_to(
graph_index_file_path,
f'/workspace/{workspace_dir_name}/_index_data/graph_index_v2.3',
@@ -364,9 +364,13 @@ def initialize_runtime(
)
obs = runtime.run_action(action)
bm25_index_dir = os.path.join(INDEX_BASE_DIR, 'BM25_index', instance['instance_id'])
bm25_index_dir = os.path.join(
INDEX_BASE_DIR, 'BM25_index', instance['instance_id']
)
runtime.copy_to(
bm25_index_dir, f'/workspace/{workspace_dir_name}/_index_data', recursive=True
bm25_index_dir,
f'/workspace/{workspace_dir_name}/_index_data',
recursive=True,
)
action = CmdRunAction(
command=f'mv _index_data/{instance["instance_id"]} _index_data/bm25_index'

View File

@@ -41,7 +41,7 @@ from evaluation.utils.shared import (
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.core.config import AppConfig, SandboxConfig, get_parser
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_parser
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
from openhands.events.action import CmdRunAction
@@ -52,13 +52,13 @@ DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/kdja
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
def get_config(instance: pd.Series) -> AppConfig:
def get_config(instance: pd.Series) -> OpenHandsConfig:
base_container_image = get_instance_docker_image(instance['instance_id_swebench'])
assert base_container_image, (
f'Invalid container image for instance {instance["instance_id_swebench"]}.'
)
logger.info(f'Using instance container image: {base_container_image}.')
return AppConfig(
return OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(

View File

@@ -35,7 +35,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
SandboxConfig,
get_llm_config_arg,
get_parser,
@@ -117,7 +117,7 @@ def get_instance_docker_image(instance_id: str) -> str:
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of TestGenEval
base_container_image = get_instance_docker_image(instance['instance_id_swebench'])
logger.info(
@@ -126,7 +126,7 @@ def get_config(
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,

View File

@@ -15,8 +15,8 @@ from browsing import pre_login
from evaluation.utils.shared import get_default_sandbox_config_for_eval
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_agent_config_arg,
get_llm_config_arg,
get_parser,
@@ -36,13 +36,13 @@ def get_config(
mount_path_on_host: str,
llm_config: LLMConfig,
agent_config: AgentConfig | None,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
config = AppConfig(
config = OpenHandsConfig(
run_as_openhands=False,
max_budget_per_task=4,
max_iterations=100,
@@ -126,7 +126,7 @@ def codeact_user_response(state: State) -> str:
def run_solver(
runtime: Runtime,
task_name: str,
config: AppConfig,
config: OpenHandsConfig,
dependencies: list[str],
save_final_state: bool,
state_dir: str,
@@ -274,7 +274,7 @@ if __name__ == '__main__':
temp_dir = os.path.abspath(os.getenv('TMPDIR'))
else:
temp_dir = tempfile.mkdtemp()
config: AppConfig = get_config(
config: OpenHandsConfig = get_config(
args.task_image_name, task_short_name, temp_dir, agent_llm_config, agent_config
)
runtime: Runtime = create_runtime(config)

View File

@@ -18,7 +18,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -40,10 +40,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
get_parser,
)
@@ -135,7 +135,7 @@ def get_instance_docker_image(instance_id: str, official_image: bool = False) ->
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
) -> OpenHandsConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
@@ -160,7 +160,7 @@ def get_config(
instance_id=instance['instance_id'],
)
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,

View File

@@ -20,7 +20,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -48,7 +48,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
base_url = os.environ.get('VISUALWEBARENA_BASE_URL', None)
openai_api_key = os.environ.get('OPENAI_API_KEY', None)
openai_base_url = os.environ.get('OPENAI_BASE_URL', None)
@@ -72,7 +72,7 @@ def get_config(
'VWA_WIKIPEDIA': f'{base_url}:8888',
'VWA_HOMEPAGE': f'{base_url}:4399',
}
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -19,7 +19,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -44,7 +44,7 @@ SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
base_url = os.environ.get('WEBARENA_BASE_URL', None)
openai_api_key = os.environ.get('OPENAI_API_KEY', None)
assert base_url is not None, 'WEBARENA_BASE_URL must be set'
@@ -64,7 +64,7 @@ def get_config(
'MAP': f'{base_url}:3000',
'HOMEPAGE': f'{base_url}:4399',
}
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',

View File

@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
OpenHandsConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -41,10 +41,10 @@ FAKE_RESPONSES = {
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> AppConfig:
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.platform = 'linux/amd64'
config = AppConfig(
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),

View File

@@ -2,9 +2,9 @@ import argparse
import pytest
from openhands.config import load_app_config
from openhands.config import load_openhands_config
config = load_app_config()
config = load_openhands_config()
if __name__ == '__main__':
"""Main entry point of the script.

View File

@@ -26,7 +26,6 @@ import { downloadTrajectory } from "#/utils/download-trajectory";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
import i18n from "#/i18n";
import { ErrorMessageBanner } from "./error-message-banner";
import { shouldRenderEvent } from "./event-content-helpers/should-render-event";
@@ -181,11 +180,7 @@ export function ChatInterface() {
{!hitBottom && <ScrollToBottomButton onClick={scrollDomToBottom} />}
</div>
{errorMessage && (
<ErrorMessageBanner
message={i18n.exists(errorMessage) ? t(errorMessage) : errorMessage}
/>
)}
{errorMessage && <ErrorMessageBanner message={errorMessage} />}
<InteractiveChatBox
onSubmit={handleSendMessage}

View File

@@ -1,3 +1,7 @@
import { Trans } from "react-i18next";
import { Link } from "react-router";
import i18n from "#/i18n";
interface ErrorMessageBannerProps {
message: string;
}
@@ -5,7 +9,23 @@ interface ErrorMessageBannerProps {
export function ErrorMessageBanner({ message }: ErrorMessageBannerProps) {
return (
<div className="w-full rounded-lg p-2 text-black border border-red-800 bg-red-500">
{message}
{i18n.exists(message) ? (
<Trans
i18nKey={message}
components={{
a: (
<Link
className="underline font-bold cursor-pointer"
to="/settings/billing"
>
link
</Link>
),
}}
/>
) : (
message
)}
</div>
);
}

View File

@@ -129,7 +129,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
isDisabled={isPending}
>
{isPending
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) || "Submitting..."
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL)
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
</BrandButton>
<BrandButton
@@ -144,8 +144,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
</div>
{isPending && (
<p className="text-sm text-center text-neutral-400">
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE) ||
"Submitting your feedback, please wait..."}
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE)}
</p>
)}
</form>

View File

@@ -9,7 +9,6 @@ import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
import { useAuthUrl } from "#/hooks/use-auth-url";
import { GetConfigResponse } from "#/api/open-hands.types";
import { LoginMethod, setLoginMethod } from "#/utils/local-storage";
interface AuthModalProps {
githubAuthUrl: string | null;
@@ -26,10 +25,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => {
if (githubAuthUrl) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITHUB);
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
}
@@ -37,10 +32,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitLabAuth = () => {
if (gitlabAuthUrl) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITLAB);
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
}

View File

@@ -166,6 +166,8 @@ export function WsClientProvider({
}
function handleMessage(event: Record<string, unknown>) {
handleAssistantMessage(event);
if (isOpenHandsEvent(event)) {
const isStatusUpdateError =
isStatusUpdate(event) && event.type === "error";
@@ -217,9 +219,14 @@ export function WsClientProvider({
isFileWriteAction(event) ||
isCommandAction(event)
) {
queryClient.removeQueries({
queryKey: ["file_changes", conversationId],
});
queryClient.invalidateQueries(
{
queryKey: ["file_changes", conversationId],
},
// Do not refetch if we are still receiving messages at a high rate (e.g., loading an existing conversation)
// This prevents unnecessary refetches when the user is still receiving messages
{ cancelRefetch: false },
);
// Invalidate file diff cache when a file is edited or written
if (!isCommandAction(event)) {
@@ -250,8 +257,6 @@ export function WsClientProvider({
if (!Number.isNaN(parseInt(event.id as string, 10))) {
lastEventRef.current = event;
}
handleAssistantMessage(event);
}
function handleDisconnect(data: unknown) {
@@ -284,14 +289,14 @@ export function WsClientProvider({
React.useEffect(() => {
lastEventRef.current = null;
}, [conversationId]);
React.useEffect(() => {
// reset events when conversationId changes
setEvents([]);
setParsedEvents([]);
setStatus(WsClientProviderStatus.DISCONNECTED);
}, [conversationId]);
React.useEffect(() => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}

View File

@@ -1,5 +1,4 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import { useConfig } from "../query/use-config";
@@ -8,7 +7,6 @@ import { clearLoginData } from "#/utils/local-storage";
export const useLogout = () => {
const queryClient = useQueryClient();
const { data: config } = useConfig();
const navigate = useNavigate();
return useMutation({
mutationFn: () => OpenHands.logout(config?.APP_MODE ?? "oss"),
@@ -24,7 +22,6 @@ export const useLogout = () => {
}
posthog.reset();
await navigate("/");
// Refresh the page after all logout logic is completed
window.location.reload();

View File

@@ -0,0 +1,49 @@
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router";
import { useIsAuthed } from "./query/use-is-authed";
import { LoginMethod, setLoginMethod } from "#/utils/local-storage";
import { useConfig } from "./query/use-config";
/**
* Hook to handle authentication callback and set login method after successful authentication
*/
export const useAuthCallback = () => {
const location = useLocation();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
const { data: config } = useConfig();
const navigate = useNavigate();
useEffect(() => {
// Only run in SAAS mode
if (config?.APP_MODE !== "saas") {
return;
}
// Wait for auth to load
if (isAuthLoading) {
return;
}
// Only set login method if authentication was successful
if (!isAuthed) {
return;
}
// Check if we have a login_method query parameter
const searchParams = new URLSearchParams(location.search);
const loginMethod = searchParams.get("login_method");
// Set the login method if it's valid
if (
loginMethod === LoginMethod.GITHUB ||
loginMethod === LoginMethod.GITLAB
) {
setLoginMethod(loginMethod as LoginMethod);
// Clean up the URL by removing the login_method parameter
searchParams.delete("login_method");
const newUrl = `${location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
navigate(newUrl, { replace: true });
}
}, [isAuthed, isAuthLoading, location.search, config?.APP_MODE]);
};

View File

@@ -53,8 +53,12 @@ export const useAutoLogin = () => {
// If we have an auth URL, redirect to it
if (authUrl) {
// Add the login method as a query parameter
const url = new URL(authUrl);
url.searchParams.append("login_method", loginMethod);
// After successful login, the user will be redirected back and can navigate to the last page
window.location.href = authUrl;
window.location.href = url.toString();
}
}, [
config?.APP_MODE,

View File

@@ -228,8 +228,6 @@ export enum I18nKey {
FEEDBACK$FAILED_TO_SHARE = "FEEDBACK$FAILED_TO_SHARE",
FEEDBACK$COPY_LABEL = "FEEDBACK$COPY_LABEL",
FEEDBACK$SHARING_SETTINGS_LABEL = "FEEDBACK$SHARING_SETTINGS_LABEL",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
SECURITY$UNKNOWN_ANALYZER_LABEL = "SECURITY$UNKNOWN_ANALYZER_LABEL",
INVARIANT$UPDATE_POLICY_LABEL = "INVARIANT$UPDATE_POLICY_LABEL",
INVARIANT$UPDATE_SETTINGS_LABEL = "INVARIANT$UPDATE_SETTINGS_LABEL",
@@ -550,4 +548,6 @@ export enum I18nKey {
TIPS$API_USAGE = "TIPS$API_USAGE",
TIPS$LEARN_MORE = "TIPS$LEARN_MORE",
TIPS$PROTIP = "TIPS$PROTIP",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
}

View File

@@ -6400,20 +6400,20 @@
"uk": "Запит не вдалося виконати через внутрішню помилку сервера."
},
"STATUS$ERROR_LLM_OUT_OF_CREDITS": {
"en": "You're out of OpenHands Credits",
"ja": "OpenHandsクレジットが不足しています",
"zh-CN": "您的OpenHands点数已用完",
"zh-TW": "您的OpenHands點數已用完",
"ko-KR": "OpenHands 크레딧이 소진되었습니다",
"no": "Du er tom for OpenHands-kreditter",
"it": "Hai esaurito i crediti OpenHands",
"pt": "Você está sem créditos OpenHands",
"es": "Te has quedado sin créditos de OpenHands",
"ar": "لقد نفدت رصيدك من OpenHands",
"fr": "Vous n'avez plus de crédits OpenHands",
"tr": "OpenHands kredileriniz tükendi",
"de": "Ihre OpenHands-Guthaben sind aufgebraucht",
"uk": "У вас закінчилися кредити OpenHands"
"en": "You're out of OpenHands Credits. <a>Add funds</a>",
"ja": "OpenHandsクレジットが不足しています。<a>資金を追加</a>",
"zh-CN": "您的OpenHands点数已用完。<a>添加资金</a>",
"zh-TW": "您的OpenHands點數已用完。<a>添加資金</a>",
"ko-KR": "OpenHands 크레딧이 소진되었습니다. <a>자금 추가</a>",
"no": "Du er tom for OpenHands-kreditter. <a>Legg til midler</a>",
"it": "Hai esaurito i crediti OpenHands. <a>Aggiungi fondi</a>",
"pt": "Você está sem créditos OpenHands. <a>Adicionar fundos</a>",
"es": "Te has quedado sin créditos de OpenHands. <a>Añadir fondos</a>",
"ar": "لقد نفدت رصيدك من OpenHands. <a>إضافة رصيد</a>",
"fr": "Vous n'avez plus de crédits OpenHands. <a>Ajouter des fonds</a>",
"tr": "OpenHands kredileriniz tükendi. <a>Bakiye ekle</a>",
"de": "Ihre OpenHands-Guthaben sind aufgebraucht. <a>Guthaben hinzufügen</a>",
"uk": "У вас закінчилися кредити OpenHands. <a>Додати кошти</a>"
},
"STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION": {
"en": "Content policy violation. The output was blocked by content filtering policy.",
@@ -8780,7 +8780,7 @@
"ar": "إرسال...",
"fr": "Envoi...",
"tr": "Gönderiliyor...",
"de": "Senden...",
"de": "Senden...",
"uk": "Відправляємо..."
},
"FEEDBACK$SUBMITTING_MESSAGE": {

View File

@@ -36,6 +36,7 @@ import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { TabContent } from "#/components/layout/tab-content";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
function AppContent() {
useConversationConfig();
@@ -43,6 +44,7 @@ function AppContent() {
const { data: settings } = useSettings();
const { conversationId } = useConversationId();
const { data: conversation, isFetched } = useActiveConversation();
const { data: isAuthed } = useIsAuthed();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
@@ -54,13 +56,13 @@ function AppContent() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
if (isFetched && !conversation) {
if (isFetched && !conversation && isAuthed) {
displayErrorToast(
"This conversation does not exist, or you do not have permission to access it.",
);
navigate("/");
}
}, [conversation, isFetched]);
}, [conversation, isFetched, isAuthed]);
React.useEffect(() => {
dispatch(clearTerminal());

View File

@@ -23,6 +23,7 @@ import { SetupPaymentModal } from "#/components/features/payment/setup-payment-m
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
export function ErrorBoundary() {
@@ -88,6 +89,9 @@ export default function MainApp() {
// Auto-login if login method is stored in local storage
useAutoLogin();
// Handle authentication callback and set login method after successful authentication
useAuthCallback();
React.useEffect(() => {
// Don't change language when on TOS page
if (!isOnTosPage && settings?.LANGUAGE) {
@@ -131,8 +135,8 @@ export default function MainApp() {
}
}, [error?.status, pathname, isOnTosPage]);
// Check if login method exists in local storage
const loginMethodExists = React.useMemo(() => {
// Function to check if login method exists in local storage
const checkLoginMethodExists = React.useCallback(() => {
// Only check localStorage if we're in a browser environment
if (typeof window !== "undefined" && window.localStorage) {
return localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD) !== null;
@@ -140,6 +144,39 @@ export default function MainApp() {
return false;
}, []);
// State to track if login method exists
const [loginMethodExists, setLoginMethodExists] = React.useState(
checkLoginMethodExists(),
);
// Listen for storage events to update loginMethodExists when logout happens
React.useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === LOCAL_STORAGE_KEYS.LOGIN_METHOD) {
setLoginMethodExists(checkLoginMethodExists());
}
};
// Also check on window focus, as logout might happen in another tab
const handleWindowFocus = () => {
setLoginMethodExists(checkLoginMethodExists());
};
window.addEventListener("storage", handleStorageChange);
window.addEventListener("focus", handleWindowFocus);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener("focus", handleWindowFocus);
};
}, [checkLoginMethodExists]);
// Check login method status when auth status changes
React.useEffect(() => {
// When auth status changes (especially on logout), recheck login method
setLoginMethodExists(checkLoginMethodExists());
}, [isAuthed, checkLoginMethodExists]);
const renderAuthModal =
!isAuthed &&
!isAuthError &&

View File

@@ -16,5 +16,8 @@ export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
authUrl = `auth.${requestUrl.hostname}`;
}
const scope = "openid email profile"; // OAuth scope - not user-facing
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(requestUrl.href)}`;
const separator = requestUrl.search ? "&" : "?";
const cleanHref = requestUrl.href.replace(/\/$/, "");
const state = `${cleanHref}${separator}login_method=${identityProvider}`;
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`;
};

View File

@@ -1,13 +1,16 @@
import os
from typing import TYPE_CHECKING
from browsergym.core.action.highlevel import HighLevelActionSet
from browsergym.utils.obs import flatten_axtree_to_str
from openhands.agenthub.browsing_agent.response_parser import BrowsingResponseParser
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
# Import Agent for both type checking and runtime
from openhands.controller.agent import Agent
from openhands.core.message import Message, TextContent
from openhands.events.action import (
Action,
@@ -91,6 +94,8 @@ In order to accomplish my goal I need to click on the button with bid 12
return prompt
class BrowsingAgent(Agent):
VERSION = '1.0'
"""

View File

@@ -25,7 +25,7 @@ from openhands.cli.utils import (
write_to_file,
)
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
)
from openhands.core.schema import AgentState
from openhands.events import EventSource
@@ -42,7 +42,7 @@ async def handle_commands(
event_stream: EventStream,
usage_metrics: UsageMetrics,
sid: str,
config: AppConfig,
config: OpenHandsConfig,
current_dir: str,
settings_store: FileSettingsStore,
) -> tuple[bool, bool, bool]:
@@ -105,7 +105,7 @@ def handle_help_command() -> None:
async def handle_init_command(
config: AppConfig, event_stream: EventStream, current_dir: str
config: OpenHandsConfig, event_stream: EventStream, current_dir: str
) -> tuple[bool, bool]:
REPO_MD_CREATE_PROMPT = """
Please explore this repository. Create the file .openhands/microagents/repo.md with:
@@ -166,7 +166,7 @@ def handle_new_command(
async def handle_settings_command(
config: AppConfig,
config: OpenHandsConfig,
settings_store: FileSettingsStore,
) -> None:
display_settings(config)
@@ -264,7 +264,7 @@ async def init_repository(current_dir: str) -> bool:
return init_repo
def check_folder_security_agreement(config: AppConfig, current_dir: str) -> bool:
def check_folder_security_agreement(config: OpenHandsConfig, current_dir: str) -> bool:
# Directories trusted by user for the CLI to use as workspace
# Config from ~/.openhands/config.toml overrides the app config

View File

@@ -30,7 +30,7 @@ from openhands.cli.utils import (
from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
parse_arguments,
setup_config_from_args,
)
@@ -103,7 +103,7 @@ async def cleanup_session(
async def run_session(
loop: asyncio.AbstractEventLoop,
config: AppConfig,
config: OpenHandsConfig,
settings_store: FileSettingsStore,
current_dir: str,
task_content: str | None = None,
@@ -334,7 +334,7 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
logger.setLevel(logging.WARNING)
# Load config from toml and override with command line arguments
config: AppConfig = setup_config_from_args(args)
config: OpenHandsConfig = setup_config_from_args(args)
# Load settings from Settings Store
# TODO: Make this generic?

View File

@@ -18,7 +18,7 @@ from openhands.cli.utils import (
organize_models_and_providers,
)
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import OH_DEFAULT_AGENT
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
@@ -29,7 +29,7 @@ from openhands.storage.settings.file_settings_store import FileSettingsStore
from openhands.utils.llm import get_supported_llm_models
def display_settings(config: AppConfig) -> None:
def display_settings(config: OpenHandsConfig) -> None:
llm_config = config.get_llm_config()
advanced_llm_settings = True if llm_config.base_url else False
@@ -145,7 +145,7 @@ def save_settings_confirmation() -> bool:
async def modify_llm_settings_basic(
config: AppConfig, settings_store: FileSettingsStore
config: OpenHandsConfig, settings_store: FileSettingsStore
) -> None:
model_list = get_supported_llm_models(config)
organized_models = organize_models_and_providers(model_list)
@@ -243,7 +243,7 @@ async def modify_llm_settings_basic(
async def modify_llm_settings_advanced(
config: AppConfig, settings_store: FileSettingsStore
config: OpenHandsConfig, settings_store: FileSettingsStore
) -> None:
session = PromptSession(key_bindings=kb_cancel())

View File

@@ -27,7 +27,7 @@ from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
from openhands import __version__
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.schema import AgentState
from openhands.events import EventSource, EventStream
from openhands.events.action import (
@@ -180,7 +180,7 @@ def display_initial_user_prompt(prompt: str) -> None:
# Prompt output display functions
def display_event(event: Event, config: AppConfig) -> None:
def display_event(event: Event, config: OpenHandsConfig) -> None:
global streaming_output_text_area
with print_lock:
if isinstance(event, Action):

View File

@@ -0,0 +1,6 @@
"""Core components of the OpenHands system."""
from openhands.core.conversation import Conversation
from openhands.core.openhands import OpenHands
__all__ = ['Conversation', 'OpenHands']

View File

@@ -1,5 +1,4 @@
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.app_config import AppConfig
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
OH_MAX_ITERATIONS,
@@ -8,6 +7,7 @@ from openhands.core.config.config_utils import (
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.core.config.utils import (
@@ -15,9 +15,9 @@ from openhands.core.config.utils import (
get_agent_config_arg,
get_llm_config_arg,
get_parser,
load_app_config,
load_from_env,
load_from_toml,
load_openhands_config,
parse_arguments,
setup_config_from_args,
)
@@ -26,13 +26,13 @@ __all__ = [
'OH_DEFAULT_AGENT',
'OH_MAX_ITERATIONS',
'AgentConfig',
'AppConfig',
'OpenHandsConfig',
'MCPConfig',
'LLMConfig',
'SandboxConfig',
'SecurityConfig',
'ExtendedConfig',
'load_app_config',
'load_openhands_config',
'load_from_env',
'load_from_toml',
'finalize_config',

View File

@@ -1,10 +1,11 @@
import os
from urllib.parse import urlparse
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from pydantic import BaseModel, Field, ValidationError, model_validator
if TYPE_CHECKING:
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.utils.import_utils import get_impl
@@ -147,7 +148,7 @@ class MCPConfig(BaseModel):
class OpenHandsMCPConfig:
@staticmethod
def add_search_engine(app_config: "AppConfig") -> MCPStdioServerConfig | None:
def add_search_engine(app_config: 'OpenHandsConfig') -> MCPStdioServerConfig | None:
"""Add search engine to the MCP config"""
if (
app_config.search_api_key
@@ -165,17 +166,16 @@ class OpenHandsMCPConfig:
# Do not add search engine to MCP config in SaaS mode since it will be added by the OpenHands server
return None
@staticmethod
def create_default_mcp_server_config(
host: str, config: "AppConfig", user_id: str | None = None
host: str, config: 'OpenHandsConfig', user_id: str | None = None
) -> tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]:
"""
Create a default MCP server configuration.
Args:
host: Host string
config: AppConfig
config: OpenHandsConfig
Returns:
tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]: A tuple containing the default SSE server configuration and a list of MCP stdio server configurations
"""

View File

@@ -16,7 +16,7 @@ from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
class AppConfig(BaseModel):
class OpenHandsConfig(BaseModel):
"""Configuration for the app.
Attributes:
@@ -65,7 +65,10 @@ class AppConfig(BaseModel):
save_trajectory_path: str | None = Field(default=None)
save_screenshots_in_trajectory: bool = Field(default=False)
replay_trajectory_path: str | None = Field(default=None)
search_api_key: SecretStr | None = Field(default=None, description="API key for Tavily search engine (https://tavily.com/). Required for search functionality.")
search_api_key: SecretStr | None = Field(
default=None,
description='API key for Tavily search engine (https://tavily.com/). Required for search functionality.',
)
# Deprecated parameters - will be removed in a future version
workspace_base: str | None = Field(default=None, deprecated=True)
@@ -73,7 +76,7 @@ class AppConfig(BaseModel):
workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True)
workspace_mount_rewrite: str | None = Field(default=None, deprecated=True)
# End of deprecated parameters
cache_dir: str = Field(default='/tmp/cache')
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
@@ -148,5 +151,5 @@ class AppConfig(BaseModel):
"""Post-initialization hook, called when the instance is created with only default values."""
super().model_post_init(__context)
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
AppConfig.defaults_dict = model_defaults_to_dict(self)
if not OpenHandsConfig.defaults_dict: # Only set defaults_dict if it's empty
OpenHandsConfig.defaults_dict = model_defaults_to_dict(self)

View File

@@ -15,7 +15,6 @@ from pydantic import BaseModel, SecretStr, ValidationError
from openhands import __version__
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.app_config import AppConfig
from openhands.core.config.condenser_config import (
CondenserConfig,
condenser_config_from_toml_section,
@@ -28,6 +27,7 @@ from openhands.core.config.config_utils import (
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.storage import get_file_store
@@ -39,7 +39,7 @@ load_dotenv()
def load_from_env(
cfg: AppConfig, env_or_toml_dict: dict | MutableMapping[str, str]
cfg: OpenHandsConfig, env_or_toml_dict: dict | MutableMapping[str, str]
) -> None:
"""Sets config attributes from environment variables or TOML dictionary.
@@ -48,7 +48,7 @@ def load_from_env(
(e.g., AGENT_MEMORY_ENABLED), sandbox settings (e.g., SANDBOX_TIMEOUT), and more.
Args:
cfg: The AppConfig object to set attributes on.
cfg: The OpenHandsConfig object to set attributes on.
env_or_toml_dict: The environment variables or a config.toml dict.
"""
@@ -121,11 +121,11 @@ def load_from_env(
set_attr_from_env(default_agent_config, 'AGENT_')
def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
def load_from_toml(cfg: OpenHandsConfig, toml_file: str = 'config.toml') -> None:
"""Load the config from the toml file. Supports both styles of config vars.
Args:
cfg: The AppConfig object to update attributes of.
cfg: The OpenHandsConfig object to update attributes of.
toml_file: The path to the toml file. Defaults to 'config.toml'.
See Also:
@@ -302,7 +302,7 @@ def get_or_create_jwt_secret(file_store: FileStore) -> str:
return new_secret
def finalize_config(cfg: AppConfig) -> None:
def finalize_config(cfg: OpenHandsConfig) -> None:
"""More tweaks to the config after it's been loaded."""
# Handle the sandbox.volumes parameter
if cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
@@ -759,7 +759,7 @@ def parse_arguments() -> argparse.Namespace:
return args
def register_custom_agents(config: AppConfig) -> None:
def register_custom_agents(config: OpenHandsConfig) -> None:
"""Register custom agents from configuration.
This function is called after configuration is loaded to ensure all custom agents
@@ -782,16 +782,16 @@ def register_custom_agents(config: AppConfig) -> None:
)
def load_app_config(
def load_openhands_config(
set_logging_levels: bool = True, config_file: str = 'config.toml'
) -> AppConfig:
) -> OpenHandsConfig:
"""Load the configuration from the specified config file and environment variables.
Args:
set_logging_levels: Whether to set the global variables for logging levels.
config_file: Path to the config file. Defaults to 'config.toml' in the current directory.
"""
config = AppConfig()
config = OpenHandsConfig()
load_from_toml(config, config_file)
load_from_env(config, os.environ)
finalize_config(config)
@@ -802,13 +802,13 @@ def load_app_config(
return config
def setup_config_from_args(args: argparse.Namespace) -> AppConfig:
def setup_config_from_args(args: argparse.Namespace) -> OpenHandsConfig:
"""Load config from toml and override with command line arguments.
Common setup used by both CLI and main.py entry points.
"""
# Load base config from toml and env vars
config = load_app_config(config_file=args.config_file)
config = load_openhands_config(config_file=args.config_file)
# Override with command line arguments if provided
if args.llm_config:

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.controller.agent_controller import AgentController
from openhands.events.stream import EventStream
from openhands.llm.llm import LLM
from openhands.runtime.base import Runtime
@dataclass
class Conversation:
"""Main interface for conversations in OpenHands.
This class serves as a container for all the components needed for a conversation
between a user and an OpenHands agent.
Attributes:
conversation_id: Unique identifier for the conversation
runtime: Runtime environment where the agent operates
llm: Language model used by the agent
event_stream: Stream of events (actions and observations) in the conversation
agent_controller: Controller that manages the agent's behavior and state
"""
conversation_id: str
runtime: 'Runtime'
llm: 'LLM'
event_stream: 'EventStream'
agent_controller: 'AgentController'

View File

@@ -9,7 +9,7 @@ from openhands.controller.agent import Agent
from openhands.controller.replay import ReplayManager
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
parse_arguments,
setup_config_from_args,
)
@@ -47,7 +47,7 @@ class FakeUserResponseFunc(Protocol):
async def run_controller(
config: AppConfig,
config: OpenHandsConfig,
initial_user_action: Action,
sid: str | None = None,
runtime: Runtime | None = None,
@@ -90,7 +90,7 @@ async def run_controller(
config.max_budget_per_task.
Example:
>>> config = load_app_config()
>>> config = load_openhands_config()
>>> action = MessageAction(content="Write a hello world program")
>>> state = await run_controller(config=config, initial_user_action=action)
"""
@@ -279,7 +279,7 @@ def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]:
if __name__ == '__main__':
args = parse_arguments()
config: AppConfig = setup_config_from_args(args)
config: OpenHandsConfig = setup_config_from_args(args)
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)

View File

@@ -0,0 +1,93 @@
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.setup import create_agent, create_runtime
from openhands.events.stream import EventStream
from openhands.llm.llm import LLM
from openhands.storage import get_file_store
if TYPE_CHECKING:
from openhands.core.conversation import Conversation
class OpenHands:
"""Main class for creating and managing OpenHands conversations.
This class is responsible for creating new conversations based on the provided
configuration. It serves as the primary entry point for interacting with the
OpenHands system.
Attributes:
config: Configuration for the OpenHands system
"""
def __init__(self, config: OpenHandsConfig):
"""Initialize the OpenHands instance with the provided configuration.
Args:
config: Configuration for the OpenHands system
"""
self.config = config
def create_conversation(self, conversation_id: str | None = None) -> 'Conversation':
"""Create a new conversation with all necessary components.
This method creates a Runtime, LLM, EventStream, and AgentController according
to the configuration provided to the constructor, and returns a Conversation
object containing all these components.
Args:
conversation_id: Optional identifier for the conversation. If not provided,
a unique ID will be generated.
Returns:
A Conversation object containing all the components needed for interaction.
"""
# Create a runtime based on the configuration
runtime = create_runtime(self.config)
# Create a file store
file_store = get_file_store(self.config.file_store, self.config.file_store_path)
# Create an event stream for the conversation
# Generate a unique ID if none is provided
sid = (
conversation_id
if conversation_id is not None
else f'conversation-{uuid.uuid4()}'
)
event_stream = EventStream(sid=sid, file_store=file_store)
# Get the default LLM configuration and create an LLM instance
llm_config = self.config.get_llm_config()
llm = LLM(llm_config)
# Create an agent using the factory function
agent = create_agent(self.config)
# Create an agent controller
from openhands.controller.agent_controller import AgentController
agent_controller = AgentController(
agent=agent,
event_stream=event_stream,
max_iterations=self.config.max_iterations,
max_budget_per_task=self.config.max_budget_per_task,
agent_to_llm_config=self.config.get_agent_to_llm_config_map(),
agent_configs=self.config.get_agent_configs(),
sid=conversation_id,
)
# Create and return a Conversation object
from openhands.core.conversation import Conversation
return Conversation(
conversation_id=conversation_id or event_stream.sid,
runtime=runtime,
llm=llm,
event_stream=event_stream,
agent_controller=agent_controller,
)

View File

@@ -10,7 +10,7 @@ from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
OpenHandsConfig,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
@@ -28,7 +28,7 @@ from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync
def create_runtime(
config: AppConfig,
config: OpenHandsConfig,
sid: str | None = None,
headless_mode: bool = True,
agent: Agent | None = None,
@@ -172,7 +172,7 @@ def create_memory(
return memory
def create_agent(config: AppConfig) -> Agent:
def create_agent(config: OpenHandsConfig) -> Agent:
agent_cls: type[Agent] = Agent.get_cls(config.default_agent)
agent_config = config.get_agent_config(config.default_agent)
llm_config = config.get_llm_config_from_agent(config.default_agent)
@@ -188,7 +188,7 @@ def create_agent(config: AppConfig) -> Agent:
def create_controller(
agent: Agent,
runtime: Runtime,
config: AppConfig,
config: OpenHandsConfig,
headless_mode: bool = True,
replay_events: list[Event] | None = None,
) -> tuple[AgentController, State | None]:
@@ -218,7 +218,7 @@ def create_controller(
return (controller, initial_state)
def generate_sid(config: AppConfig, session_name: str | None = None) -> str:
def generate_sid(config: OpenHandsConfig, session_name: str | None = None) -> str:
"""Generate a session id based on the session name and the jwt secret."""
session_name = session_name or str(uuid.uuid4())
jwt_secret = config.jwt_secret

View File

@@ -1,3 +1,4 @@
import logging
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, Protocol
@@ -6,9 +7,11 @@ from httpx import AsyncClient, HTTPError, HTTPStatusError
from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel, SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.server.types import AppMode
# Use standard logging instead of importing from core.logger
logger = logging.getLogger('openhands')
class ProviderType(Enum):
GITHUB = 'github'

View File

@@ -4,11 +4,11 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.controller.agent import Agent
from openhands.core.config.app_config import AppConfig
from openhands.core.config.mcp_config import (
MCPConfig,
MCPSSEServerConfig,
)
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.mcp import MCPAction
from openhands.events.observation.mcp import MCPObservation
@@ -162,7 +162,7 @@ async def call_tool_mcp(mcp_clients: list[MCPClient], action: MCPAction) -> Obse
async def add_mcp_tools_to_agent(
agent: 'Agent', runtime: Runtime, memory: 'Memory', app_config: AppConfig
agent: 'Agent', runtime: Runtime, memory: 'Memory', app_config: OpenHandsConfig
):
"""
Add MCP tools to an agent.

View File

@@ -16,7 +16,7 @@ from termcolor import colored
import openhands
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig, AppConfig, LLMConfig, SandboxConfig
from openhands.core.config import AgentConfig, LLMConfig, OpenHandsConfig, SandboxConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
@@ -377,7 +377,7 @@ class IssueResolver:
shutil.rmtree(workspace_base)
shutil.copytree(os.path.join(self.output_dir, 'repo'), workspace_base)
config = AppConfig(
config = OpenHandsConfig(
default_agent='CodeActAgent',
runtime='docker',
max_budget_per_task=4,

View File

@@ -1013,12 +1013,12 @@ if __name__ == '__main__':
if not os.path.exists(full_path):
# if user just removed a folder, prevent server error 500 in UI
return []
return JSONResponse(content=[])
try:
# Check if the directory exists
if not os.path.exists(full_path) or not os.path.isdir(full_path):
return []
return JSONResponse(content=[])
entries = os.listdir(full_path)
@@ -1047,11 +1047,11 @@ if __name__ == '__main__':
# Combine sorted directories and files
sorted_entries = directories + files
return sorted_entries
return JSONResponse(content=sorted_entries)
except Exception as e:
logger.error(f'Error listing files: {e}')
return []
return JSONResponse(content=[])
logger.debug(f'Starting action execution API on port {args.port}')
run(app, host='0.0.0.0', port=args.port)

View File

@@ -15,7 +15,7 @@ from zipfile import ZipFile
import httpx
from openhands.core.config import AppConfig, SandboxConfig
from openhands.core.config import OpenHandsConfig, SandboxConfig
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
from openhands.core.exceptions import AgentRuntimeDisconnectedError
from openhands.core.logger import openhands_logger as logger
@@ -97,7 +97,7 @@ class Runtime(FileEditRuntimeMixin):
"""
sid: str
config: AppConfig
config: OpenHandsConfig
initial_env_vars: dict[str, str]
attach_to_existing: bool
status_callback: Callable[[str, str, str], None] | None
@@ -105,7 +105,7 @@ class Runtime(FileEditRuntimeMixin):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -9,7 +9,7 @@ import httpcore
import httpx
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.config.mcp_config import (
MCPConfig,
MCPSSEServerConfig,
@@ -65,7 +65,7 @@ class ActionExecutionClient(Runtime):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
@@ -404,12 +404,12 @@ class ActionExecutionClient(Runtime):
if response.status_code != 200:
self.log('warning', f'Failed to update MCP server: {response.text}')
else:
if result['router_error_log']:
if result.get('router_error_log'):
self.log(
'warning',
f'Some MCP servers failed to be added: {result["router_error_log"]}',
)
# Update our cached list with combined servers after successful update
self._last_updated_mcp_stdio_servers = combined_servers.copy()
self.log(

View File

@@ -22,7 +22,7 @@ from openhands_aci.editor.results import ToolResult
from openhands_aci.utils.diff import get_diff
from pydantic import SecretStr
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
from openhands.core.exceptions import LLMMalformedActionError
from openhands.core.logger import openhands_logger as logger
@@ -57,7 +57,7 @@ class CLIRuntime(Runtime):
file operations using Python's standard library. It does not implement browser functionality.
Args:
config (AppConfig): The application configuration.
config (OpenHandsConfig): The application configuration.
event_stream (EventStream): The event stream to subscribe to.
sid (str, optional): The session ID. Defaults to 'default'.
plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None.
@@ -71,7 +71,7 @@ class CLIRuntime(Runtime):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -11,7 +11,7 @@ from daytona_sdk import (
Workspace,
)
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.events.stream import EventStream
from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
@@ -33,7 +33,7 @@ class DaytonaRuntime(ActionExecutionClient):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -8,7 +8,7 @@ import httpx
import tenacity
from docker.models.containers import Container
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.exceptions import (
AgentRuntimeDisconnectedError,
AgentRuntimeNotFoundError,
@@ -23,7 +23,10 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
from openhands.runtime.impl.docker.containers import stop_all_containers
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils import find_available_tcp_port
from openhands.runtime.utils.command import DEFAULT_MAIN_MODULE, get_action_execution_server_startup_command
from openhands.runtime.utils.command import (
DEFAULT_MAIN_MODULE,
get_action_execution_server_startup_command,
)
from openhands.runtime.utils.log_streamer import LogStreamer
from openhands.runtime.utils.runtime_build import build_runtime_image
from openhands.utils.async_utils import call_sync_from_async
@@ -62,7 +65,7 @@ class DockerRuntime(ActionExecutionClient):
When receive an event, it will send the event to runtime-client which run inside the docker environment.
Args:
config (AppConfig): The application configuration.
config (OpenHandsConfig): The application configuration.
event_stream (EventStream): The event stream to subscribe to.
sid (str, optional): The session ID. Defaults to 'default'.
plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None.
@@ -73,7 +76,7 @@ class DockerRuntime(ActionExecutionClient):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -1,6 +1,6 @@
from typing import Callable
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.events.action import (
FileReadAction,
FileWriteAction,
@@ -22,7 +22,7 @@ from openhands.runtime.utils.files import insert_lines, read_lines
class E2BRuntime(Runtime):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -12,7 +12,7 @@ import httpx
import tenacity
import openhands
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.exceptions import AgentRuntimeDisconnectedError
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
@@ -107,7 +107,7 @@ class LocalRuntime(ActionExecutionClient):
When receiving an event, it will send the event to the server via HTTP.
Args:
config (AppConfig): The application configuration.
config (OpenHandsConfig): The application configuration.
event_stream (EventStream): The event stream to subscribe to.
sid (str, optional): The session ID. Defaults to 'default'.
plugins (list[PluginRequirement] | None, optional): list of plugin requirements. Defaults to None.
@@ -116,7 +116,7 @@ class LocalRuntime(ActionExecutionClient):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -7,7 +7,7 @@ import httpx
import modal
import tenacity
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.events import EventStream
from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
@@ -31,7 +31,7 @@ class ModalRuntime(ActionExecutionClient):
When receive an event, it will send the event to runtime-client which run inside the Modal sandbox environment.
Args:
config (AppConfig): The application configuration.
config (OpenHandsConfig): The application configuration.
event_stream (EventStream): The event stream to subscribe to.
sid (str, optional): The session ID. Defaults to 'default'.
plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None.
@@ -44,7 +44,7 @@ class ModalRuntime(ActionExecutionClient):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -8,7 +8,7 @@ import httpx
import tenacity
from tenacity import RetryCallState
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.exceptions import (
AgentRuntimeDisconnectedError,
AgentRuntimeError,
@@ -24,7 +24,10 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
)
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.command import DEFAULT_MAIN_MODULE, get_action_execution_server_startup_command
from openhands.runtime.utils.command import (
DEFAULT_MAIN_MODULE,
get_action_execution_server_startup_command,
)
from openhands.runtime.utils.request import send_request
from openhands.runtime.utils.runtime_build import build_runtime_image
from openhands.utils.async_utils import call_sync_from_async
@@ -45,7 +48,7 @@ class RemoteRuntime(ActionExecutionClient):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -6,7 +6,7 @@ from runloop_api_client import Runloop
from runloop_api_client.types import DevboxView
from runloop_api_client.types.shared_params import LaunchParameters
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.runtime.impl.action_execution.action_execution_client import (
@@ -27,7 +27,7 @@ class RunloopRuntime(ActionExecutionClient):
def __init__(
self,
config: AppConfig,
config: OpenHandsConfig,
event_stream: EventStream,
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,

View File

@@ -1,4 +1,4 @@
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.runtime.plugins import PluginRequirement
DEFAULT_PYTHON_PREFIX = [
@@ -15,7 +15,7 @@ DEFAULT_MAIN_MODULE = 'openhands.runtime.action_execution_server'
def get_action_execution_server_startup_command(
server_port: int,
plugins: list[PluginRequirement],
app_config: AppConfig,
app_config: OpenHandsConfig,
python_prefix: list[str] = DEFAULT_PYTHON_PREFIX,
override_user_id: int | None = None,
override_username: str | None = None,

View File

@@ -6,7 +6,7 @@ from typing import Any
from openhands_aci.utils.diff import get_diff
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
FileEditAction,
@@ -83,7 +83,7 @@ def get_new_file_contents(
class FileEditRuntimeInterface(ABC):
config: AppConfig
config: OpenHandsConfig
@abstractmethod
def read(self, action: FileReadAction) -> Observation:

View File

@@ -4,14 +4,12 @@ from abc import ABC, abstractmethod
import socketio
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.events.action import MessageAction
from openhands.events.event_store import EventStore
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.data_models.conversation_info import ConversationInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.conversation import Conversation
from openhands.server.session.conversation import ServerConversation
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.settings import Settings
from openhands.storage.files import FileStore
@@ -26,7 +24,7 @@ class ConversationManager(ABC):
"""
sio: socketio.AsyncServer
config: AppConfig
config: OpenHandsConfig
file_store: FileStore
conversation_store: ConversationStore
@@ -41,11 +39,11 @@ class ConversationManager(ABC):
@abstractmethod
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
) -> Conversation | None:
) -> ServerConversation | None:
"""Attach to an existing conversation or create a new one."""
@abstractmethod
async def detach_from_conversation(self, conversation: Conversation):
async def detach_from_conversation(self, conversation: ServerConversation):
"""Detach from a conversation."""
@abstractmethod
@@ -109,7 +107,7 @@ class ConversationManager(ABC):
def get_instance(
cls,
sio: socketio.AsyncServer,
config: AppConfig,
config: OpenHandsConfig,
file_store: FileStore,
server_config: ServerConfig,
monitoring_listener: MonitoringListener,

View File

@@ -15,7 +15,7 @@ from docker.models.containers import Container
from fastapi import status
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import MessageAction
from openhands.events.nested_event_store import NestedEventStore
@@ -30,7 +30,7 @@ from openhands.server.conversation_manager.conversation_manager import (
)
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.conversation import Conversation
from openhands.server.session.conversation import ServerConversation
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.session.session import ROOM_KEY, Session
from openhands.storage.conversation.conversation_store import ConversationStore
@@ -45,10 +45,10 @@ from openhands.utils.import_utils import get_impl
@dataclass
class DockerNestedConversationManager(ConversationManager):
"""Conversation manager where the agent loops exist inside the docker containers."""
"""ServerConversation manager where the agent loops exist inside the docker containers."""
sio: socketio.AsyncServer
config: AppConfig
config: OpenHandsConfig
server_config: ServerConfig
file_store: FileStore
docker_client: docker.DockerClient = field(default_factory=docker.from_env)
@@ -65,11 +65,11 @@ class DockerNestedConversationManager(ConversationManager):
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
) -> Conversation | None:
) -> ServerConversation | None:
# Not supported - clients should connect directly to the nested server!
raise ValueError('unsupported_operation')
async def detach_from_conversation(self, conversation: Conversation):
async def detach_from_conversation(self, conversation: ServerConversation):
# Not supported - clients should connect directly to the nested server!
raise ValueError('unsupported_operation')
@@ -309,7 +309,7 @@ class DockerNestedConversationManager(ConversationManager):
def get_instance(
cls,
sio: socketio.AsyncServer,
config: AppConfig,
config: OpenHandsConfig,
file_store: FileStore,
server_config: ServerConfig,
monitoring_listener: MonitoringListener,
@@ -433,7 +433,7 @@ class DockerNestedConversationManager(ConversationManager):
volumes = [v.strip() for v in config.sandbox.volumes.split(',')]
conversation_dir = get_conversation_dir(sid, user_id)
volumes.append(
f'{config.file_store_path}/{conversation_dir}:{AppConfig.model_fields["file_store_path"].default}/{conversation_dir}:rw'
f'{config.file_store_path}/{conversation_dir}:{OpenHandsConfig.model_fields["file_store_path"].default}/{conversation_dir}:rw'
)
config.sandbox.volumes = ','.join(volumes)

View File

@@ -6,7 +6,7 @@ from typing import Callable, Iterable
import socketio
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.exceptions import AgentRuntimeUnavailableError
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema.agent import AgentState
@@ -16,7 +16,7 @@ from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE
from openhands.server.session.conversation import Conversation
from openhands.server.session.conversation import ServerConversation
from openhands.server.session.session import ROOM_KEY, Session
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
@@ -41,17 +41,17 @@ class StandaloneConversationManager(ConversationManager):
"""Manages conversations in standalone mode (single server instance)."""
sio: socketio.AsyncServer
config: AppConfig
config: OpenHandsConfig
file_store: FileStore
server_config: ServerConfig
# Defaulting monitoring_listener for temp backward compatibility.
monitoring_listener: MonitoringListener = MonitoringListener()
_local_agent_loops_by_sid: dict[str, Session] = field(default_factory=dict)
_local_connection_id_to_session_id: dict[str, str] = field(default_factory=dict)
_active_conversations: dict[str, tuple[Conversation, int]] = field(
_active_conversations: dict[str, tuple[ServerConversation, int]] = field(
default_factory=dict
)
_detached_conversations: dict[str, tuple[Conversation, float]] = field(
_detached_conversations: dict[str, tuple[ServerConversation, float]] = field(
default_factory=dict
)
_conversations_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
@@ -69,7 +69,7 @@ class StandaloneConversationManager(ConversationManager):
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
) -> Conversation | None:
) -> ServerConversation | None:
start_time = time.time()
if not await session_exists(sid, self.file_store, user_id=user_id):
return None
@@ -94,7 +94,7 @@ class StandaloneConversationManager(ConversationManager):
return conversation
# Create new conversation if none exists
c = Conversation(
c = ServerConversation(
sid, file_store=self.file_store, config=self.config, user_id=user_id
)
try:
@@ -108,7 +108,7 @@ class StandaloneConversationManager(ConversationManager):
return None
end_time = time.time()
logger.info(
f'Conversation {c.sid} connected in {end_time - start_time} seconds'
f'ServerConversation {c.sid} connected in {end_time - start_time} seconds'
)
self._active_conversations[sid] = (c, 1)
return c
@@ -129,7 +129,7 @@ class StandaloneConversationManager(ConversationManager):
agent_loop_info = await self.maybe_start_agent_loop(sid, settings, user_id)
return agent_loop_info
async def detach_from_conversation(self, conversation: Conversation):
async def detach_from_conversation(self, conversation: ServerConversation):
sid = conversation.sid
async with self._conversations_lock:
if sid in self._active_conversations:
@@ -377,7 +377,7 @@ class StandaloneConversationManager(ConversationManager):
def get_instance(
cls,
sio: socketio.AsyncServer,
config: AppConfig,
config: OpenHandsConfig,
file_store: FileStore,
server_config: ServerConfig,
monitoring_listener: MonitoringListener | None,
@@ -475,7 +475,7 @@ class StandaloneConversationManager(ConversationManager):
continue
results.append(self._agent_loop_info_from_session(session))
return results
def _agent_loop_info_from_session(self, session: Session):
return AgentLoopInfo(
conversation_id=session.sid,
@@ -485,7 +485,7 @@ class StandaloneConversationManager(ConversationManager):
)
def _get_conversation_url(self, conversation_id: str):
return f"/api/conversations/{conversation_id}"
return f'/api/conversations/{conversation_id}'
def _last_updated_at_key(conversation: ConversationMetadata) -> float:

View File

@@ -1,7 +1,7 @@
import os
import re
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.server.shared import config as shared_config
@@ -30,7 +30,7 @@ def sanitize_filename(filename: str) -> str:
def load_file_upload_config(
config: AppConfig = shared_config,
config: OpenHandsConfig = shared_config,
) -> tuple[int, bool, list[str]]:
"""Load file upload configuration from the config object.

View File

@@ -1,4 +1,4 @@
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.events.event import Event
@@ -33,6 +33,6 @@ class MonitoringListener:
@classmethod
def get_instance(
cls,
config: AppConfig,
config: OpenHandsConfig,
) -> 'MonitoringListener':
return cls()

View File

@@ -103,7 +103,7 @@ async def search_events(
end_id: int | None = None,
reverse: bool = False,
filter: EventFilter | None = None,
limit: int = 20
limit: int = 20,
):
"""Search through the event stream with filtering and pagination.
Args:
@@ -123,22 +123,24 @@ async def search_events(
"""
if not request.state.conversation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Conversation not found'
status_code=status.HTTP_404_NOT_FOUND, detail='ServerConversation not found'
)
if limit < 0 or limit > 100:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid limit'
)
# Get matching events from the stream
event_stream = request.state.conversation.event_stream
events = list(event_stream.search_events(
start_id=start_id,
end_id=end_id,
reverse=reverse,
filter=filter,
limit=limit + 1,
))
events = list(
event_stream.search_events(
start_id=start_id,
end_id=end_id,
reverse=reverse,
filter=filter,
limit=limit + 1,
)
)
# Check if there are more events
has_more = len(events) > limit
@@ -156,4 +158,4 @@ async def search_events(
async def add_event(request: Request):
data = request.json()
conversation_manager.send_to_event_stream(request.state.sid, data)
return JSONResponse({"success": True})
return JSONResponse({'success': True})

View File

@@ -1,27 +1,36 @@
import re
from typing import Annotated
from pydantic import Field
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_request
from pydantic import Field
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.provider import ProviderToken
from openhands.integrations.service_types import ProviderType
from openhands.server.shared import ConversationStoreImpl, config
from openhands.server.user_auth import get_access_token, get_provider_tokens, get_user_id
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth import (
get_access_token,
get_provider_tokens,
get_user_id,
)
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
mcp_server = FastMCP('mcp')
async def save_pr_metadata(user_id: str, conversation_id: str, tool_result: str) -> None:
async def save_pr_metadata(
user_id: str, conversation_id: str, tool_result: str
) -> None:
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
conversation: ConversationMetadata = await conversation_store.get_metadata(conversation_id)
conversation: ConversationMetadata = await conversation_store.get_metadata(
conversation_id
)
pull_pattern = r"pull/(\d+)"
merge_request_pattern = r"merge_requests/(\d+)"
pull_pattern = r'pull/(\d+)'
merge_request_pattern = r'merge_requests/(\d+)'
# Check if the tool_result contains the PR number
pr_number = None
@@ -33,11 +42,11 @@ async def save_pr_metadata(user_id: str, conversation_id: str, tool_result: str)
elif match_merge_request:
pr_number = int(match_merge_request.group(1))
if pr_number:
conversation.pr_number.append(pr_number)
await conversation_store.save_metadata(conversation)
@mcp_server.tool()
async def create_pr(
repo_name: Annotated[
@@ -46,7 +55,7 @@ async def create_pr(
source_branch: Annotated[str, Field(description='Source branch on repo')],
target_branch: Annotated[str, Field(description='Target branch on repo')],
title: Annotated[str, Field(description='PR Title')],
body: Annotated[str | None, Field(description='PR body')]
body: Annotated[str | None, Field(description='PR body')],
) -> str:
"""Open a draft PR in GitHub"""
@@ -54,20 +63,24 @@ async def create_pr(
request = get_http_request()
headers = request.headers
conversation_id = headers.get('X-OpenHands-Conversation-ID', None)
conversation_id = headers.get('X-OpenHands-ServerConversation-ID', None)
provider_tokens = await get_provider_tokens(request)
access_token = await get_access_token(request)
user_id = await get_user_id(request)
github_token = provider_tokens.get(ProviderType.GITHUB, ProviderToken()) if provider_tokens else ProviderToken()
github_token = (
provider_tokens.get(ProviderType.GITHUB, ProviderToken())
if provider_tokens
else ProviderToken()
)
github_service = GithubServiceImpl(
user_id=github_token.user_id,
external_auth_id=user_id,
external_auth_token=access_token,
token=github_token.token,
base_domain=github_token.host
base_domain=github_token.host,
)
try:
@@ -76,7 +89,7 @@ async def create_pr(
source_branch=source_branch,
target_branch=target_branch,
title=title,
body=body
body=body,
)
if conversation_id and user_id:
@@ -88,37 +101,41 @@ async def create_pr(
return response
@mcp_server.tool()
async def create_mr(
id: Annotated[
int | str, Field(description='GitLab repository (ID or URL-encoded path of the project)')
int | str,
Field(description='GitLab repository (ID or URL-encoded path of the project)'),
],
source_branch: Annotated[str, Field(description='Source branch on repo')],
target_branch: Annotated[str, Field(description='Target branch on repo')],
title: Annotated[str, Field(description='MR Title')],
description: Annotated[str | None, Field(description='MR description')]
description: Annotated[str | None, Field(description='MR description')],
) -> str:
"""Open a draft MR in GitLab"""
logger.info('Calling OpenHands MCP create_mr')
request = get_http_request()
headers = request.headers
conversation_id = headers.get('X-OpenHands-Conversation-ID', None)
conversation_id = headers.get('X-OpenHands-ServerConversation-ID', None)
provider_tokens = await get_provider_tokens(request)
access_token = await get_access_token(request)
user_id = await get_user_id(request)
github_token = provider_tokens.get(ProviderType.GITLAB, ProviderToken()) if provider_tokens else ProviderToken()
github_token = (
provider_tokens.get(ProviderType.GITLAB, ProviderToken())
if provider_tokens
else ProviderToken()
)
github_service = GitLabServiceImpl(
user_id=github_token.user_id,
external_auth_id=user_id,
external_auth_token=access_token,
token=github_token.token,
base_domain=github_token.host
base_domain=github_token.host,
)
try:
@@ -137,5 +154,3 @@ async def create_mr(
response = str(e)
return response

View File

@@ -188,10 +188,7 @@ async def load_custom_secrets_names(
) -> GETCustomSecrets | JSONResponse:
try:
if not user_secrets:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={'error': 'User secrets not found'},
)
return GETCustomSecrets(custom_secrets=[])
custom_secrets: list[CustomSecretWithoutValueModel] = []
if user_secrets.custom_secrets:
@@ -220,31 +217,30 @@ async def create_custom_secret(
) -> JSONResponse:
try:
existing_secrets = await secrets_store.load()
if existing_secrets:
custom_secrets = dict(existing_secrets.custom_secrets)
custom_secrets = dict(existing_secrets.custom_secrets) if existing_secrets else {}
secret_name = incoming_secret.name
secret_value = incoming_secret.value
secret_description = incoming_secret.description
secret_name = incoming_secret.name
secret_value = incoming_secret.value
secret_description = incoming_secret.description
if secret_name in custom_secrets:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'message': f'Secret {secret_name} already exists'},
)
custom_secrets[secret_name] = CustomSecret(
secret=secret_value,
description=secret_description or '',
if secret_name in custom_secrets:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={'message': f'Secret {secret_name} already exists'},
)
# Create a new UserSecrets that preserves provider tokens
updated_user_secrets = UserSecrets(
custom_secrets=custom_secrets,
provider_tokens=existing_secrets.provider_tokens,
)
custom_secrets[secret_name] = CustomSecret(
secret=secret_value,
description=secret_description or '',
)
await secrets_store.store(updated_user_secrets)
# Create a new UserSecrets that preserves provider tokens
updated_user_secrets = UserSecrets(
custom_secrets=custom_secrets,
provider_tokens=existing_secrets.provider_tokens if existing_secrets else {},
)
await secrets_store.store(updated_user_secrets)
return JSONResponse(
status_code=status.HTTP_201_CREATED,

View File

@@ -1,16 +1,26 @@
from typing import Any
import uuid
from typing import Any
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.message import MessageAction
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA, PROVIDER_TOKEN_TYPE
from openhands.integrations.provider import (
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA,
PROVIDER_TOKEN_TYPE,
)
from openhands.integrations.service_types import ProviderType
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import ConversationStoreImpl, SettingsStoreImpl, config, conversation_manager
from openhands.server.shared import (
ConversationStoreImpl,
SettingsStoreImpl,
config,
conversation_manager,
)
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.conversation_metadata import ConversationMetadata, ConversationTrigger
from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
from openhands.utils.conversation_summary import get_default_conversation_title
@@ -69,7 +79,7 @@ async def create_new_conversation(
conversation_init_data = ConversationInitData(**session_init_args)
logger.info('Loading conversation store')
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
logger.info('Conversation store loaded')
logger.info('ServerConversation store loaded')
# For nested runtimes, we allow a single conversation id, passed in on container creation
if conversation_id is None:
@@ -101,7 +111,7 @@ async def create_new_conversation(
extra={'user_id': user_id, 'session_id': conversation_id},
)
initial_message_action = None
if initial_user_msg or image_urls:
if initial_user_msg or image_urls:
initial_message_action = MessageAction(
content=initial_user_msg or '',
image_urls=image_urls or [],
@@ -109,7 +119,7 @@ async def create_new_conversation(
if attach_convo_id and conversation_instructions:
conversation_instructions = conversation_instructions.format(conversation_id)
agent_loop_info = await conversation_manager.maybe_start_agent_loop(
conversation_id,
conversation_init_data,

View File

@@ -9,15 +9,18 @@ from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.controller.replay import ReplayManager
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig, AppConfig, LLMConfig
from openhands.core.config import AgentConfig, LLMConfig, OpenHandsConfig
from openhands.core.exceptions import AgentRuntimeUnavailableError
from openhands.core.logger import OpenHandsLoggerAdapter
from openhands.core.schema.agent import AgentState
from openhands.events.action import ChangeAgentStateAction, MessageAction
from openhands.events.event import Event, EventSource
from openhands.events.stream import EventStream
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE, ProviderHandler
from openhands.integrations.service_types import ProviderType
from openhands.integrations.provider import (
CUSTOM_SECRETS_TYPE,
PROVIDER_TOKEN_TYPE,
ProviderHandler,
)
from openhands.mcp import add_mcp_tools_to_agent
from openhands.memory.memory import Memory
from openhands.microagent.microagent import BaseMicroagent
@@ -80,7 +83,7 @@ class AgentSession:
async def start(
self,
runtime_name: str,
config: AppConfig,
config: OpenHandsConfig,
agent: Agent,
max_iterations: int,
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
@@ -118,7 +121,9 @@ class AgentSession:
finished = False # For monitoring
runtime_connected = False
custom_secrets_handler = UserSecrets(custom_secrets=custom_secrets if custom_secrets else {})
custom_secrets_handler = UserSecrets(
custom_secrets=custom_secrets if custom_secrets else {}
)
try:
self._create_security_analyzer(config.security.security_analyzer)
@@ -147,7 +152,7 @@ class AgentSession:
selected_repository=selected_repository,
repo_directory=repo_directory,
conversation_instructions=conversation_instructions,
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions()
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions(),
)
# NOTE: this needs to happen before controller is created
@@ -232,7 +237,7 @@ class AgentSession:
initial_message: MessageAction | None,
replay_json: str,
agent: Agent,
config: AppConfig,
config: OpenHandsConfig,
max_iterations: int,
max_budget_per_task: float | None,
agent_to_llm_config: dict[str, LLMConfig] | None,
@@ -271,11 +276,10 @@ class AgentSession:
security_analyzer, SecurityAnalyzer
)(self.event_stream)
def override_provider_tokens_with_custom_secret(
self,
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
custom_secrets: CUSTOM_SECRETS_TYPE | None
custom_secrets: CUSTOM_SECRETS_TYPE | None,
):
if git_provider_tokens and custom_secrets:
tokens = dict(git_provider_tokens)
@@ -283,15 +287,14 @@ class AgentSession:
token_name = ProviderHandler.get_provider_env_key(provider)
if token_name in custom_secrets or token_name.upper() in custom_secrets:
del tokens[provider]
return MappingProxyType(tokens)
return git_provider_tokens
async def _create_runtime(
self,
runtime_name: str,
config: AppConfig,
config: OpenHandsConfig,
agent: Agent,
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
custom_secrets: CUSTOM_SECRETS_TYPE | None = None,
@@ -317,10 +320,14 @@ class AgentSession:
self.logger.debug(f'Initializing runtime `{runtime_name}` now...')
runtime_cls = get_runtime_cls(runtime_name)
if runtime_cls == RemoteRuntime:
if runtime_cls == RemoteRuntime:
# If provider tokens is passed in custom secrets, then remove provider from provider tokens
# We prioritize provider tokens set in custom secrets
provider_tokens_without_gitlab = self.override_provider_tokens_with_custom_secret(git_provider_tokens, custom_secrets)
provider_tokens_without_gitlab = (
self.override_provider_tokens_with_custom_secret(
git_provider_tokens, custom_secrets
)
)
self.runtime = runtime_cls(
config=config,
@@ -339,7 +346,7 @@ class AgentSession:
provider_tokens=git_provider_tokens
or cast(PROVIDER_TOKEN_TYPE, MappingProxyType({}))
)
# Merge git provider tokens with custom secrets before passing over to runtime
env_vars.update(await provider_handler.get_env_vars(expose_secrets=True))
self.runtime = runtime_cls(
@@ -436,11 +443,11 @@ class AgentSession:
return controller
async def _create_memory(
self,
selected_repository: str | None,
repo_directory: str | None,
self,
selected_repository: str | None,
repo_directory: str | None,
conversation_instructions: str | None,
custom_secrets_descriptions: dict[str, str]
custom_secrets_descriptions: dict[str, str],
) -> Memory:
memory = Memory(
event_stream=self.event_stream,
@@ -461,10 +468,7 @@ class AgentSession:
memory.load_user_workspace_microagents(microagents)
if selected_repository and repo_directory:
memory.set_repository_info(
selected_repository,
repo_directory
)
memory.set_repository_info(selected_repository, repo_directory)
return memory
def _maybe_restore_state(self) -> State | None:

View File

@@ -1,6 +1,6 @@
import asyncio
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.events.stream import EventStream
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
@@ -9,7 +9,7 @@ from openhands.storage.files import FileStore
from openhands.utils.async_utils import call_sync_from_async
class Conversation:
class ServerConversation:
sid: str
file_store: FileStore
event_stream: EventStream
@@ -17,7 +17,11 @@ class Conversation:
user_id: str | None
def __init__(
self, sid: str, file_store: FileStore, config: AppConfig, user_id: str | None
self,
sid: str,
file_store: FileStore,
config: OpenHandsConfig,
user_id: str | None,
):
self.sid = sid
self.config = config

View File

@@ -6,7 +6,7 @@ from logging import LoggerAdapter
import socketio
from openhands.controller.agent import Agent
from openhands.core.config import AppConfig
from openhands.core.config import OpenHandsConfig
from openhands.core.config.condenser_config import (
BrowserOutputCondenserConfig,
CondenserPipelineConfig,
@@ -43,7 +43,7 @@ class Session:
is_alive: bool = True
agent_session: AgentSession
loop: asyncio.AbstractEventLoop
config: AppConfig
config: OpenHandsConfig
file_store: FileStore
user_id: str | None
logger: LoggerAdapter
@@ -51,7 +51,7 @@ class Session:
def __init__(
self,
sid: str,
config: AppConfig,
config: OpenHandsConfig,
file_store: FileStore,
sio: socketio.AsyncServer | None,
user_id: str | None = None,
@@ -128,9 +128,15 @@ class Session:
self.config.search_api_key = settings.search_api_key
# NOTE: this need to happen AFTER the config is updated with the search_api_key
self.config.mcp = settings.mcp_config or MCPConfig(sse_servers=[], stdio_servers=[])
self.config.mcp = settings.mcp_config or MCPConfig(
sse_servers=[], stdio_servers=[]
)
# Add OpenHands' MCP server by default
openhands_mcp_server, openhands_mcp_stdio_servers = OpenHandsMCPConfigImpl.create_default_mcp_server_config(self.config.mcp_host, self.config, self.user_id)
openhands_mcp_server, openhands_mcp_stdio_servers = (
OpenHandsMCPConfigImpl.create_default_mcp_server_config(
self.config.mcp_host, self.config, self.user_id
)
)
if openhands_mcp_server:
self.config.mcp.sse_servers.append(openhands_mcp_server)
self.config.mcp.stdio_servers.extend(openhands_mcp_stdio_servers)
@@ -154,7 +160,7 @@ class Session:
),
]
)
self.logger.info(
f'Enabling pipeline condenser with:'
f' browser_output_masking(attention_window=2), '

View File

@@ -3,8 +3,8 @@ import os
import socketio
from dotenv import load_dotenv
from openhands.core.config import load_app_config
from openhands.core.config.app_config import AppConfig
from openhands.core.config import load_openhands_config
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.server.config.server_config import ServerConfig, load_server_config
from openhands.server.conversation_manager.conversation_manager import (
ConversationManager,
@@ -20,7 +20,7 @@ from openhands.utils.import_utils import get_impl
load_dotenv()
config: AppConfig = load_app_config()
config: OpenHandsConfig = load_openhands_config()
server_config_interface: ServerConfigInterface = load_server_config()
assert isinstance(server_config_interface, ServerConfig), (
'Loaded server config interface is not a ServerConfig, despite this being assumed'

View File

@@ -1,5 +1,6 @@
from openhands.storage.files import FileStore
from openhands.storage.google_cloud import GoogleCloudFileStore
from openhands.storage.http import HTTPFileStore
from openhands.storage.local import LocalFileStore
from openhands.storage.memory import InMemoryFileStore
from openhands.storage.s3 import S3FileStore
@@ -14,4 +15,8 @@ def get_file_store(file_store: str, file_store_path: str | None = None) -> FileS
return S3FileStore(file_store_path)
elif file_store == 'google_cloud':
return GoogleCloudFileStore(file_store_path)
elif file_store == 'http':
if file_store_path is None:
raise ValueError('file_store_path is required for HTTP file store')
return HTTPFileStore(file_store_path)
return InMemoryFileStore()

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Iterable
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
from openhands.storage.data_models.conversation_metadata_result_set import (
ConversationMetadataResultSet,
@@ -22,9 +22,7 @@ class ConversationStore(ABC):
async def get_metadata(self, conversation_id: str) -> ConversationMetadata:
"""Load conversation metadata."""
async def validate_metadata(
self, conversation_id: str, user_id: str
) -> bool:
async def validate_metadata(self, conversation_id: str, user_id: str) -> bool:
"""Validate that conversation belongs to the current user."""
metadata = await self.get_metadata(conversation_id)
if not metadata.user_id or metadata.user_id != user_id:
@@ -57,6 +55,6 @@ class ConversationStore(ABC):
@classmethod
@abstractmethod
async def get_instance(
cls, config: AppConfig, user_id: str | None
cls, config: OpenHandsConfig, user_id: str | None
) -> ConversationStore:
"""Get a store for the user represented by the token given."""

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from pydantic import TypeAdapter
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.storage import get_file_store
from openhands.storage.conversation.conversation_store import ConversationStore
@@ -42,7 +42,7 @@ class FileConversationStore(ConversationStore):
json_obj = json.loads(json_str)
if 'created_at' not in json_obj:
raise FileNotFoundError(path)
# Remove github_user_id if it exists
if 'github_user_id' in json_obj:
json_obj.pop('github_user_id')
@@ -103,7 +103,7 @@ class FileConversationStore(ConversationStore):
@classmethod
async def get_instance(
cls, config: AppConfig, user_id: str | None
cls, config: OpenHandsConfig, user_id: str | None
) -> FileConversationStore:
file_store = get_file_store(config.file_store, config.file_store_path)
return FileConversationStore(file_store)

View File

@@ -12,7 +12,7 @@ from pydantic.json import pydantic_encoder
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.utils import load_app_config
from openhands.core.config.utils import load_openhands_config
from openhands.storage.data_models.user_secrets import UserSecrets
@@ -41,7 +41,6 @@ class Settings(BaseModel):
mcp_config: MCPConfig | None = None
search_api_key: SecretStr | None = None
model_config = {
'validate_assignment': True,
}
@@ -54,7 +53,7 @@ class Settings(BaseModel):
"""
if api_key is None:
return None
context = info.context
if context and context.get('expose_secrets', False):
return api_key.get_secret_value()
@@ -106,7 +105,7 @@ class Settings(BaseModel):
@staticmethod
def from_config() -> Settings | None:
app_config = load_app_config()
app_config = load_openhands_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

209
openhands/storage/http.py Normal file
View File

@@ -0,0 +1,209 @@
import json
import logging
import os
import urllib.parse
from typing import Union
import httpx
from requests.exceptions import RequestException
from openhands.storage.files import FileStore
# Use standard logging instead of importing from core.logger
logger = logging.getLogger('openhands')
class HTTPFileStore(FileStore):
"""
A FileStore implementation that uses HTTP requests to store and retrieve files.
This implementation allows storing files on a remote HTTP server that implements
a simple REST API for file operations.
The server should implement the following endpoints:
- POST /files/{path} - Write a file
- GET /files/{path} - Read a file
- OPTIONS /files/{path} - List files in a directory
- DELETE /files/{path} - Delete a file or directory
Authentication can be provided by customizing the provided httpx client.
A (mock) server implementation is available in the MockHttpxClient class
located at /tests/unit/test_storage.py
"""
base_url: str
client: httpx.Client
def __init__(
self,
base_url: str,
client: httpx.Client | None = None,
) -> None:
"""
Initialize the HTTP file store.
Args:
base_url: The base URL of the HTTP file server
api_key: Optional API key for authentication
username: Optional username for basic authentication
password: Optional password for basic authentication
bearer_token: Optional bearer token for authentication
timeout: Request timeout in seconds
verify_ssl: Whether to verify SSL certificates
"""
self.base_url = base_url.rstrip('/')
if not client:
headers = {}
if os.getenv('SESSION_API_KEY'):
headers['X-Session-API-Key'] = os.getenv('SESSION_API_KEY')
client = httpx.Client(headers=headers)
self.client = client
def _get_file_url(self, path: str) -> str:
"""
Get the full URL for a file path.
Args:
path: The file path
Returns:
The full URL
"""
# Ensure path starts with a slash
if not path.startswith('/'):
path = '/' + path
# URL encode the path
encoded_path = urllib.parse.quote(path)
return f'{self.base_url}{encoded_path}'
def write(self, path: str, contents: Union[str, bytes]) -> None:
"""
Write contents to a file.
Args:
path: The file path
contents: The file contents (string or bytes)
Raises:
FileNotFoundError: If the file cannot be written
"""
url = self._get_file_url(path)
try:
# Convert string to bytes if needed
if isinstance(contents, str):
contents = contents.encode('utf-8')
response = self.client.post(url, content=contents)
if response.status_code not in (200, 201, 204):
raise FileNotFoundError(
f'Error: Failed to write to path {path}. '
f'Status code: {response.status_code}, Response: {response.text}'
)
logger.debug(f'Successfully wrote to {path}')
except RequestException as e:
raise FileNotFoundError(f'Error: Failed to write to path {path}: {str(e)}')
def read(self, path: str) -> str:
"""
Read contents from a file.
Args:
path: The file path
Returns:
The file contents as a string
Raises:
FileNotFoundError: If the file cannot be read
"""
url = self._get_file_url(path)
try:
response = self.client.get(url)
if response.status_code != 200:
raise FileNotFoundError(
f'Error: Failed to read from path {path}. '
f'Status code: {response.status_code}, Response: {response.text}'
)
return response.text
except RequestException as e:
raise FileNotFoundError(f'Error: Failed to read from path {path}: {str(e)}')
def list(self, path: str) -> list[str]:
"""
List files in a directory.
Args:
path: The directory path
Returns:
A list of file paths
Raises:
FileNotFoundError: If the directory cannot be listed
"""
url = f'{self._get_file_url(path)}'
try:
response = self.client.options(url)
if response.status_code != 200:
if response.status_code == 404:
return []
raise FileNotFoundError(
f'Error: Failed to list path {path}. '
f'Status code: {response.status_code}, Response: {response.text}'
)
try:
files = response.json()
if not isinstance(files, list):
raise FileNotFoundError(
f'Error: Invalid response format when listing path {path}. '
f'Expected a list, got: {type(files)}'
)
return files
except json.JSONDecodeError:
raise FileNotFoundError(
f'Error: Invalid JSON response when listing path {path}. '
f'Response: {response.text}'
)
except RequestException as e:
raise FileNotFoundError(f'Error: Failed to list path {path}: {str(e)}')
def delete(self, path: str) -> None:
"""
Delete a file or directory.
Args:
path: The file or directory path
Raises:
FileNotFoundError: If the file or directory cannot be deleted
"""
url = self._get_file_url(path)
try:
response = self.client.delete(url)
# 404 is acceptable for delete operations
if response.status_code not in (200, 202, 204, 404):
raise FileNotFoundError(
f'Error: Failed to delete path {path}. '
f'Status code: {response.status_code}, Response: {response.text}'
)
logger.debug(f'Successfully deleted {path}')
except RequestException as e:
raise FileNotFoundError(f'Error: Failed to delete path {path}: {str(e)}')

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import json
from dataclasses import dataclass
from openhands.core.config.app_config import AppConfig
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.storage import get_file_store
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.files import FileStore
@@ -37,7 +37,7 @@ class FileSecretsStore(SecretsStore):
@classmethod
async def get_instance(
cls, config: AppConfig, user_id: str | None
cls, config: OpenHandsConfig, user_id: str | None
) -> FileSecretsStore:
file_store = get_file_store(config.file_store, config.file_store_path)
return FileSecretsStore(file_store)

Some files were not shown because too many files have changed in this diff Show More