Compare commits

..

1 Commits

Author SHA1 Message Date
Robert Brennan 1da841fd49 Update repo.md 2025-05-28 09:34:51 -04:00
201 changed files with 3153 additions and 3545 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
/frontend/ @rbren @amanape
# Evaluation code owners
/evaluation/ @xingyaoww @neubig
/evaluation/ @xingyaoww @neubig
# Documentation code owners
/docs/ @mamoodi
+1 -14
View File
@@ -161,6 +161,7 @@ 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
@@ -170,20 +171,6 @@ 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
+1
View File
@@ -26,6 +26,7 @@ Backend:
- Located in the `openhands` directory
- Testing:
- All tests are in `tests/unit/test_*.py`
- To run all the unit tests, run `poetry run pytest --forked -n auto -svv ./tests/unit`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
@@ -178,4 +178,4 @@ interface OpenHandsEvent {
### Event Handling Issues
- Check that you're correctly parsing the event data
- Verify that your event handlers are properly registered
- Verify that your event handlers are properly registered
+1 -1
View File
@@ -1,6 +1,6 @@
# Azure
OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their documentation on using Azure as a
OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their documentation on using Azure as a
provider [here](https://docs.litellm.ai/docs/providers/azure).
## Azure OpenAI Configuration
+2 -2
View File
@@ -10,7 +10,7 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
- `LLM Provider` to `Gemini`
- `LLM Model` to the model you will be using.
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
(e.g. gemini/<model-name> like `gemini/gemini-2.0-flash`).
- `API Key` to your Gemini API key
@@ -28,5 +28,5 @@ VERTEXAI_LOCATION="<your-gcp-location>"
Then set the following in the OpenHands UI through the Settings under the `LLM` tab:
- `LLM Provider` to `VertexAI`
- `LLM Model` to the model you will be using.
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
If the model is not in the list, enable `Advanced` options, and enter it in `Custom Model`
(e.g. vertex_ai/&lt;model-name&gt;).
+2 -2
View File
@@ -1,6 +1,6 @@
# Groq
OpenHands uses LiteLLM to make calls to chat models on Groq. You can find their documentation on using Groq as a
OpenHands uses LiteLLM to make calls to chat models on Groq. You can find their documentation on using Groq as a
provider [here](https://docs.litellm.ai/docs/providers/groq).
## Configuration
@@ -8,7 +8,7 @@ provider [here](https://docs.litellm.ai/docs/providers/groq).
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
- `LLM Provider` to `Groq`
- `LLM Model` to the model you will be using. [Visit here to see the list of
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list,
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list,
enable `Advanced` options, and enter it in `Custom Model` (e.g. groq/&lt;model-name&gt; like `groq/llama3-70b-8192`).
- `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
+1 -1
View File
@@ -15,7 +15,7 @@ To use LiteLLM proxy with OpenHands, you need to:
## Supported Models
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy
is configured to handle.
Refer to your LiteLLM proxy configuration for the list of available models and their names.
+1 -1
View File
@@ -25,7 +25,7 @@ OpenHands will issue many prompts to the LLM you configure. Most of these LLMs c
limits and monitor usage.
:::
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
For a full list of the providers and models available, please consult the
+2 -2
View File
@@ -25,7 +25,7 @@ We recommend using [LMStudio](https://lmstudio.ai/) for serving these models loc
- Option 2: Download a LLM in GGUF format. For example, to download [Devstral Small 2505 GGUF](https://huggingface.co/mistralai/Devstral-Small-2505_gguf), using `huggingface-cli download mistralai/Devstral-Small-2505_gguf --local-dir mistralai/Devstral-Small-2505_gguf`. Then in bash terminal, run `lms import {model_name}` in the directory where you've downloaded the model checkpoint (e.g. run `lms import devstralQ4_K_M.gguf` in `mistralai/Devstral-Small-2505_gguf`)
3. Open LM Studio application, you should first switch to `power user` mode, and then open the developer tab:
![image](./screenshots/1_select_power_user.png)
4. Then click `Select a model to load` on top of the application:
@@ -154,7 +154,7 @@ Start OpenHands using `make run`.
### Configure OpenHands
Once OpenHands is running, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
Once OpenHands is running, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
1. Enable `Advanced` options.
2. Set the following:
- `Custom Model` to `openai/<served-model-name>` (e.g. `openai/openhands-lm-32b-v0.1`)
+1 -1
View File
@@ -1,6 +1,6 @@
# OpenAI
OpenHands uses LiteLLM to make calls to OpenAI's chat models. You can find their documentation on using OpenAI as a
OpenHands uses LiteLLM to make calls to OpenAI's chat models. You can find their documentation on using OpenAI as a
provider [here](https://docs.litellm.ai/docs/providers/openai).
## Configuration
+2 -2
View File
@@ -1,6 +1,6 @@
# OpenRouter
OpenHands uses LiteLLM to make calls to chat models on OpenRouter. You can find their documentation on using
OpenHands uses LiteLLM to make calls to chat models on OpenRouter. You can find their documentation on using
OpenRouter as a provider [here](https://docs.litellm.ai/docs/providers/openrouter).
## Configuration
@@ -9,6 +9,6 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
* `LLM Provider` to `OpenRouter`
* `LLM Model` to the model you will be using.
[Visit here to see a full list of OpenRouter models](https://openrouter.ai/models).
If the model is not in the list, enable `Advanced` options, and enter it in
If the model is not in the list, enable `Advanced` options, and enter it in
`Custom Model` (e.g. openrouter/&lt;model-name&gt; like `openrouter/anthropic/claude-3.5-sonnet`).
* `API Key` to your OpenRouter API key.
@@ -6,7 +6,7 @@ Organizations and users can define microagents that apply to all repositories be
## Usage
These microagents can be [any type of microagent](./microagents-overview#microagent-types) and will be loaded
These microagents can be [any type of microagent](./microagents-overview#microagent-types) and will be loaded
accordingly. However, they are applied to all repositories belonging to the organization or user.
Add a `.openhands` repository under the organization or user and create a `microagents` directory and place the
+1 -1
View File
@@ -15,7 +15,7 @@ Before using the Local Runtime, ensure that:
1. You can run OpenHands using the [Development workflow](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
2. For Linux and Mac, tmux is available on your system.
3. For Windows, PowerShell is available on your system.
- Only [CLI mode](../how-to/cli-mode) and [headless mode](../how-to/headless-mode) are supported in Windows with Local Runtime.
- Only [CLI mode](../how-to/cli-mode) and [headless mode](../how-to/headless-mode) are supported in Windows with Local Runtime.
## Configuration
+1906 -1686
View File
File diff suppressed because it is too large Load Diff
+5 -5
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.8.0",
"@docusaurus/plugin-content-pages": "^3.8.0",
"@docusaurus/preset-classic": "^3.8.0",
"@docusaurus/theme-mermaid": "^3.8.0",
"@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-pages": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.7.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.8.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.5.1",
"swagger-cli": "^4.0.4",
"swagger-ui-dist": "^5.22.0",
+3 -3
View File
@@ -17,7 +17,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
@@ -59,10 +59,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-slim'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
@@ -24,7 +24,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
load_from_toml,
parse_arguments,
@@ -46,10 +46,10 @@ SKIP_NUM = (
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.11-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
+3 -3
View File
@@ -22,7 +22,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -55,12 +55,12 @@ FILE_EXT_MAP = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -70,11 +70,11 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -18,7 +18,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -33,13 +33,13 @@ SUPPORTED_AGENT_CLS = {'CodeActAgent'}
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -25,7 +25,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -61,10 +61,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
@@ -47,10 +47,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -19,7 +19,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
@@ -39,10 +39,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -37,7 +37,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
@@ -60,10 +60,10 @@ ACTION_FORMAT = """
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -81,10 +81,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -201,7 +201,7 @@ def process_instance(
) -> EvalOutput:
config = get_config(metadata)
# use a session id for concurrent evaluation
conversation_id = _get_instance_id(instance)
sid = _get_instance_id(instance)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
@@ -218,7 +218,7 @@ def process_instance(
# Prepare instruction
instruction = (
f'Please fix the function in {conversation_id}.py such that all test cases pass.\n'
f'Please fix the function in {sid}.py such that all test cases pass.\n'
'Environment has been set up for you to start working. You may assume all necessary tools are installed.\n\n'
'# Problem Statement\n'
f'{problem_statement}\n\n'
@@ -19,10 +19,10 @@ from evaluation.utils.shared import (
make_metadata,
)
from openhands.core.config import (
AppConfig,
LLMConfig,
OpenHandsConfig,
get_parser,
load_openhands_config,
load_app_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,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -53,7 +53,7 @@ def get_config(
return config
config = load_openhands_config()
config = load_app_config()
def load_bench_config():
@@ -29,10 +29,10 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
load_openhands_config,
load_app_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,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -63,7 +63,7 @@ def get_config(
return config
config = load_openhands_config()
config = load_app_config()
def load_bench_config():
@@ -17,7 +17,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
@@ -44,14 +44,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
+3 -3
View File
@@ -22,7 +22,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -4,11 +4,11 @@ import pprint
import tqdm
from openhands.core.config import get_llm_config_arg, get_parser, load_openhands_config
from openhands.core.config import get_llm_config_arg, get_parser, load_app_config
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
config = load_openhands_config()
config = load_app_config()
def extract_test_results(res_file_path: str) -> tuple[list[str], list[str]]:
+5 -5
View File
@@ -33,10 +33,10 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
load_openhands_config,
load_app_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_openhands_config()
config = load_app_config()
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -76,10 +76,10 @@ ID2CONDA = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -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) -> OpenHandsConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
# 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) -> OpenHandsConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
config = AppConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -20,7 +20,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = (
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
)
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
@@ -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) -> OpenHandsConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
# 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) -> OpenHandsConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
config = AppConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
@@ -1,4 +1,4 @@
TASK_INSTRUECTION = """
TASK_INSTRUECTION="""
Given the following GitHub problem description, your objective is to localize the specific files, classes or functions, and lines of code that need modification or contain key information to resolve the issue.
Follow these steps to localize the issue:
@@ -66,4 +66,4 @@ FAKE_USER_MSG_FOR_LOC = (
'Verify that you have carefully analyzed the impact of the found locations on the repository, especially their dependencies. '
'If you think you have solved the task, please send your final answer (including the former answer and reranking) to user through message and then call `finish` to finish.\n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
)
)
+8 -11
View File
@@ -40,12 +40,12 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.config.condenser_config import NoOpCondenserConfig
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,
) -> OpenHandsConfig:
) -> AppConfig:
# 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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -721,16 +721,15 @@ 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"...')
@@ -807,9 +806,7 @@ 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)
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
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):
_get_swebench_workspace_dir_name(instance)
workspace_dir_name = _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,
) -> OpenHandsConfig:
) -> AppConfig:
# 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 = OpenHandsConfig(
config = AppConfig(
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,13 +364,9 @@ 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'
@@ -41,7 +41,7 @@ from evaluation.utils.shared import (
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_parser
from openhands.core.config import AppConfig, 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) -> OpenHandsConfig:
def get_config(instance: pd.Series) -> AppConfig:
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 OpenHandsConfig(
return AppConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
@@ -35,7 +35,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
# 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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -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,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
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: OpenHandsConfig,
config: AppConfig,
dependencies: list[str],
save_final_state: bool,
state_dir: str,
@@ -141,7 +141,7 @@ def run_solver(
state: State | None = asyncio.run(
run_controller(
config=config,
conversation_id=task_name,
sid=task_name,
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=codeact_user_response,
@@ -274,7 +274,7 @@ if __name__ == '__main__':
temp_dir = os.path.abspath(os.getenv('TMPDIR'))
else:
temp_dir = tempfile.mkdtemp()
config: OpenHandsConfig = get_config(
config: AppConfig = get_config(
args.task_image_name, task_short_name, temp_dir, agent_llm_config, agent_config
)
runtime: Runtime = create_runtime(config)
+3 -3
View File
@@ -18,7 +18,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
@@ -40,10 +40,10 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
# 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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
@@ -20,7 +20,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
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,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -19,7 +19,7 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -44,7 +44,7 @@ SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> OpenHandsConfig:
) -> AppConfig:
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 = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
+3 -3
View File
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
AppConfig,
get_llm_config_arg,
parse_arguments,
)
@@ -41,10 +41,10 @@ FAKE_RESPONSES = {
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> OpenHandsConfig:
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.platform = 'linux/amd64'
config = OpenHandsConfig(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
+2 -2
View File
@@ -2,9 +2,9 @@ import argparse
import pytest
from openhands.config import load_openhands_config
from openhands.config import load_app_config
config = load_openhands_config()
config = load_app_config()
if __name__ == '__main__':
"""Main entry point of the script.
@@ -16,8 +16,8 @@ vi.mock("react-i18next", async () => {
if (i18nKey === "SETTINGS$API_KEYS_DESCRIPTION") {
return (
<span>
API keys allow you to authenticate with the OpenHands API programmatically.
Keep your API keys secure; anyone with your API key can access your account.
API keys allow you to authenticate with the OpenHands API programmatically.
Keep your API keys secure; anyone with your API key can access your account.
For more information on how to use the API, see our {components.a}
</span>
);
@@ -48,7 +48,7 @@ describe("ApiKeysManager", () => {
it("should render the API documentation link", () => {
renderComponent();
// Find the link to the API documentation
const link = screen.getByRole("link");
expect(link).toBeInTheDocument();
@@ -56,4 +56,4 @@ describe("ApiKeysManager", () => {
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
});
});
@@ -60,11 +60,11 @@ Object.entries(translationJson).forEach(([key, translations]) => {
if (Object.keys(missingTranslations).length > 0) {
console.error('\x1b[31m%s\x1b[0m', 'ERROR: Missing translations detected');
console.error(`Found ${Object.keys(missingTranslations).length} translation keys with missing languages:`);
Object.entries(missingTranslations).forEach(([key, langs]) => {
console.error(`- Key "${key}" is missing translations for: ${langs.join(', ')}`);
});
console.error('\nPlease add the missing translations before committing.');
}
@@ -72,11 +72,11 @@ if (Object.keys(missingTranslations).length > 0) {
if (Object.keys(extraLanguages).length > 0) {
console.error('\x1b[31m%s\x1b[0m', 'ERROR: Extra languages detected');
console.error(`Found ${Object.keys(extraLanguages).length} translation keys with extra languages not in AvailableLanguages:`);
Object.entries(extraLanguages).forEach(([key, langs]) => {
console.error(`- Key "${key}" has translations for unsupported languages: ${langs.join(', ')}`);
});
console.error('\nPlease remove the extra languages before committing.');
}
@@ -85,4 +85,4 @@ if (hasErrors) {
process.exit(1);
} else {
console.log('\x1b[32m%s\x1b[0m', 'All translation keys have complete language coverage!');
}
}
@@ -129,7 +129,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
isDisabled={isPending}
>
{isPending
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL)
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) || "Submitting..."
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
</BrandButton>
<BrandButton
@@ -144,7 +144,8 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
</div>
{isPending && (
<p className="text-sm text-center text-neutral-400">
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE)}
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE) ||
"Submitting your feedback, please wait..."}
</p>
)}
</form>
@@ -9,6 +9,7 @@ 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;
@@ -25,6 +26,10 @@ 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;
}
@@ -32,6 +37,10 @@ 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;
}
+4 -4
View File
@@ -166,8 +166,6 @@ export function WsClientProvider({
}
function handleMessage(event: Record<string, unknown>) {
handleAssistantMessage(event);
if (isOpenHandsEvent(event)) {
const isStatusUpdateError =
isStatusUpdate(event) && event.type === "error";
@@ -257,6 +255,8 @@ export function WsClientProvider({
if (!Number.isNaN(parseInt(event.id as string, 10))) {
lastEventRef.current = event;
}
handleAssistantMessage(event);
}
function handleDisconnect(data: unknown) {
@@ -289,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");
}
@@ -1,4 +1,5 @@
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";
@@ -7,6 +8,7 @@ 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"),
@@ -22,6 +24,7 @@ export const useLogout = () => {
}
posthog.reset();
await navigate("/");
// Refresh the page after all logout logic is completed
window.location.reload();
-49
View File
@@ -1,49 +0,0 @@
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]);
};
+1 -5
View File
@@ -53,12 +53,8 @@ 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 = url.toString();
window.location.href = authUrl;
}
}, [
config?.APP_MODE,
+2 -4
View File
@@ -36,7 +36,6 @@ 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();
@@ -44,7 +43,6 @@ 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();
@@ -56,13 +54,13 @@ function AppContent() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
if (isFetched && !conversation && isAuthed) {
if (isFetched && !conversation) {
displayErrorToast(
"This conversation does not exist, or you do not have permission to access it.",
);
navigate("/");
}
}, [conversation, isFetched, isAuthed]);
}, [conversation, isFetched]);
React.useEffect(() => {
dispatch(clearTerminal());
+2 -39
View File
@@ -23,7 +23,6 @@ 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() {
@@ -89,9 +88,6 @@ 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) {
@@ -135,8 +131,8 @@ export default function MainApp() {
}
}, [error?.status, pathname, isOnTosPage]);
// Function to check if login method exists in local storage
const checkLoginMethodExists = React.useCallback(() => {
// Check if login method exists in local storage
const loginMethodExists = React.useMemo(() => {
// 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;
@@ -144,39 +140,6 @@ 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 &&
+1 -4
View File
@@ -16,8 +16,5 @@ export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
authUrl = `auth.${requestUrl.hostname}`;
}
const scope = "openid email profile"; // OAuth scope - not user-facing
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)}`;
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)}`;
};
+27 -27
View File
@@ -19,10 +19,10 @@ vi.mock("react-i18next", () => ({
describe("RepositorySelectionForm", () => {
const mockOnRepoSelection = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
// Mock the hooks with default values
(useUserRepositories as any).mockReturnValue({
data: [
@@ -32,7 +32,7 @@ describe("RepositorySelectionForm", () => {
isLoading: false,
isError: false,
});
(useRepositoryBranches as any).mockReturnValue({
data: [
{ name: "main" },
@@ -41,90 +41,90 @@ describe("RepositorySelectionForm", () => {
isLoading: false,
isError: false,
});
(useCreateConversation as any).mockReturnValue({
mutate: vi.fn(),
isPending: false,
isSuccess: false,
});
(useIsCreatingConversation as any).mockReturnValue(false);
});
it("should clear selected branch when input is empty", async () => {
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
// First select a repository to enable the branch dropdown
const repoDropdown = screen.getByTestId("repository-dropdown");
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
// Get the branch dropdown and verify it's enabled
const branchDropdown = screen.getByTestId("branch-dropdown");
expect(branchDropdown).not.toBeDisabled();
// Simulate deleting all text in the branch input
fireEvent.change(branchDropdown, { target: { value: "" } });
// Verify the branch input is cleared (no selected branch)
expect(branchDropdown).toHaveValue("");
});
it("should clear selected branch when input contains only whitespace", async () => {
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
// First select a repository to enable the branch dropdown
const repoDropdown = screen.getByTestId("repository-dropdown");
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
// Get the branch dropdown and verify it's enabled
const branchDropdown = screen.getByTestId("branch-dropdown");
expect(branchDropdown).not.toBeDisabled();
// Simulate entering only whitespace in the branch input
fireEvent.change(branchDropdown, { target: { value: " " } });
// Verify the branch input is cleared (no selected branch)
expect(branchDropdown).toHaveValue("");
});
it("should keep branch empty after being cleared even with auto-selection", async () => {
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />);
// First select a repository to enable the branch dropdown
const repoDropdown = screen.getByTestId("repository-dropdown");
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
// Get the branch dropdown and verify it's enabled
const branchDropdown = screen.getByTestId("branch-dropdown");
expect(branchDropdown).not.toBeDisabled();
// The branch should be auto-selected to "main" initially
expect(branchDropdown).toHaveValue("main");
// Simulate deleting all text in the branch input
fireEvent.change(branchDropdown, { target: { value: "" } });
// Verify the branch input is cleared (no selected branch)
expect(branchDropdown).toHaveValue("");
// Trigger a re-render by changing something else
fireEvent.change(repoDropdown, { target: { value: "test/repo2" } });
fireEvent.change(repoDropdown, { target: { value: "test/repo1" } });
// The branch should be auto-selected to "main" again after repo change
expect(branchDropdown).toHaveValue("main");
// Clear it again
fireEvent.change(branchDropdown, { target: { value: "" } });
// Verify it stays empty
expect(branchDropdown).toHaveValue("");
// Simulate a component update without changing repos
// This would normally trigger the useEffect if our fix wasn't working
fireEvent.blur(branchDropdown);
// Verify it still stays empty
expect(branchDropdown).toHaveValue("");
});
});
});
@@ -266,6 +266,5 @@ class CodeActAgent(Agent):
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
return codeact_function_calling.response_to_actions(
response,
mcp_tool_names=list(self.mcp_tools.keys()),
response, mcp_tool_names=list(self.mcp_tools.keys()),
)
@@ -5,13 +5,14 @@ This is similar to the functionality of `CodeActResponseParser`.
import json
from litellm import (
ChatCompletionToolParam,
ModelResponse,
)
from openhands.agenthub.codeact_agent.function_calling import combine_thought
from openhands.agenthub.codeact_agent.tools import FinishTool
from openhands.agenthub.codeact_agent.function_calling import combine_thought
from openhands.agenthub.loc_agent.tools import (
SearchEntityTool,
SearchRepoTool,
@@ -31,8 +32,7 @@ from openhands.events.tool import ToolCallMetadata
def response_to_actions(
response: ModelResponse,
mcp_tool_names: list[str] | None = None,
response: ModelResponse, mcp_tool_names: list[str] | None = None,
) -> list[Action]:
actions: list[Action] = []
assert len(response.choices) == 1, 'Only one choice is supported for now'
@@ -87,7 +87,7 @@ def response_to_actions(
raise FunctionCallNotExistsError(
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
)
# We only add thought to the first action
if i == 0:
action = combine_thought(action, thought)
@@ -106,7 +106,7 @@ def response_to_actions(
wait_for_response=True,
)
)
# Add response id to actions
# This will ensure we can match both actions without tool calls (e.g. MessageAction)
# and actions with tool calls (e.g. CmdRunAction, IPythonRunCellAction, etc.)
@@ -116,7 +116,7 @@ def response_to_actions(
assert len(actions) >= 1
return actions
def get_tools() -> list[ChatCompletionToolParam]:
tools = [FinishTool]
+5 -5
View File
@@ -1,12 +1,13 @@
from typing import TYPE_CHECKING
import openhands.agenthub.loc_agent.function_calling as locagent_function_calling
from openhands.agenthub.codeact_agent import CodeActAgent
import openhands.agenthub.loc_agent.function_calling as locagent_function_calling
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.events.action import Action
from openhands.llm.llm import ModelResponse
@@ -34,6 +35,5 @@ class LocAgent(CodeActAgent):
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
return locagent_function_calling.response_to_actions(
response,
mcp_tool_names=list(self.mcp_tools.keys()),
response, mcp_tool_names=list(self.mcp_tools.keys()),
)
+15 -15
View File
@@ -25,7 +25,7 @@ from openhands.cli.utils import (
write_to_file,
)
from openhands.core.config import (
OpenHandsConfig,
AppConfig,
)
from openhands.core.schema import AgentState
from openhands.events import EventSource
@@ -41,8 +41,8 @@ async def handle_commands(
command: str,
event_stream: EventStream,
usage_metrics: UsageMetrics,
conversation_id: str,
config: OpenHandsConfig,
sid: str,
config: AppConfig,
current_dir: str,
settings_store: FileSettingsStore,
) -> tuple[bool, bool, bool]:
@@ -54,7 +54,7 @@ async def handle_commands(
close_repl = handle_exit_command(
event_stream,
usage_metrics,
conversation_id,
sid,
)
elif command == '/help':
handle_help_command()
@@ -63,10 +63,10 @@ async def handle_commands(
config, event_stream, current_dir
)
elif command == '/status':
handle_status_command(usage_metrics, conversation_id)
handle_status_command(usage_metrics, sid)
elif command == '/new':
close_repl, new_session_requested = handle_new_command(
event_stream, usage_metrics, conversation_id
event_stream, usage_metrics, sid
)
elif command == '/settings':
await handle_settings_command(config, settings_store)
@@ -81,7 +81,7 @@ async def handle_commands(
def handle_exit_command(
event_stream: EventStream, usage_metrics: UsageMetrics, conversation_id: str
event_stream: EventStream, usage_metrics: UsageMetrics, sid: str
) -> bool:
close_repl = False
@@ -94,7 +94,7 @@ def handle_exit_command(
ChangeAgentStateAction(AgentState.STOPPED),
EventSource.ENVIRONMENT,
)
display_shutdown_message(usage_metrics, conversation_id)
display_shutdown_message(usage_metrics, sid)
close_repl = True
return close_repl
@@ -105,7 +105,7 @@ def handle_help_command() -> None:
async def handle_init_command(
config: OpenHandsConfig, event_stream: EventStream, current_dir: str
config: AppConfig, 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:
@@ -135,12 +135,12 @@ async def handle_init_command(
return close_repl, reload_microagents
def handle_status_command(usage_metrics: UsageMetrics, conversation_id: str) -> None:
display_status(usage_metrics, conversation_id)
def handle_status_command(usage_metrics: UsageMetrics, sid: str) -> None:
display_status(usage_metrics, sid)
def handle_new_command(
event_stream: EventStream, usage_metrics: UsageMetrics, conversation_id: str
event_stream: EventStream, usage_metrics: UsageMetrics, sid: str
) -> tuple[bool, bool]:
close_repl = False
new_session_requested = False
@@ -160,13 +160,13 @@ def handle_new_command(
ChangeAgentStateAction(AgentState.STOPPED),
EventSource.ENVIRONMENT,
)
display_shutdown_message(usage_metrics, conversation_id)
display_shutdown_message(usage_metrics, sid)
return close_repl, new_session_requested
async def handle_settings_command(
config: OpenHandsConfig,
config: AppConfig,
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: OpenHandsConfig, current_dir: str) -> bool:
def check_folder_security_agreement(config: AppConfig, current_dir: str) -> bool:
# Directories trusted by user for the CLI to use as workspace
# Config from ~/.openhands/config.toml overrides the app config
+12 -12
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 (
OpenHandsConfig,
AppConfig,
parse_arguments,
setup_config_from_args,
)
@@ -44,7 +44,7 @@ from openhands.core.setup import (
create_controller,
create_memory,
create_runtime,
generate_conversation_id,
generate_sid,
initialize_repository_for_runtime,
)
from openhands.events import EventSource, EventStreamSubscriber
@@ -77,7 +77,7 @@ async def cleanup_session(
event_stream = runtime.event_stream
end_state = controller.get_state()
end_state.save_to_session(
event_stream.conversation_id,
event_stream.sid,
event_stream.file_store,
event_stream.user_id,
)
@@ -103,7 +103,7 @@ async def cleanup_session(
async def run_session(
loop: asyncio.AbstractEventLoop,
config: OpenHandsConfig,
config: AppConfig,
settings_store: FileSettingsStore,
current_dir: str,
task_content: str | None = None,
@@ -113,7 +113,7 @@ async def run_session(
reload_microagents = False
new_session_requested = False
conversation_id = generate_conversation_id(config, session_name)
sid = generate_sid(config, session_name)
is_loaded = asyncio.Event()
is_paused = asyncio.Event() # Event to track agent pause requests
always_confirm_mode = False # Flag to enable always confirm mode
@@ -129,7 +129,7 @@ async def run_session(
agent = create_agent(config)
runtime = create_runtime(
config,
conversation_id=conversation_id,
sid=sid,
headless_mode=True,
agent=agent,
)
@@ -164,7 +164,7 @@ async def run_session(
next_message,
event_stream,
usage_metrics,
conversation_id,
sid,
config,
current_dir,
settings_store,
@@ -238,7 +238,7 @@ async def run_session(
def on_event(event: Event) -> None:
loop.create_task(on_event_async(event))
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, conversation_id)
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, sid)
await runtime.connect()
@@ -254,7 +254,7 @@ async def run_session(
memory = create_memory(
runtime=runtime,
event_stream=event_stream,
conversation_id=conversation_id,
sid=sid,
selected_repository=config.sandbox.selected_repo,
repo_directory=repo_directory,
conversation_instructions=conversation_instructions,
@@ -282,7 +282,7 @@ async def run_session(
clear()
# Show OpenHands banner and session ID
display_banner(session_id=conversation_id)
display_banner(session_id=sid)
welcome_message = 'What do you want to build?' # from the application
initial_message = '' # from the user
@@ -292,7 +292,7 @@ async def run_session(
# If we loaded a state, we are resuming a previous session
if initial_state is not None:
logger.info(f'Resuming session: {conversation_id}')
logger.info(f'Resuming session: {sid}')
if initial_state.last_error:
# If the last session ended in an error, provide a message.
@@ -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: OpenHandsConfig = setup_config_from_args(args)
config: AppConfig = setup_config_from_args(args)
# Load settings from Settings Store
# TODO: Make this generic?
+4 -4
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 OpenHandsConfig
from openhands.core.config import AppConfig
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: OpenHandsConfig) -> None:
def display_settings(config: AppConfig) -> 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: OpenHandsConfig, settings_store: FileSettingsStore
config: AppConfig, 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: OpenHandsConfig, settings_store: FileSettingsStore
config: AppConfig, settings_store: FileSettingsStore
) -> None:
session = PromptSession(key_bindings=kb_cancel())
+2 -2
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 OpenHandsConfig
from openhands.core.config import AppConfig
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: OpenHandsConfig) -> None:
def display_event(event: Event, config: AppConfig) -> None:
global streaming_output_text_area
with print_lock:
if isinstance(event, Action):
+4 -4
View File
@@ -109,7 +109,7 @@ class AgentController:
max_budget_per_task: float | None = None,
agent_to_llm_config: dict[str, LLMConfig] | None = None,
agent_configs: dict[str, AgentConfig] | None = None,
conversation_id: str | None = None,
sid: str | None = None,
confirmation_mode: bool = False,
initial_state: State | None = None,
is_delegate: bool = False,
@@ -128,7 +128,7 @@ class AgentController:
we delegate to a different agent.
agent_configs: A dictionary mapping agent names to agent configurations in the case that
we delegate to a different agent.
conversation_id: The session ID of the agent.
sid: The session ID of the agent.
confirmation_mode: Whether to enable confirmation mode for agent actions.
initial_state: The initial state of the controller.
is_delegate: Whether this controller is a delegate.
@@ -136,7 +136,7 @@ class AgentController:
status_callback: Optional callback function to handle status updates.
replay_events: A list of logs to replay.
"""
self.id = conversation_id or event_stream.conversation_id
self.id = sid or event_stream.sid
self.agent = agent
self.headless_mode = headless_mode
self.is_delegate = is_delegate
@@ -700,7 +700,7 @@ class AgentController:
# Create the delegate with is_delegate=True so it does NOT subscribe directly
self.delegate = AgentController(
conversation_id=self.id + '-delegate',
sid=self.id + '-delegate',
agent=delegate_agent,
event_stream=self.event_stream,
max_iterations=self.state.max_iterations,
+8 -8
View File
@@ -105,19 +105,19 @@ class State:
last_error: str = ''
def save_to_session(
self, conversation_id: str, file_store: FileStore, user_id: str | None
self, sid: str, file_store: FileStore, user_id: str | None
) -> None:
pickled = pickle.dumps(self)
logger.debug(f'Saving state to session {conversation_id}:{self.agent_state}')
logger.debug(f'Saving state to session {sid}:{self.agent_state}')
encoded = base64.b64encode(pickled).decode('utf-8')
try:
file_store.write(
get_conversation_agent_state_filename(conversation_id, user_id), encoded
get_conversation_agent_state_filename(sid, user_id), encoded
)
# see if state is in the old directory on saas/remote use cases and delete it.
if user_id:
filename = get_conversation_agent_state_filename(conversation_id)
filename = get_conversation_agent_state_filename(sid)
try:
file_store.delete(filename)
except Exception:
@@ -128,7 +128,7 @@ class State:
@staticmethod
def restore_from_session(
conversation_id: str, file_store: FileStore, user_id: str | None = None
sid: str, file_store: FileStore, user_id: str | None = None
) -> 'State':
"""
Restores the state from the previously saved session.
@@ -137,7 +137,7 @@ class State:
state: State
try:
encoded = file_store.read(
get_conversation_agent_state_filename(conversation_id, user_id)
get_conversation_agent_state_filename(sid, user_id)
)
pickled = base64.b64decode(encoded)
state = pickle.loads(pickled)
@@ -145,13 +145,13 @@ class State:
# if user_id is provided, we are in a saas/remote use case
# and we need to check if the state is in the old directory.
if user_id:
filename = get_conversation_agent_state_filename(conversation_id)
filename = get_conversation_agent_state_filename(sid)
encoded = file_store.read(filename)
pickled = base64.b64decode(encoded)
state = pickle.loads(pickled)
else:
raise FileNotFoundError(
f'Could not restore state from session file for conversation_id: {conversation_id}'
f'Could not restore state from session file for sid: {sid}'
)
except Exception as e:
logger.debug(f'Could not restore state from session: {e}')
+4 -4
View File
@@ -1,4 +1,5 @@
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,
@@ -7,7 +8,6 @@ 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',
'OpenHandsConfig',
'AppConfig',
'MCPConfig',
'LLMConfig',
'SandboxConfig',
'SecurityConfig',
'ExtendedConfig',
'load_openhands_config',
'load_app_config',
'load_from_env',
'load_from_toml',
'finalize_config',
@@ -16,7 +16,7 @@ from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
class OpenHandsConfig(BaseModel):
class AppConfig(BaseModel):
"""Configuration for the app.
Attributes:
@@ -65,10 +65,7 @@ class OpenHandsConfig(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)
@@ -76,7 +73,7 @@ class OpenHandsConfig(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)
@@ -151,5 +148,5 @@ class OpenHandsConfig(BaseModel):
"""Post-initialization hook, called when the instance is created with only default values."""
super().model_post_init(__context)
if not OpenHandsConfig.defaults_dict: # Only set defaults_dict if it's empty
OpenHandsConfig.defaults_dict = model_defaults_to_dict(self)
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
AppConfig.defaults_dict = model_defaults_to_dict(self)
+6 -6
View File
@@ -1,11 +1,10 @@
import os
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from typing import TYPE_CHECKING
from pydantic import BaseModel, Field, ValidationError, model_validator
if TYPE_CHECKING:
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.config.app_config import AppConfig
from openhands.core.logger import openhands_logger as logger
from openhands.utils.import_utils import get_impl
@@ -148,7 +147,7 @@ class MCPConfig(BaseModel):
class OpenHandsMCPConfig:
@staticmethod
def add_search_engine(app_config: 'OpenHandsConfig') -> MCPStdioServerConfig | None:
def add_search_engine(app_config: "AppConfig") -> MCPStdioServerConfig | None:
"""Add search engine to the MCP config"""
if (
app_config.search_api_key
@@ -166,16 +165,17 @@ 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: 'OpenHandsConfig', user_id: str | None = None
host: str, config: "AppConfig", user_id: str | None = None
) -> tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]:
"""
Create a default MCP server configuration.
Args:
host: Host string
config: OpenHandsConfig
config: AppConfig
Returns:
tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]: A tuple containing the default SSE server configuration and a list of MCP stdio server configurations
"""
+12 -12
View File
@@ -15,6 +15,7 @@ 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,
@@ -27,7 +28,6 @@ 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: OpenHandsConfig, env_or_toml_dict: dict | MutableMapping[str, str]
cfg: AppConfig, 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 OpenHandsConfig object to set attributes on.
cfg: The AppConfig 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: OpenHandsConfig, toml_file: str = 'config.toml') -> None:
def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
"""Load the config from the toml file. Supports both styles of config vars.
Args:
cfg: The OpenHandsConfig object to update attributes of.
cfg: The AppConfig 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: OpenHandsConfig) -> None:
def finalize_config(cfg: AppConfig) -> 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: OpenHandsConfig) -> None:
def register_custom_agents(config: AppConfig) -> 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: OpenHandsConfig) -> None:
)
def load_openhands_config(
def load_app_config(
set_logging_levels: bool = True, config_file: str = 'config.toml'
) -> OpenHandsConfig:
) -> AppConfig:
"""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 = OpenHandsConfig()
config = AppConfig()
load_from_toml(config, config_file)
load_from_env(config, os.environ)
finalize_config(config)
@@ -802,13 +802,13 @@ def load_openhands_config(
return config
def setup_config_from_args(args: argparse.Namespace) -> OpenHandsConfig:
def setup_config_from_args(args: argparse.Namespace) -> AppConfig:
"""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_openhands_config(config_file=args.config_file)
config = load_app_config(config_file=args.config_file)
# Override with command line arguments if provided
if args.llm_config:
+15 -17
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 (
OpenHandsConfig,
AppConfig,
parse_arguments,
setup_config_from_args,
)
@@ -22,7 +22,7 @@ from openhands.core.setup import (
create_controller,
create_memory,
create_runtime,
generate_conversation_id,
generate_sid,
initialize_repository_for_runtime,
)
from openhands.events import EventSource, EventStreamSubscriber
@@ -47,9 +47,9 @@ class FakeUserResponseFunc(Protocol):
async def run_controller(
config: OpenHandsConfig,
config: AppConfig,
initial_user_action: Action,
conversation_id: str | None = None,
sid: str | None = None,
runtime: Runtime | None = None,
agent: Agent | None = None,
exit_on_message: bool = False,
@@ -65,7 +65,7 @@ async def run_controller(
Args:
config: The app config.
initial_user_action: An Action object containing initial user input
conversation_id: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
runtime: (optional) A runtime for the agent to run on.
agent: (optional) A agent to run.
@@ -90,11 +90,11 @@ async def run_controller(
config.max_budget_per_task.
Example:
>>> config = load_openhands_config()
>>> config = load_app_config()
>>> action = MessageAction(content="Write a hello world program")
>>> state = await run_controller(config=config, initial_user_action=action)
"""
conversation_id = conversation_id or generate_conversation_id(config)
sid = sid or generate_sid(config)
if agent is None:
agent = create_agent(config)
@@ -104,7 +104,7 @@ async def run_controller(
if runtime is None:
runtime = create_runtime(
config,
conversation_id=conversation_id,
sid=sid,
headless_mode=headless_mode,
agent=agent,
)
@@ -125,7 +125,7 @@ async def run_controller(
memory = create_memory(
runtime=runtime,
event_stream=event_stream,
conversation_id=conversation_id,
sid=sid,
selected_repository=config.sandbox.selected_repo,
repo_directory=repo_directory,
conversation_instructions=conversation_instructions,
@@ -194,7 +194,7 @@ async def run_controller(
action = MessageAction(content=message)
event_stream.add_event(action, EventSource.USER)
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, conversation_id)
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, sid)
end_states = [
AgentState.FINISHED,
@@ -214,7 +214,7 @@ async def run_controller(
end_state = controller.get_state()
# NOTE: the saved state does not include delegates events
end_state.save_to_session(
event_stream.conversation_id, event_stream.file_store, event_stream.user_id
event_stream.sid, event_stream.file_store, event_stream.user_id
)
await controller.close(set_stop_state=False)
@@ -225,9 +225,7 @@ async def run_controller(
if config.save_trajectory_path is not None:
# if save_trajectory_path is a folder, use session id as file name
if os.path.isdir(config.save_trajectory_path):
file_path = os.path.join(
config.save_trajectory_path, conversation_id + '.json'
)
file_path = os.path.join(config.save_trajectory_path, sid + '.json')
else:
file_path = config.save_trajectory_path
os.makedirs(os.path.dirname(file_path), exist_ok=True)
@@ -281,7 +279,7 @@ def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]:
if __name__ == '__main__':
args = parse_arguments()
config: OpenHandsConfig = setup_config_from_args(args)
config: AppConfig = setup_config_from_args(args)
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
@@ -301,13 +299,13 @@ if __name__ == '__main__':
# Set session name
session_name = args.name
conversation_id = generate_conversation_id(config, session_name)
sid = generate_sid(config, session_name)
asyncio.run(
run_controller(
config=config,
initial_user_action=initial_user_action,
conversation_id=conversation_id,
sid=sid,
fake_user_response_fn=None
if args.no_auto_continue
else auto_continue_response,
+17 -19
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 (
OpenHandsConfig,
AppConfig,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
@@ -28,8 +28,8 @@ from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync
def create_runtime(
config: OpenHandsConfig,
conversation_id: str | None = None,
config: AppConfig,
sid: str | None = None,
headless_mode: bool = True,
agent: Agent | None = None,
) -> Runtime:
@@ -37,7 +37,7 @@ def create_runtime(
Args:
config: The app config.
conversation_id: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
@@ -46,10 +46,10 @@ def create_runtime(
Returns:
The created Runtime instance (not yet connected or initialized).
"""
# if conversation_id is provided on the command line, use it as the name of the event stream
# if sid is provided on the command line, use it as the name of the event stream
# otherwise generate it on the basis of the configured jwt_secret
# we can do this better, this is just so that the conversation_id is retrieved when we want to restore the session
session_id = conversation_id or generate_conversation_id(config)
# we can do this better, this is just so that the sid is retrieved when we want to restore the session
session_id = sid or generate_sid(config)
# set up the event stream
file_store = get_file_store(config.file_store, config.file_store_path)
@@ -73,7 +73,7 @@ def create_runtime(
runtime: Runtime = runtime_cls(
config=config,
event_stream=event_stream,
conversation_id=session_id,
sid=session_id,
plugins=agent_cls.sandbox_plugins,
headless_mode=headless_mode,
)
@@ -131,7 +131,7 @@ def initialize_repository_for_runtime(
def create_memory(
runtime: Runtime,
event_stream: EventStream,
conversation_id: str,
sid: str,
selected_repository: str | None = None,
repo_directory: str | None = None,
status_callback: Callable | None = None,
@@ -142,7 +142,7 @@ def create_memory(
Args:
runtime: The runtime to use.
event_stream: The event stream it will subscribe to.
conversation_id: The session id.
sid: The session id.
selected_repository: The repository to clone and start with, if any.
repo_directory: The repository directory, if any.
status_callback: Optional callback function to handle status updates.
@@ -150,7 +150,7 @@ def create_memory(
"""
memory = Memory(
event_stream=event_stream,
conversation_id=conversation_id,
sid=sid,
status_callback=status_callback,
)
@@ -172,7 +172,7 @@ def create_memory(
return memory
def create_agent(config: OpenHandsConfig) -> Agent:
def create_agent(config: AppConfig) -> 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: OpenHandsConfig) -> Agent:
def create_controller(
agent: Agent,
runtime: Runtime,
config: OpenHandsConfig,
config: AppConfig,
headless_mode: bool = True,
replay_events: list[Event] | None = None,
) -> tuple[AgentController, State | None]:
@@ -196,10 +196,10 @@ def create_controller(
initial_state = None
try:
logger.debug(
f'Trying to restore agent state from session {event_stream.conversation_id} if available'
f'Trying to restore agent state from session {event_stream.sid} if available'
)
initial_state = State.restore_from_session(
event_stream.conversation_id, event_stream.file_store
event_stream.sid, event_stream.file_store
)
except Exception as e:
logger.debug(f'Cannot restore agent state: {e}')
@@ -218,10 +218,8 @@ def create_controller(
return (controller, initial_state)
def generate_conversation_id(
config: OpenHandsConfig, session_name: str | None = None
) -> str:
"""Generate a conversation id based on the session name and the jwt secret."""
def generate_sid(config: AppConfig, 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
+5 -7
View File
@@ -46,7 +46,7 @@ class EventStore(EventStoreABC):
A stored list of events backing a conversation
"""
conversation_id: str
sid: str
file_store: FileStore
user_id: str | None
cur_id: int = -1 # We fix this in post init if it is not specified
@@ -57,12 +57,10 @@ class EventStore(EventStoreABC):
return
events = []
try:
events_dir = get_conversation_events_dir(self.conversation_id, self.user_id)
events_dir = get_conversation_events_dir(self.sid, self.user_id)
events = self.file_store.list(events_dir)
except FileNotFoundError:
logger.debug(
f'No events found for session {self.conversation_id} at {events_dir}'
)
logger.debug(f'No events found for session {self.sid} at {events_dir}')
if not events:
self.cur_id = 0
@@ -147,10 +145,10 @@ class EventStore(EventStoreABC):
yield event
def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
return get_conversation_event_filename(self.conversation_id, id, user_id)
return get_conversation_event_filename(self.sid, id, user_id)
def _get_filename_for_cache(self, start: int, end: int) -> str:
return f'{get_conversation_dir(self.conversation_id, self.user_id)}event_cache/{start}-{end}.json'
return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
def _load_cache_page(self, start: int, end: int) -> _CachePage:
"""Read a page from the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
+1 -1
View File
@@ -13,7 +13,7 @@ class EventStoreABC:
A stored list of events backing a conversation
"""
conversation_id: str
sid: str
user_id: str | None
@abstractmethod
+1 -1
View File
@@ -17,7 +17,7 @@ class NestedEventStore(EventStoreABC):
"""
base_url: str
conversation_id: str
sid: str
user_id: str | None
def search_events(
+1 -3
View File
@@ -11,9 +11,7 @@ class MCPObservation(Observation):
observation: str = ObservationType.MCP
name: str = '' # The name of the MCP tool that was called
arguments: dict[str, Any] = field(
default_factory=dict
) # The arguments passed to the MCP tool
arguments: dict[str, Any] = field(default_factory=dict) # The arguments passed to the MCP tool
@property
def message(self) -> str:
+4 -8
View File
@@ -32,12 +32,10 @@ class EventStreamSubscriber(str, Enum):
async def session_exists(
conversation_id: str, file_store: FileStore, user_id: str | None = None
sid: str, file_store: FileStore, user_id: str | None = None
) -> bool:
try:
await call_sync_from_async(
file_store.list, get_conversation_dir(conversation_id, user_id)
)
await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
return True
except FileNotFoundError:
return False
@@ -56,10 +54,8 @@ class EventStream(EventStore):
_thread_loops: dict[str, dict[str, asyncio.AbstractEventLoop]]
_write_page_cache: list[dict]
def __init__(
self, conversation_id: str, file_store: FileStore, user_id: str | None = None
):
super().__init__(conversation_id, file_store, user_id)
def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
super().__init__(sid, file_store, user_id)
self._stop_flag = threading.Event()
self._queue: queue.Queue[Event] = queue.Queue()
self._thread_pools = {}
@@ -1 +1 @@
{{ issue_comment }}
{{ issue_comment }}
@@ -1 +1 @@
Please fix issue number #{{ issue_number }} in your repository.
Please fix issue number #{{ issue_number }} in your repository.
@@ -1 +1 @@
{{ pr_comment }}
{{ pr_comment }}
+2 -2
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: OpenHandsConfig
agent: 'Agent', runtime: Runtime, memory: 'Memory', app_config: AppConfig
):
"""
Add MCP tools to an agent.
+4 -4
View File
@@ -40,7 +40,7 @@ class Memory:
(a RecallAction) and publishes observations with the content (such as RecallObservation).
"""
conversation_id: str
sid: str
event_stream: EventStream
status_callback: Callable | None
loop: asyncio.AbstractEventLoop | None
@@ -48,18 +48,18 @@ class Memory:
def __init__(
self,
event_stream: EventStream,
conversation_id: str,
sid: str,
status_callback: Callable | None = None,
):
self.event_stream = event_stream
self.conversation_id = conversation_id if conversation_id else str(uuid.uuid4())
self.sid = sid if sid else str(uuid.uuid4())
self.status_callback = status_callback
self.loop = None
self.event_stream.subscribe(
EventStreamSubscriber.MEMORY,
self.on_event,
self.conversation_id,
self.sid,
)
# Additional placeholders to store user workspace microagents
+2 -2
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, LLMConfig, OpenHandsConfig, SandboxConfig
from openhands.core.config import AgentConfig, AppConfig, LLMConfig, 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 = OpenHandsConfig(
config = AppConfig(
default_agent='CodeActAgent',
runtime='docker',
max_budget_per_task=4,
@@ -4,4 +4,4 @@ You SHOULD INCLUDE PROPER INDENTATION in your edit commands.{% if repo_instructi
Some basic information about this repository:
{{ repo_instruction }}{% endif %}
When you think you have fixed the issue through code changes, please finish the interaction.
When you think you have fixed the issue through code changes, please finish the interaction.
@@ -13,4 +13,4 @@ You SHOULD INCLUDE PROPER INDENTATION in your edit commands.{% if repo_instructi
Some basic information about this repository:
{{ repo_instruction }}{% endif %}
When you think you have fixed the issue through code changes, please finish the interaction.
When you think you have fixed the issue through code changes, please finish the interaction.
@@ -2,4 +2,4 @@ Please fix the following issue for the repository in /workspace.
An environment has been set up for you to start working. You may assume all necessary tools are installed.
# Problem Statement
{{ body }}
{{ body }}
@@ -2,4 +2,4 @@ Please fix the following issue for the repository in /workspace.
An environment has been set up for you to start working. You may assume all necessary tools are installed.
# Problem Statement
{{ body }}
{{ body }}
+11 -13
View File
@@ -15,7 +15,7 @@ from zipfile import ZipFile
import httpx
from openhands.core.config import OpenHandsConfig, SandboxConfig
from openhands.core.config import AppConfig, 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
@@ -93,11 +93,11 @@ class Runtime(FileEditRuntimeMixin):
"""The runtime is how the agent interacts with the external environment.
This includes a bash sandbox, a browser, and filesystem interactions.
conversation_id is the session id, which is used to identify the current user session.
sid is the session id, which is used to identify the current user session.
"""
conversation_id: str
config: OpenHandsConfig
sid: str
config: AppConfig
initial_env_vars: dict[str, str]
attach_to_existing: bool
status_callback: Callable[[str, str, str], None] | None
@@ -105,9 +105,9 @@ class Runtime(FileEditRuntimeMixin):
def __init__(
self,
config: OpenHandsConfig,
config: AppConfig,
event_stream: EventStream,
conversation_id: str = 'default',
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None,
status_callback: Callable[[str, str, str], None] | None = None,
@@ -119,11 +119,11 @@ class Runtime(FileEditRuntimeMixin):
self.git_handler = GitHandler(
execute_shell_fn=self._execute_shell_fn_git_handler
)
self.conversation_id = conversation_id
self.sid = sid
self.event_stream = event_stream
if event_stream:
event_stream.subscribe(
EventStreamSubscriber.RUNTIME, self.on_event, self.conversation_id
EventStreamSubscriber.RUNTIME, self.on_event, self.sid
)
self.plugins = (
copy.deepcopy(plugins) if plugins is not None and len(plugins) > 0 else []
@@ -189,7 +189,7 @@ class Runtime(FileEditRuntimeMixin):
pass
def log(self, level: str, message: str) -> None:
message = f'[runtime {self.conversation_id}] {message}'
message = f'[runtime {self.sid}] {message}'
getattr(logger, level)(message, stacklevel=2)
def send_status_message(self, message_id: str):
@@ -266,9 +266,7 @@ class Runtime(FileEditRuntimeMixin):
if not providers_called:
return
logger.info(
f'Fetching latest provider tokens for runtime: {self.conversation_id}'
)
logger.info(f'Fetching latest provider tokens for runtime: {self.sid}')
env_vars = await self.provider_handler.get_env_vars(
providers=providers_called, expose_secrets=False, get_latest=True
)
@@ -284,7 +282,7 @@ class Runtime(FileEditRuntimeMixin):
self.add_env_vars(self.provider_handler.expose_env_vars(env_vars))
except Exception as e:
logger.warning(
f'Failed export latest github token to runtime: {self.conversation_id}, {e}'
f'Failed export latest github token to runtime: {self.sid}, {e}'
)
async def _handle_action(self, event: Action) -> None:
+2 -5
View File
@@ -1,9 +1,7 @@
import base64
import io
import numpy as np
import base64
from PIL import Image
import numpy as np
def image_to_png_base64_url(
image: np.ndarray | Image.Image, add_data_prefix: bool = False
@@ -23,7 +21,6 @@ def image_to_png_base64_url(
else f'{image_base64}'
)
def png_base64_url_to_image(png_base64_url: str) -> Image.Image:
"""Convert a base64 encoded png image url to a PIL Image."""
splited = png_base64_url.split(',')
+1 -2
View File
@@ -12,14 +12,13 @@ from browsergym.utils.obs import flatten_dom_to_str, overlay_som
from openhands.core.exceptions import BrowserInitException
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.browser.base64 import image_to_png_base64_url
from openhands.utils.shutdown_listener import should_continue, should_exit
from openhands.utils.tenacity_stop import stop_if_should_exit
from openhands.runtime.browser.base64 import image_to_png_base64_url
BROWSER_EVAL_GET_GOAL_ACTION = 'GET_EVAL_GOAL'
BROWSER_EVAL_GET_REWARDS_ACTION = 'GET_EVAL_REWARDS'
class BrowserEnv:
def __init__(self, browsergym_eval_env: str | None = None):
self.html_text_converter = self.get_html_text_converter()
@@ -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 OpenHandsConfig
from openhands.core.config import AppConfig
from openhands.core.config.mcp_config import (
MCPConfig,
MCPSSEServerConfig,
@@ -65,9 +65,9 @@ class ActionExecutionClient(Runtime):
def __init__(
self,
config: OpenHandsConfig,
config: AppConfig,
event_stream: EventStream,
conversation_id: str = 'default',
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None,
status_callback: Any | None = None,
@@ -84,7 +84,7 @@ class ActionExecutionClient(Runtime):
super().__init__(
config,
event_stream,
conversation_id,
sid,
plugins,
env_vars,
status_callback,
@@ -409,7 +409,7 @@ class ActionExecutionClient(Runtime):
'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(
@@ -448,9 +448,7 @@ class ActionExecutionClient(Runtime):
)
# Create clients for this specific operation
mcp_clients = await create_mcp_clients(
updated_mcp_config.sse_servers, self.conversation_id
)
mcp_clients = await create_mcp_clients(updated_mcp_config.sse_servers, self.sid)
# Call the tool and return the result
# No need for try/finally since disconnect() is now just resetting state
+7 -7
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 OpenHandsConfig
from openhands.core.config import AppConfig
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,9 +57,9 @@ class CLIRuntime(Runtime):
file operations using Python's standard library. It does not implement browser functionality.
Args:
config (OpenHandsConfig): The application configuration.
config (AppConfig): The application configuration.
event_stream (EventStream): The event stream to subscribe to.
conversation_id (str, optional): The session ID. Defaults to 'default'.
sid (str, optional): The session ID. Defaults to 'default'.
plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None.
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
status_callback (Callable | None, optional): Callback for status updates. Defaults to None.
@@ -71,9 +71,9 @@ class CLIRuntime(Runtime):
def __init__(
self,
config: OpenHandsConfig,
config: AppConfig,
event_stream: EventStream,
conversation_id: str = 'default',
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None,
status_callback: Callable[[str, str, str], None] | None = None,
@@ -85,7 +85,7 @@ class CLIRuntime(Runtime):
super().__init__(
config,
event_stream,
conversation_id,
sid,
plugins,
env_vars,
status_callback,
@@ -106,7 +106,7 @@ class CLIRuntime(Runtime):
else:
# Create a temporary directory for the workspace
self._workspace_path = tempfile.mkdtemp(
prefix=f'openhands_workspace_{conversation_id}_'
prefix=f'openhands_workspace_{sid}_'
)
logger.info(f'Created temporary workspace at {self._workspace_path}')
@@ -11,7 +11,7 @@ from daytona_sdk import (
Workspace,
)
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.config.app_config import AppConfig
from openhands.events.stream import EventStream
from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
@@ -33,9 +33,9 @@ class DaytonaRuntime(ActionExecutionClient):
def __init__(
self,
config: OpenHandsConfig,
config: AppConfig,
event_stream: EventStream,
conversation_id: str = 'default',
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None,
status_callback: Callable | None = None,
@@ -45,8 +45,8 @@ class DaytonaRuntime(ActionExecutionClient):
assert config.daytona_api_key, 'Daytona API key is required'
self.config = config
self.conversation_id = conversation_id
self.workspace_id = WORKSPACE_PREFIX + conversation_id
self.sid = sid
self.workspace_id = WORKSPACE_PREFIX + sid
self.workspace: Workspace | None = None
self._vscode_url: str | None = None
@@ -67,7 +67,7 @@ class DaytonaRuntime(ActionExecutionClient):
super().__init__(
config,
event_stream,
conversation_id,
sid,
plugins,
env_vars,
status_callback,
@@ -8,7 +8,7 @@ import httpx
import tenacity
from docker.models.containers import Container
from openhands.core.config import OpenHandsConfig
from openhands.core.config import AppConfig
from openhands.core.exceptions import (
AgentRuntimeDisconnectedError,
AgentRuntimeNotFoundError,
@@ -23,10 +23,7 @@ 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
@@ -65,9 +62,9 @@ class DockerRuntime(ActionExecutionClient):
When receive an event, it will send the event to runtime-client which run inside the docker environment.
Args:
config (OpenHandsConfig): The application configuration.
config (AppConfig): The application configuration.
event_stream (EventStream): The event stream to subscribe to.
conversation_id (str, optional): The session ID. Defaults to 'default'.
sid (str, optional): The session ID. Defaults to 'default'.
plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None.
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
"""
@@ -76,9 +73,9 @@ class DockerRuntime(ActionExecutionClient):
def __init__(
self,
config: OpenHandsConfig,
config: AppConfig,
event_stream: EventStream,
conversation_id: str = 'default',
sid: str = 'default',
plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None,
status_callback: Callable | None = None,
@@ -112,7 +109,7 @@ class DockerRuntime(ActionExecutionClient):
self.base_container_image = self.config.sandbox.base_container_image
self.runtime_container_image = self.config.sandbox.runtime_container_image
self.container_name = CONTAINER_NAME_PREFIX + conversation_id
self.container_name = CONTAINER_NAME_PREFIX + sid
self.container: Container | None = None
self.main_module = main_module
@@ -124,7 +121,7 @@ class DockerRuntime(ActionExecutionClient):
super().__init__(
config,
event_stream,
conversation_id,
sid,
plugins,
env_vars,
status_callback,
@@ -132,7 +129,7 @@ class DockerRuntime(ActionExecutionClient):
headless_mode,
)
# Log runtime_extra_deps after base class initialization so self.conversation_id is available
# Log runtime_extra_deps after base class initialization so self.sid is available
if self.config.sandbox.runtime_extra_deps:
self.log(
'debug',

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