mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
5 Commits
openhands/
...
downgrade-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c95185a627 | ||
|
|
eb6eb00519 | ||
|
|
d4115859ba | ||
|
|
605f068e0e | ||
|
|
d534d6609b |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -5,7 +5,7 @@
|
||||
/frontend/ @rbren @amanape
|
||||
|
||||
# Evaluation code owners
|
||||
/evaluation/ @xingyaoww @neubig
|
||||
/evaluation/ @xingyaoww @neubig
|
||||
|
||||
# Documentation code owners
|
||||
/docs/ @mamoodi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Development Guide
|
||||
|
||||
This guide is for people working on OpenHands and editing the source code.
|
||||
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project
|
||||
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project
|
||||
initially before moving on. Otherwise, you can clone the OpenHands project directly.
|
||||
|
||||
## Start the Server for Development
|
||||
@@ -21,7 +21,7 @@ Make sure you have all these dependencies installed before moving on to `make bu
|
||||
|
||||
#### Develop without sudo access
|
||||
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
|
||||
`conda` or `mamba` to manage the packages for you:
|
||||
|
||||
```bash
|
||||
@@ -37,7 +37,7 @@ mamba install conda-forge::poetry
|
||||
|
||||
### 2. Build and Setup The Environment
|
||||
|
||||
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
|
||||
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
|
||||
that OpenHands is ready to run on your system:
|
||||
|
||||
```bash
|
||||
@@ -54,11 +54,11 @@ To configure the LM of your choice, run:
|
||||
make setup-config
|
||||
```
|
||||
|
||||
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
|
||||
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
|
||||
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
|
||||
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
|
||||
please set the model in the UI.
|
||||
|
||||
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
|
||||
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
|
||||
variables in your terminal. The final configurations are set from highest to lowest priority:
|
||||
Environment variables > config.toml variables > default variables
|
||||
|
||||
@@ -77,14 +77,14 @@ make run
|
||||
|
||||
#### Option B: Individual Server Startup
|
||||
|
||||
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
|
||||
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
|
||||
backend-related tasks or configurations.
|
||||
|
||||
```bash
|
||||
make start-backend
|
||||
```
|
||||
|
||||
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
|
||||
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
|
||||
components or interface enhancements.
|
||||
```bash
|
||||
make start-frontend
|
||||
@@ -120,7 +120,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
|
||||
### 9. Use existing Docker image
|
||||
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.39-nikolaik`
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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/<model-name>).
|
||||
|
||||
@@ -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/<model-name> 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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||

|
||||
|
||||
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,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
|
||||
|
||||
@@ -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/<model-name> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ from openhands.core.config import (
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.config.utils import get_condenser_config_arg
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.utils import get_condenser_config_arg
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.critic import AgentFinishedCritic
|
||||
@@ -721,15 +721,16 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
|
||||
# repos for the swe-bench instances:
|
||||
# ['astropy/astropy', 'django/django', 'matplotlib/matplotlib', 'mwaskom/seaborn', 'pallets/flask', 'psf/requests', 'pydata/xarray', 'pylint-dev/pylint', 'pytest-dev/pytest', 'scikit-learn/scikit-learn', 'sphinx-doc/sphinx', 'sympy/sympy']
|
||||
selected_repos = data['selected_repos']
|
||||
if isinstance(selected_repos, str): selected_repos = [selected_repos]
|
||||
if isinstance(selected_repos, str):
|
||||
selected_repos = [selected_repos]
|
||||
assert isinstance(selected_repos, list)
|
||||
logger.info(
|
||||
f'Filtering {selected_repos} tasks from "selected_repos"...'
|
||||
)
|
||||
subset = dataset[dataset["repo"].isin(selected_repos)]
|
||||
subset = dataset[dataset['repo'].isin(selected_repos)]
|
||||
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
|
||||
return subset
|
||||
|
||||
|
||||
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
|
||||
if len(skip_ids) > 0:
|
||||
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
|
||||
@@ -806,7 +807,9 @@ if __name__ == '__main__':
|
||||
else:
|
||||
# If no specific condenser config is provided via env var, default to NoOpCondenser
|
||||
condenser_config = NoOpCondenserConfig()
|
||||
logger.debug('No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.')
|
||||
logger.debug(
|
||||
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
|
||||
)
|
||||
|
||||
details = {'mode': args.mode}
|
||||
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@ class Agent(ABC):
|
||||
Args:
|
||||
- mcp_tools (list[dict]): The list of MCP tools.
|
||||
"""
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f'Setting {len(mcp_tools)} MCP tools for agent {self.name}: {[tool["function"]["name"] for tool in mcp_tools]}'
|
||||
)
|
||||
for tool in mcp_tools:
|
||||
@@ -178,6 +178,6 @@ class Agent(ABC):
|
||||
continue
|
||||
self.mcp_tools[_tool['function']['name']] = _tool
|
||||
self.tools.append(_tool)
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f'Tools updated for agent {self.name}, total {len(self.tools)}: {[tool["function"]["name"] for tool in self.tools]}'
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ class ReplayManager:
|
||||
# event, we override wait_for_response to False, as a response
|
||||
# would have been included in the next event, and we don't
|
||||
# want the user to interfere with the replay process
|
||||
logger.info(
|
||||
logger.debug(
|
||||
'Replay events contains wait_for_response message action, ignoring wait_for_response'
|
||||
)
|
||||
event.wait_for_response = False
|
||||
|
||||
@@ -218,7 +218,7 @@ async def run_controller(
|
||||
file_path = config.save_trajectory_path
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
histories = controller.get_trajectory(config.save_screenshots_in_trajectory)
|
||||
with open(file_path, 'w') as f: # noqa: ASYNC101
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(histories, f, indent=4)
|
||||
|
||||
return state
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -241,7 +241,7 @@ class IssueResolver:
|
||||
It sets up git configuration and runs the setup script if it exists.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Completion Fn')
|
||||
logger.info('BEGIN Runtime Initialization')
|
||||
logger.info('-' * 30)
|
||||
obs: Observation
|
||||
|
||||
@@ -259,9 +259,9 @@ class IssueResolver:
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
action = CmdRunAction(command='git config --global core.pager ""')
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
logger.debug(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
logger.debug(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
|
||||
raise RuntimeError(f'Failed to set git config.\n{obs}')
|
||||
|
||||
@@ -273,6 +273,10 @@ class IssueResolver:
|
||||
logger.info('Checking for .openhands/pre-commit.sh script...')
|
||||
runtime.maybe_setup_git_hooks()
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Initialization')
|
||||
logger.info('-' * 30)
|
||||
|
||||
async def complete_runtime(
|
||||
self,
|
||||
runtime: Runtime,
|
||||
@@ -285,7 +289,7 @@ class IssueResolver:
|
||||
the agent has run, modify this function.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Completion Fn')
|
||||
logger.info('BEGIN Runtime Completion')
|
||||
logger.info('-' * 30)
|
||||
obs: Observation
|
||||
|
||||
@@ -299,9 +303,9 @@ class IssueResolver:
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='git config --global core.pager ""')
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
logger.debug(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
logger.debug(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
|
||||
raise RuntimeError(f'Failed to set git config. Observation: {obs}')
|
||||
|
||||
@@ -332,7 +336,7 @@ class IssueResolver:
|
||||
action.set_hard_timeout(600 + 100 * n_retries)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
logger.debug(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
n_retries += 1
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
if obs.exit_code == 0:
|
||||
@@ -348,7 +352,7 @@ class IssueResolver:
|
||||
raise ValueError(f'Unexpected observation type: {type(obs)}')
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Completion Fn')
|
||||
logger.info('END Runtime Completion')
|
||||
logger.info('-' * 30)
|
||||
return {'git_patch': git_patch}
|
||||
|
||||
@@ -545,7 +549,7 @@ class IssueResolver:
|
||||
# checkout the repo
|
||||
repo_dir = os.path.join(self.output_dir, 'repo')
|
||||
if not os.path.exists(repo_dir):
|
||||
checkout_output = subprocess.check_output( # noqa: ASYNC101
|
||||
checkout_output = subprocess.check_output(
|
||||
[
|
||||
'git',
|
||||
'clone',
|
||||
@@ -558,7 +562,7 @@ class IssueResolver:
|
||||
|
||||
# get the commit id of current repo for reproducibility
|
||||
base_commit = (
|
||||
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo_dir) # noqa: ASYNC101
|
||||
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo_dir)
|
||||
.decode('utf-8')
|
||||
.strip()
|
||||
)
|
||||
@@ -570,7 +574,7 @@ class IssueResolver:
|
||||
repo_dir, '.openhands_instructions'
|
||||
)
|
||||
if os.path.exists(openhands_instructions_path):
|
||||
with open(openhands_instructions_path, 'r') as f: # noqa: ASYNC101
|
||||
with open(openhands_instructions_path, 'r') as f:
|
||||
self.repo_instruction = f.read()
|
||||
|
||||
# OUTPUT FILE
|
||||
@@ -579,7 +583,7 @@ class IssueResolver:
|
||||
|
||||
# Check if this issue was already processed
|
||||
if os.path.exists(output_file):
|
||||
with open(output_file, 'r') as f: # noqa: ASYNC101
|
||||
with open(output_file, 'r') as f:
|
||||
for line in f:
|
||||
data = ResolverOutput.model_validate_json(line)
|
||||
if data.issue.number == self.issue_number:
|
||||
@@ -588,7 +592,7 @@ class IssueResolver:
|
||||
)
|
||||
return
|
||||
|
||||
output_fp = open(output_file, 'a') # noqa: ASYNC101
|
||||
output_fp = open(output_file, 'a')
|
||||
|
||||
logger.info(
|
||||
f'Resolving issue {self.issue_number} with Agent {AGENT_CLASS}, model {model_name}, max iterations {self.max_iterations}.'
|
||||
@@ -607,20 +611,20 @@ class IssueResolver:
|
||||
|
||||
# Fetch the branch first to ensure it exists locally
|
||||
fetch_cmd = ['git', 'fetch', 'origin', branch_to_use]
|
||||
subprocess.check_output( # noqa: ASYNC101
|
||||
subprocess.check_output(
|
||||
fetch_cmd,
|
||||
cwd=repo_dir,
|
||||
)
|
||||
|
||||
# Checkout the branch
|
||||
checkout_cmd = ['git', 'checkout', branch_to_use]
|
||||
subprocess.check_output( # noqa: ASYNC101
|
||||
subprocess.check_output(
|
||||
checkout_cmd,
|
||||
cwd=repo_dir,
|
||||
)
|
||||
|
||||
base_commit = (
|
||||
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo_dir) # noqa: ASYNC101
|
||||
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo_dir)
|
||||
.decode('utf-8')
|
||||
.strip()
|
||||
)
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -474,7 +474,7 @@ class ActionExecutor:
|
||||
filepath = self._resolve_path(action.path, working_dir)
|
||||
try:
|
||||
if filepath.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
||||
with open(filepath, 'rb') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'rb') as file:
|
||||
image_data = file.read()
|
||||
encoded_image = base64.b64encode(image_data).decode('utf-8')
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
@@ -484,13 +484,13 @@ class ActionExecutor:
|
||||
|
||||
return FileReadObservation(path=filepath, content=encoded_image)
|
||||
elif filepath.lower().endswith('.pdf'):
|
||||
with open(filepath, 'rb') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'rb') as file:
|
||||
pdf_data = file.read()
|
||||
encoded_pdf = base64.b64encode(pdf_data).decode('utf-8')
|
||||
encoded_pdf = f'data:application/pdf;base64,{encoded_pdf}'
|
||||
return FileReadObservation(path=filepath, content=encoded_pdf)
|
||||
elif filepath.lower().endswith(('.mp4', '.webm', '.ogg')):
|
||||
with open(filepath, 'rb') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'rb') as file:
|
||||
video_data = file.read()
|
||||
encoded_video = base64.b64encode(video_data).decode('utf-8')
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
@@ -500,7 +500,7 @@ class ActionExecutor:
|
||||
|
||||
return FileReadObservation(path=filepath, content=encoded_video)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'r', encoding='utf-8') as file:
|
||||
lines = read_lines(file.readlines(), action.start, action.end)
|
||||
except FileNotFoundError:
|
||||
return ErrorObservation(
|
||||
@@ -533,7 +533,7 @@ class ActionExecutor:
|
||||
|
||||
mode = 'w' if not file_exists else 'r+'
|
||||
try:
|
||||
with open(filepath, mode, encoding='utf-8') as file: # noqa: ASYNC101
|
||||
with open(filepath, mode, encoding='utf-8') as file:
|
||||
if mode != 'w':
|
||||
all_lines = file.readlines()
|
||||
new_file = insert_lines(insert, all_lines, action.start, action.end)
|
||||
@@ -883,7 +883,7 @@ if __name__ == '__main__':
|
||||
)
|
||||
|
||||
zip_path = os.path.join(full_dest_path, file.filename)
|
||||
with open(zip_path, 'wb') as buffer: # noqa: ASYNC101
|
||||
with open(zip_path, 'wb') as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Extract the zip file
|
||||
@@ -896,7 +896,7 @@ if __name__ == '__main__':
|
||||
else:
|
||||
# For single file uploads
|
||||
file_path = os.path.join(full_dest_path, file.filename)
|
||||
with open(file_path, 'wb') as buffer: # noqa: ASYNC101
|
||||
with open(file_path, 'wb') as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
logger.debug(f'Uploaded file {file.filename} to {destination}')
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import io
|
||||
import base64
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def image_to_png_base64_url(
|
||||
image: np.ndarray | Image.Image, add_data_prefix: bool = False
|
||||
@@ -21,6 +23,7 @@ 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(',')
|
||||
|
||||
@@ -12,13 +12,14 @@ 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()
|
||||
|
||||
@@ -23,7 +23,10 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
from openhands.runtime.impl.docker.containers import stop_all_containers
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.runtime.utils.command import DEFAULT_MAIN_MODULE, get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.command import (
|
||||
DEFAULT_MAIN_MODULE,
|
||||
get_action_execution_server_startup_command,
|
||||
)
|
||||
from openhands.runtime.utils.log_streamer import LogStreamer
|
||||
from openhands.runtime.utils.runtime_build import build_runtime_image
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
@@ -24,7 +24,10 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.utils.command import DEFAULT_MAIN_MODULE, get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.command import (
|
||||
DEFAULT_MAIN_MODULE,
|
||||
get_action_execution_server_startup_command,
|
||||
)
|
||||
from openhands.runtime.utils.request import send_request
|
||||
from openhands.runtime.utils.runtime_build import build_runtime_image
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
@@ -69,7 +69,7 @@ class JupyterPlugin(Plugin):
|
||||
|
||||
# Using synchronous subprocess.Popen for Windows as asyncio.create_subprocess_shell
|
||||
# has limitations on Windows platforms
|
||||
self.gateway_process = subprocess.Popen( # type: ignore[ASYNC101] # noqa: ASYNC101
|
||||
self.gateway_process = subprocess.Popen( # type: ignore[ASYNC101]
|
||||
jupyter_launch_command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
@@ -82,19 +82,19 @@ class JupyterPlugin(Plugin):
|
||||
output = ''
|
||||
while should_continue():
|
||||
if self.gateway_process.stdout is None:
|
||||
time.sleep(1) # type: ignore[ASYNC101] # noqa: ASYNC101
|
||||
time.sleep(1) # type: ignore[ASYNC101]
|
||||
continue
|
||||
|
||||
line = self.gateway_process.stdout.readline()
|
||||
if not line:
|
||||
time.sleep(1) # type: ignore[ASYNC101] # noqa: ASYNC101
|
||||
time.sleep(1) # type: ignore[ASYNC101]
|
||||
continue
|
||||
|
||||
output += line
|
||||
if 'at' in line:
|
||||
break
|
||||
|
||||
time.sleep(1) # type: ignore[ASYNC101] # noqa: ASYNC101
|
||||
time.sleep(1) # type: ignore[ASYNC101]
|
||||
logger.debug('Waiting for jupyter kernel gateway to start...')
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -86,7 +86,7 @@ async def read_file(
|
||||
)
|
||||
|
||||
try:
|
||||
with open(whole_path, 'r', encoding='utf-8') as file: # noqa: ASYNC101
|
||||
with open(whole_path, 'r', encoding='utf-8') as file:
|
||||
lines = read_lines(file.readlines(), start, end)
|
||||
except FileNotFoundError:
|
||||
return ErrorObservation(f'File not found: {path}')
|
||||
@@ -127,7 +127,7 @@ async def write_file(
|
||||
os.makedirs(os.path.dirname(whole_path))
|
||||
mode = 'w' if not os.path.exists(whole_path) else 'r+'
|
||||
try:
|
||||
with open(whole_path, mode, encoding='utf-8') as file: # noqa: ASYNC101
|
||||
with open(whole_path, mode, encoding='utf-8') as file:
|
||||
if mode != 'w':
|
||||
all_lines = file.readlines()
|
||||
new_file = insert_lines(insert, all_lines, start, end)
|
||||
|
||||
@@ -22,7 +22,7 @@ class ServerConfig(ServerConfigInterface):
|
||||
'openhands.storage.conversation.file_conversation_store.FileConversationStore'
|
||||
)
|
||||
conversation_manager_class: str = os.environ.get(
|
||||
"CONVERSATION_MANAGER_CLASS",
|
||||
'CONVERSATION_MANAGER_CLASS',
|
||||
'openhands.server.conversation_manager.standalone_conversation_manager.StandaloneConversationManager',
|
||||
)
|
||||
monitoring_listener_class: str = 'openhands.server.monitoring.MonitoringListener'
|
||||
|
||||
@@ -6,10 +6,8 @@ import socketio
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.event_store import EventStore
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
|
||||
from openhands.server.data_models.conversation_info import ConversationInfo
|
||||
from openhands.server.monitoring import MonitoringListener
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
|
||||
@@ -475,7 +475,7 @@ class StandaloneConversationManager(ConversationManager):
|
||||
continue
|
||||
results.append(self._agent_loop_info_from_session(session))
|
||||
return results
|
||||
|
||||
|
||||
def _agent_loop_info_from_session(self, session: Session):
|
||||
return AgentLoopInfo(
|
||||
conversation_id=session.sid,
|
||||
@@ -485,7 +485,7 @@ class StandaloneConversationManager(ConversationManager):
|
||||
)
|
||||
|
||||
def _get_conversation_url(self, conversation_id: str):
|
||||
return f"/api/conversations/{conversation_id}"
|
||||
return f'/api/conversations/{conversation_id}'
|
||||
|
||||
|
||||
def _last_updated_at_key(conversation: ConversationMetadata) -> float:
|
||||
|
||||
@@ -9,6 +9,7 @@ class AgentLoopInfo:
|
||||
"""
|
||||
Information about an agent loop - the URL on which to locate it and the event store
|
||||
"""
|
||||
|
||||
conversation_id: str
|
||||
url: str | None
|
||||
session_api_key: str | None
|
||||
|
||||
@@ -103,7 +103,7 @@ async def search_events(
|
||||
end_id: int | None = None,
|
||||
reverse: bool = False,
|
||||
filter: EventFilter | None = None,
|
||||
limit: int = 20
|
||||
limit: int = 20,
|
||||
):
|
||||
"""Search through the event stream with filtering and pagination.
|
||||
Args:
|
||||
@@ -129,16 +129,18 @@ async def search_events(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid limit'
|
||||
)
|
||||
|
||||
|
||||
# Get matching events from the stream
|
||||
event_stream = request.state.conversation.event_stream
|
||||
events = list(event_stream.search_events(
|
||||
start_id=start_id,
|
||||
end_id=end_id,
|
||||
reverse=reverse,
|
||||
filter=filter,
|
||||
limit=limit + 1,
|
||||
))
|
||||
events = list(
|
||||
event_stream.search_events(
|
||||
start_id=start_id,
|
||||
end_id=end_id,
|
||||
reverse=reverse,
|
||||
filter=filter,
|
||||
limit=limit + 1,
|
||||
)
|
||||
)
|
||||
|
||||
# Check if there are more events
|
||||
has_more = len(events) > limit
|
||||
@@ -156,4 +158,4 @@ async def search_events(
|
||||
async def add_event(request: Request):
|
||||
data = request.json()
|
||||
conversation_manager.send_to_event_stream(request.state.sid, data)
|
||||
return JSONResponse({"success": True})
|
||||
return JSONResponse({'success': True})
|
||||
|
||||
@@ -23,20 +23,16 @@ from openhands.events.observation import (
|
||||
FileReadObservation,
|
||||
)
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.server.data_models.conversation_info import ConversationInfo
|
||||
from openhands.server.file_config import (
|
||||
FILES_TO_IGNORE,
|
||||
)
|
||||
from openhands.server.shared import (
|
||||
ConversationStoreImpl,
|
||||
config,
|
||||
conversation_manager,
|
||||
)
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.server.utils import get_conversation_store
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.storage.data_models.conversation_status import ConversationStatus
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
app = APIRouter(prefix='/api/conversations/{conversation_id}')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
from openhands.runtime.utils.system_stats import get_system_stats
|
||||
@@ -6,17 +7,16 @@ from openhands.runtime.utils.system_stats import get_system_stats
|
||||
start_time = time.time()
|
||||
last_execution_time = start_time
|
||||
|
||||
|
||||
def add_health_endpoints(app: FastAPI):
|
||||
@app.get('/alive')
|
||||
async def alive():
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
@app.get('/health')
|
||||
async def health() -> str:
|
||||
return 'OK'
|
||||
|
||||
|
||||
@app.get('/server_info')
|
||||
async def get_server_info():
|
||||
current_time = time.time()
|
||||
@@ -29,9 +29,8 @@ def add_health_endpoints(app: FastAPI):
|
||||
'resources': get_system_stats(),
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@app.middleware('http')
|
||||
async def update_last_execution_time(request: Request, call_next):
|
||||
global last_execution_time
|
||||
response = await call_next(request)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -106,8 +105,10 @@ async def new_conversation(
|
||||
if auth_type == AuthType.BEARER:
|
||||
conversation_trigger = ConversationTrigger.REMOTE_API_KEY
|
||||
|
||||
|
||||
if conversation_trigger == ConversationTrigger.REMOTE_API_KEY and not initial_user_msg:
|
||||
if (
|
||||
conversation_trigger == ConversationTrigger.REMOTE_API_KEY
|
||||
and not initial_user_msg
|
||||
):
|
||||
return JSONResponse(
|
||||
content={
|
||||
'status': 'error',
|
||||
@@ -196,19 +197,27 @@ async def search_conversations(
|
||||
conversation_ids = set(
|
||||
conversation.conversation_id for conversation in filtered_results
|
||||
)
|
||||
connection_ids_to_conversation_ids = await conversation_manager.get_connections(filter_to_sids=conversation_ids)
|
||||
agent_loop_info = await conversation_manager.get_agent_loop_info(filter_to_sids=conversation_ids)
|
||||
agent_loop_info_by_conversation_id = {info.conversation_id: info for info in agent_loop_info}
|
||||
connection_ids_to_conversation_ids = await conversation_manager.get_connections(
|
||||
filter_to_sids=conversation_ids
|
||||
)
|
||||
agent_loop_info = await conversation_manager.get_agent_loop_info(
|
||||
filter_to_sids=conversation_ids
|
||||
)
|
||||
agent_loop_info_by_conversation_id = {
|
||||
info.conversation_id: info for info in agent_loop_info
|
||||
}
|
||||
result = ConversationInfoResultSet(
|
||||
results=await wait_all(
|
||||
_get_conversation_info(
|
||||
conversation=conversation,
|
||||
num_connections=sum(
|
||||
1 for conversation_id in connection_ids_to_conversation_ids.values()
|
||||
1
|
||||
for conversation_id in connection_ids_to_conversation_ids.values()
|
||||
if conversation_id == conversation.conversation_id
|
||||
),
|
||||
agent_loop_info=agent_loop_info_by_conversation_id.get(conversation.conversation_id),
|
||||
|
||||
agent_loop_info=agent_loop_info_by_conversation_id.get(
|
||||
conversation.conversation_id
|
||||
),
|
||||
)
|
||||
for conversation in filtered_results
|
||||
),
|
||||
@@ -224,10 +233,16 @@ async def get_conversation(
|
||||
) -> ConversationInfo | None:
|
||||
try:
|
||||
metadata = await conversation_store.get_metadata(conversation_id)
|
||||
num_connections = len(await conversation_manager.get_connections(filter_to_sids={conversation_id}))
|
||||
agent_loop_infos = await conversation_manager.get_agent_loop_info(filter_to_sids={conversation_id})
|
||||
num_connections = len(
|
||||
await conversation_manager.get_connections(filter_to_sids={conversation_id})
|
||||
)
|
||||
agent_loop_infos = await conversation_manager.get_agent_loop_info(
|
||||
filter_to_sids={conversation_id}
|
||||
)
|
||||
agent_loop_info = agent_loop_infos[0] if agent_loop_infos else None
|
||||
conversation_info = await _get_conversation_info(metadata, num_connections, agent_loop_info)
|
||||
conversation_info = await _get_conversation_info(
|
||||
metadata, num_connections, agent_loop_info
|
||||
)
|
||||
return conversation_info
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
@@ -269,11 +284,15 @@ async def _get_conversation_info(
|
||||
created_at=conversation.created_at,
|
||||
selected_repository=conversation.selected_repository,
|
||||
status=(
|
||||
agent_loop_info.status if agent_loop_info else ConversationStatus.STOPPED
|
||||
agent_loop_info.status
|
||||
if agent_loop_info
|
||||
else ConversationStatus.STOPPED
|
||||
),
|
||||
num_connections=num_connections,
|
||||
url=agent_loop_info.url if agent_loop_info else None,
|
||||
session_api_key=agent_loop_info.session_api_key if agent_loop_info else None,
|
||||
session_api_key=agent_loop_info.session_api_key
|
||||
if agent_loop_info
|
||||
else None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import re
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
from pydantic import Field
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.integrations.provider import ProviderToken
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.server.shared import ConversationStoreImpl, config
|
||||
from openhands.server.user_auth import get_access_token, get_provider_tokens, get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import (
|
||||
get_access_token,
|
||||
get_provider_tokens,
|
||||
get_user_id,
|
||||
)
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
|
||||
mcp_server = FastMCP('mcp')
|
||||
|
||||
|
||||
async def save_pr_metadata(user_id: str, conversation_id: str, tool_result: str) -> None:
|
||||
async def save_pr_metadata(
|
||||
user_id: str, conversation_id: str, tool_result: str
|
||||
) -> None:
|
||||
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
|
||||
conversation: ConversationMetadata = await conversation_store.get_metadata(conversation_id)
|
||||
conversation: ConversationMetadata = await conversation_store.get_metadata(
|
||||
conversation_id
|
||||
)
|
||||
|
||||
|
||||
pull_pattern = r"pull/(\d+)"
|
||||
merge_request_pattern = r"merge_requests/(\d+)"
|
||||
pull_pattern = r'pull/(\d+)'
|
||||
merge_request_pattern = r'merge_requests/(\d+)'
|
||||
|
||||
# Check if the tool_result contains the PR number
|
||||
pr_number = None
|
||||
@@ -33,11 +42,11 @@ async def save_pr_metadata(user_id: str, conversation_id: str, tool_result: str)
|
||||
elif match_merge_request:
|
||||
pr_number = int(match_merge_request.group(1))
|
||||
|
||||
|
||||
if pr_number:
|
||||
conversation.pr_number.append(pr_number)
|
||||
await conversation_store.save_metadata(conversation)
|
||||
|
||||
|
||||
@mcp_server.tool()
|
||||
async def create_pr(
|
||||
repo_name: Annotated[
|
||||
@@ -46,7 +55,7 @@ async def create_pr(
|
||||
source_branch: Annotated[str, Field(description='Source branch on repo')],
|
||||
target_branch: Annotated[str, Field(description='Target branch on repo')],
|
||||
title: Annotated[str, Field(description='PR Title')],
|
||||
body: Annotated[str | None, Field(description='PR body')]
|
||||
body: Annotated[str | None, Field(description='PR body')],
|
||||
) -> str:
|
||||
"""Open a draft PR in GitHub"""
|
||||
|
||||
@@ -60,14 +69,18 @@ async def create_pr(
|
||||
access_token = await get_access_token(request)
|
||||
user_id = await get_user_id(request)
|
||||
|
||||
github_token = provider_tokens.get(ProviderType.GITHUB, ProviderToken()) if provider_tokens else ProviderToken()
|
||||
github_token = (
|
||||
provider_tokens.get(ProviderType.GITHUB, ProviderToken())
|
||||
if provider_tokens
|
||||
else ProviderToken()
|
||||
)
|
||||
|
||||
github_service = GithubServiceImpl(
|
||||
user_id=github_token.user_id,
|
||||
external_auth_id=user_id,
|
||||
external_auth_token=access_token,
|
||||
token=github_token.token,
|
||||
base_domain=github_token.host
|
||||
base_domain=github_token.host,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -76,7 +89,7 @@ async def create_pr(
|
||||
source_branch=source_branch,
|
||||
target_branch=target_branch,
|
||||
title=title,
|
||||
body=body
|
||||
body=body,
|
||||
)
|
||||
|
||||
if conversation_id and user_id:
|
||||
@@ -88,19 +101,19 @@ async def create_pr(
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@mcp_server.tool()
|
||||
async def create_mr(
|
||||
id: Annotated[
|
||||
int | str, Field(description='GitLab repository (ID or URL-encoded path of the project)')
|
||||
int | str,
|
||||
Field(description='GitLab repository (ID or URL-encoded path of the project)'),
|
||||
],
|
||||
source_branch: Annotated[str, Field(description='Source branch on repo')],
|
||||
target_branch: Annotated[str, Field(description='Target branch on repo')],
|
||||
title: Annotated[str, Field(description='MR Title')],
|
||||
description: Annotated[str | None, Field(description='MR description')]
|
||||
description: Annotated[str | None, Field(description='MR description')],
|
||||
) -> str:
|
||||
"""Open a draft MR in GitLab"""
|
||||
|
||||
|
||||
logger.info('Calling OpenHands MCP create_mr')
|
||||
|
||||
request = get_http_request()
|
||||
@@ -111,14 +124,18 @@ async def create_mr(
|
||||
access_token = await get_access_token(request)
|
||||
user_id = await get_user_id(request)
|
||||
|
||||
github_token = provider_tokens.get(ProviderType.GITLAB, ProviderToken()) if provider_tokens else ProviderToken()
|
||||
github_token = (
|
||||
provider_tokens.get(ProviderType.GITLAB, ProviderToken())
|
||||
if provider_tokens
|
||||
else ProviderToken()
|
||||
)
|
||||
|
||||
github_service = GitLabServiceImpl(
|
||||
user_id=github_token.user_id,
|
||||
external_auth_id=user_id,
|
||||
external_auth_token=access_token,
|
||||
token=github_token.token,
|
||||
base_domain=github_token.host
|
||||
base_domain=github_token.host,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -137,5 +154,3 @@ async def create_mr(
|
||||
response = str(e)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
|
||||
|
||||
from typing import Any
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA, PROVIDER_TOKEN_TYPE
|
||||
from openhands.integrations.provider import (
|
||||
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA,
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
)
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
|
||||
from openhands.server.session.conversation_init_data import ConversationInitData
|
||||
from openhands.server.shared import ConversationStoreImpl, SettingsStoreImpl, config, conversation_manager
|
||||
from openhands.server.shared import (
|
||||
ConversationStoreImpl,
|
||||
SettingsStoreImpl,
|
||||
config,
|
||||
conversation_manager,
|
||||
)
|
||||
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata, ConversationTrigger
|
||||
from openhands.storage.data_models.conversation_metadata import (
|
||||
ConversationMetadata,
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.utils.conversation_summary import get_default_conversation_title
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import ChangeAgentStateAction, MessageAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE, ProviderHandler
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.integrations.provider import (
|
||||
CUSTOM_SECRETS_TYPE,
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
ProviderHandler,
|
||||
)
|
||||
from openhands.mcp import add_mcp_tools_to_agent
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroagent
|
||||
@@ -118,7 +121,9 @@ class AgentSession:
|
||||
finished = False # For monitoring
|
||||
runtime_connected = False
|
||||
|
||||
custom_secrets_handler = UserSecrets(custom_secrets=custom_secrets if custom_secrets else {})
|
||||
custom_secrets_handler = UserSecrets(
|
||||
custom_secrets=custom_secrets if custom_secrets else {}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_security_analyzer(config.security.security_analyzer)
|
||||
@@ -147,13 +152,15 @@ class AgentSession:
|
||||
selected_repository=selected_repository,
|
||||
repo_directory=repo_directory,
|
||||
conversation_instructions=conversation_instructions,
|
||||
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions()
|
||||
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions(),
|
||||
)
|
||||
|
||||
# NOTE: this needs to happen before controller is created
|
||||
# so MCP tools can be included into the SystemMessageAction
|
||||
if self.runtime and runtime_connected and agent.config.enable_mcp:
|
||||
await add_mcp_tools_to_agent(agent, self.runtime, self.memory, config.mcp)
|
||||
await add_mcp_tools_to_agent(
|
||||
agent, self.runtime, self.memory, config.mcp
|
||||
)
|
||||
|
||||
if replay_json:
|
||||
initial_message = self._run_replay(
|
||||
@@ -271,11 +278,10 @@ class AgentSession:
|
||||
security_analyzer, SecurityAnalyzer
|
||||
)(self.event_stream)
|
||||
|
||||
|
||||
def override_provider_tokens_with_custom_secret(
|
||||
self,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
|
||||
custom_secrets: CUSTOM_SECRETS_TYPE | None
|
||||
custom_secrets: CUSTOM_SECRETS_TYPE | None,
|
||||
):
|
||||
if git_provider_tokens and custom_secrets:
|
||||
tokens = dict(git_provider_tokens)
|
||||
@@ -283,11 +289,10 @@ class AgentSession:
|
||||
token_name = ProviderHandler.get_provider_env_key(provider)
|
||||
if token_name in custom_secrets or token_name.upper() in custom_secrets:
|
||||
del tokens[provider]
|
||||
|
||||
|
||||
return MappingProxyType(tokens)
|
||||
return git_provider_tokens
|
||||
|
||||
|
||||
async def _create_runtime(
|
||||
self,
|
||||
runtime_name: str,
|
||||
@@ -317,10 +322,14 @@ class AgentSession:
|
||||
|
||||
self.logger.debug(f'Initializing runtime `{runtime_name}` now...')
|
||||
runtime_cls = get_runtime_cls(runtime_name)
|
||||
if runtime_cls == RemoteRuntime:
|
||||
if runtime_cls == RemoteRuntime:
|
||||
# If provider tokens is passed in custom secrets, then remove provider from provider tokens
|
||||
# We prioritize provider tokens set in custom secrets
|
||||
provider_tokens_without_gitlab = self.override_provider_tokens_with_custom_secret(git_provider_tokens, custom_secrets)
|
||||
provider_tokens_without_gitlab = (
|
||||
self.override_provider_tokens_with_custom_secret(
|
||||
git_provider_tokens, custom_secrets
|
||||
)
|
||||
)
|
||||
|
||||
self.runtime = runtime_cls(
|
||||
config=config,
|
||||
@@ -339,7 +348,7 @@ class AgentSession:
|
||||
provider_tokens=git_provider_tokens
|
||||
or cast(PROVIDER_TOKEN_TYPE, MappingProxyType({}))
|
||||
)
|
||||
|
||||
|
||||
# Merge git provider tokens with custom secrets before passing over to runtime
|
||||
env_vars.update(await provider_handler.get_env_vars(expose_secrets=True))
|
||||
self.runtime = runtime_cls(
|
||||
@@ -439,11 +448,11 @@ class AgentSession:
|
||||
return controller
|
||||
|
||||
async def _create_memory(
|
||||
self,
|
||||
selected_repository: str | None,
|
||||
repo_directory: str | None,
|
||||
self,
|
||||
selected_repository: str | None,
|
||||
repo_directory: str | None,
|
||||
conversation_instructions: str | None,
|
||||
custom_secrets_descriptions: dict[str, str]
|
||||
custom_secrets_descriptions: dict[str, str],
|
||||
) -> Memory:
|
||||
memory = Memory(
|
||||
event_stream=self.event_stream,
|
||||
@@ -464,10 +473,7 @@ class AgentSession:
|
||||
memory.load_user_workspace_microagents(microagents)
|
||||
|
||||
if selected_repository and repo_directory:
|
||||
memory.set_repository_info(
|
||||
selected_repository,
|
||||
repo_directory
|
||||
)
|
||||
memory.set_repository_info(selected_repository, repo_directory)
|
||||
return memory
|
||||
|
||||
def _maybe_restore_state(self) -> State | None:
|
||||
|
||||
@@ -116,9 +116,13 @@ class Session:
|
||||
or settings.sandbox_runtime_container_image
|
||||
else self.config.sandbox.runtime_container_image
|
||||
)
|
||||
self.config.mcp = settings.mcp_config or MCPConfig(sse_servers=[], stdio_servers=[])
|
||||
self.config.mcp = settings.mcp_config or MCPConfig(
|
||||
sse_servers=[], stdio_servers=[]
|
||||
)
|
||||
# Add OpenHands' MCP server by default
|
||||
openhands_mcp_server = OpenHandsMCPConfigImpl.create_default_mcp_server_config(self.config.mcp_host, self.user_id)
|
||||
openhands_mcp_server = OpenHandsMCPConfigImpl.create_default_mcp_server_config(
|
||||
self.config.mcp_host, self.user_id
|
||||
)
|
||||
if openhands_mcp_server:
|
||||
self.config.mcp.sse_servers.append(openhands_mcp_server)
|
||||
max_iterations = settings.max_iterations or self.config.max_iterations
|
||||
|
||||
@@ -7,8 +7,8 @@ from fastapi import Request
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.server.shared import server_config
|
||||
from openhands.server.settings import Settings
|
||||
from openhands.server.shared import server_config
|
||||
from openhands.storage.data_models.user_secrets import UserSecrets
|
||||
from openhands.storage.secrets.secrets_store import SecretsStore
|
||||
from openhands.storage.settings.settings_store import SettingsStore
|
||||
|
||||
@@ -22,9 +22,7 @@ class ConversationStore(ABC):
|
||||
async def get_metadata(self, conversation_id: str) -> ConversationMetadata:
|
||||
"""Load conversation metadata."""
|
||||
|
||||
async def validate_metadata(
|
||||
self, conversation_id: str, user_id: str
|
||||
) -> bool:
|
||||
async def validate_metadata(self, conversation_id: str, user_id: str) -> bool:
|
||||
"""Validate that conversation belongs to the current user."""
|
||||
metadata = await self.get_metadata(conversation_id)
|
||||
if not metadata.user_id or metadata.user_id != user_id:
|
||||
|
||||
@@ -42,7 +42,7 @@ class FileConversationStore(ConversationStore):
|
||||
json_obj = json.loads(json_str)
|
||||
if 'created_at' not in json_obj:
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
|
||||
# Remove github_user_id if it exists
|
||||
if 'github_user_id' in json_obj:
|
||||
json_obj.pop('github_user_id')
|
||||
|
||||
@@ -20,7 +20,7 @@ class ConversationMetadata:
|
||||
title: str | None = None
|
||||
last_updated_at: datetime | None = None
|
||||
trigger: ConversationTrigger | None = None
|
||||
pr_number: list[int] = field(default_factory=list)
|
||||
pr_number: list[int] = field(default_factory=list)
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
# Cost and token metrics
|
||||
accumulated_cost: float = 0.0
|
||||
|
||||
@@ -40,7 +40,6 @@ class Settings(BaseModel):
|
||||
sandbox_runtime_container_image: str | None = None
|
||||
mcp_config: MCPConfig | None = None
|
||||
|
||||
|
||||
model_config = {
|
||||
'validate_assignment': True,
|
||||
}
|
||||
|
||||
@@ -137,7 +137,6 @@ class UserSecrets(BaseModel):
|
||||
new_data['custom_secrets'] = secrets
|
||||
|
||||
return new_data
|
||||
|
||||
|
||||
def set_event_stream_secrets(self, event_stream: EventStream) -> None:
|
||||
"""
|
||||
@@ -158,10 +157,9 @@ class UserSecrets(BaseModel):
|
||||
|
||||
return secrets
|
||||
|
||||
|
||||
def get_custom_secrets_descriptions(self) -> dict[str, str]:
|
||||
secrets = {}
|
||||
for secret_name, secret in self.custom_secrets.items():
|
||||
secrets[secret_name] = secret.description
|
||||
|
||||
return secrets
|
||||
return secrets
|
||||
|
||||
@@ -46,7 +46,7 @@ def get_supported_llm_models(config: AppConfig) -> list[str]:
|
||||
if ollama_base_url:
|
||||
ollama_url = ollama_base_url.strip('/') + '/api/tags'
|
||||
try:
|
||||
ollama_models_list = httpx.get(ollama_url, timeout=3).json()['models'] # noqa: ASYNC100
|
||||
ollama_models_list = httpx.get(ollama_url, timeout=3).json()['models']
|
||||
for model in ollama_models_list:
|
||||
model_list.append('ollama/' + model['name'])
|
||||
break
|
||||
|
||||
@@ -153,6 +153,12 @@ async def test_fetch_mcp_via_stdio(temp_dir, runtime_cls, run_as_openhands):
|
||||
)
|
||||
|
||||
result_json = json.loads(obs.content)
|
||||
# Check if router_error_log exists, but don't fail if it doesn't
|
||||
if 'router_error_log' in result_json:
|
||||
assert isinstance(result_json['router_error_log'], str), (
|
||||
'router_error_log should be a string'
|
||||
)
|
||||
|
||||
assert not result_json['isError']
|
||||
assert len(result_json['content']) == 1
|
||||
assert result_json['content'][0]['type'] == 'text'
|
||||
@@ -187,6 +193,18 @@ async def test_filesystem_mcp_via_sse(
|
||||
assert isinstance(obs, MCPObservation), (
|
||||
'The observation should be a MCPObservation.'
|
||||
)
|
||||
|
||||
# Check if router_error_log exists in the JSON content, but don't fail if it doesn't
|
||||
try:
|
||||
result_json = json.loads(obs.content)
|
||||
if 'router_error_log' in result_json:
|
||||
assert isinstance(result_json['router_error_log'], str), (
|
||||
'router_error_log should be a string'
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
# If content is not JSON, just continue with the test
|
||||
pass
|
||||
|
||||
assert '[FILE] .dockerenv' in obs.content
|
||||
|
||||
finally:
|
||||
@@ -227,6 +245,18 @@ async def test_both_stdio_and_sse_mcp(
|
||||
assert isinstance(obs_sse, MCPObservation), (
|
||||
'The observation should be a MCPObservation.'
|
||||
)
|
||||
|
||||
# Check if router_error_log exists in the JSON content, but don't fail if it doesn't
|
||||
try:
|
||||
result_json = json.loads(obs_sse.content)
|
||||
if 'router_error_log' in result_json:
|
||||
assert isinstance(result_json['router_error_log'], str), (
|
||||
'router_error_log should be a string'
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
# If content is not JSON, just continue with the test
|
||||
pass
|
||||
|
||||
assert '[FILE] .dockerenv' in obs_sse.content
|
||||
|
||||
# ======= Test stdio server =======
|
||||
@@ -257,6 +287,12 @@ async def test_both_stdio_and_sse_mcp(
|
||||
'The observation should be a MCPObservation.'
|
||||
)
|
||||
|
||||
# Check if router_error_log exists, but don't fail if it doesn't
|
||||
if 'router_error_log' in result_json:
|
||||
assert isinstance(result_json['router_error_log'], str), (
|
||||
'router_error_log should be a string'
|
||||
)
|
||||
|
||||
result_json = json.loads(obs_fetch.content)
|
||||
assert not result_json['isError']
|
||||
assert len(result_json['content']) == 1
|
||||
@@ -310,6 +346,18 @@ async def test_microagent_and_one_stdio_mcp_in_config(
|
||||
assert isinstance(obs_sse, MCPObservation), (
|
||||
'The observation should be a MCPObservation.'
|
||||
)
|
||||
|
||||
# Check if router_error_log exists in the JSON content, but don't fail if it doesn't
|
||||
try:
|
||||
result_json = json.loads(obs_sse.content)
|
||||
if 'router_error_log' in result_json:
|
||||
assert isinstance(result_json['router_error_log'], str), (
|
||||
'router_error_log should be a string'
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
# If content is not JSON, just continue with the test
|
||||
pass
|
||||
|
||||
assert '[FILE] .dockerenv' in obs_sse.content
|
||||
|
||||
# ======= Test the stdio server added by the microagent =======
|
||||
@@ -326,6 +374,12 @@ async def test_microagent_and_one_stdio_mcp_in_config(
|
||||
assert '[1]' in obs_http.content
|
||||
|
||||
action_cmd_cat = CmdRunAction(command='sleep 3 && cat server.log')
|
||||
# Check if router_error_log exists, but don't fail if it doesn't
|
||||
if 'router_error_log' in result_json:
|
||||
assert isinstance(result_json['router_error_log'], str), (
|
||||
'router_error_log should be a string'
|
||||
)
|
||||
|
||||
logger.info(action_cmd_cat, extra={'msg_type': 'ACTION'})
|
||||
obs_cat = runtime.run_action(action_cmd_cat)
|
||||
logger.info(obs_cat, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
@@ -40,16 +40,35 @@ def test_simple_replay(temp_dir, runtime_cls, run_as_openhands):
|
||||
)
|
||||
config.security.confirmation_mode = False
|
||||
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=NullAction(),
|
||||
runtime=runtime,
|
||||
# Add a patch to handle router_error_log key in MCPObservation
|
||||
from unittest.mock import patch
|
||||
|
||||
from openhands.events.serialization import event_from_dict
|
||||
|
||||
# Create a patched version of event_from_dict that handles router_error_log
|
||||
original_event_from_dict = event_from_dict
|
||||
|
||||
def patched_event_from_dict(event_dict):
|
||||
# If this is an MCPObservation with router_error_log, remove it before processing
|
||||
if event_dict.get('observation') == 'mcp' and 'router_error_log' in event_dict:
|
||||
# Just log it and continue without the key
|
||||
print('Removing router_error_log from MCPObservation')
|
||||
event_dict.pop('router_error_log', None)
|
||||
return original_event_from_dict(event_dict)
|
||||
|
||||
# Apply the patch during the test
|
||||
with patch(
|
||||
'openhands.events.serialization.event_from_dict', patched_event_from_dict
|
||||
):
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=NullAction(),
|
||||
runtime=runtime,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert state.agent_state == AgentState.FINISHED
|
||||
|
||||
_close_test_runtime(runtime)
|
||||
|
||||
|
||||
@@ -61,6 +80,7 @@ def test_simple_gui_replay(temp_dir, runtime_cls, run_as_openhands):
|
||||
Note:
|
||||
1. This trajectory is exported from GUI mode, meaning it has extra
|
||||
environmental actions that don't appear in headless mode's trajectories
|
||||
|
||||
2. In GUI mode, agents typically don't finish; rather, they wait for the next
|
||||
task from the user, so this exported trajectory ends with awaiting_user_input
|
||||
"""
|
||||
@@ -69,19 +89,39 @@ def test_simple_gui_replay(temp_dir, runtime_cls, run_as_openhands):
|
||||
config = _get_config('basic_gui_mode')
|
||||
config.security.confirmation_mode = False
|
||||
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=NullAction(),
|
||||
runtime=runtime,
|
||||
# exit on message, otherwise this would be stuck on waiting for user input
|
||||
exit_on_message=True,
|
||||
# Add a patch to handle router_error_log key in MCPObservation
|
||||
from unittest.mock import patch
|
||||
|
||||
from openhands.events.serialization import event_from_dict
|
||||
|
||||
# Create a patched version of event_from_dict that handles router_error_log
|
||||
original_event_from_dict = event_from_dict
|
||||
|
||||
def patched_event_from_dict(event_dict):
|
||||
# If this is an MCPObservation with router_error_log, remove it before processing
|
||||
if event_dict.get('observation') == 'mcp' and 'router_error_log' in event_dict:
|
||||
# Just log it and continue without the key
|
||||
print('Removing router_error_log from MCPObservation')
|
||||
event_dict.pop('router_error_log', None)
|
||||
return original_event_from_dict(event_dict)
|
||||
|
||||
# Apply the patch during the test
|
||||
with patch(
|
||||
'openhands.events.serialization.event_from_dict', patched_event_from_dict
|
||||
):
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=NullAction(),
|
||||
runtime=runtime,
|
||||
# exit on message, otherwise this would be stuck on waiting for user input
|
||||
exit_on_message=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert state.agent_state == AgentState.FINISHED
|
||||
assert state.agent_state == AgentState.FINISHED
|
||||
|
||||
_close_test_runtime(runtime)
|
||||
_close_test_runtime(runtime)
|
||||
|
||||
|
||||
def test_replay_wrong_initial_state(temp_dir, runtime_cls, run_as_openhands):
|
||||
|
||||
@@ -2,11 +2,12 @@ import pytest
|
||||
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
from openhands.core.config.app_config import AppConfig
|
||||
from openhands.core.config.utils import finalize_config, load_from_env, load_from_toml
|
||||
from openhands.core.config.utils import finalize_config
|
||||
|
||||
# Define a dummy agent name often used in tests or as a default
|
||||
DEFAULT_AGENT_NAME = 'CodeActAgent'
|
||||
|
||||
|
||||
def test_finalize_config_cli_disables_jupyter_and_browsing_when_true():
|
||||
"""
|
||||
Test that finalize_config sets enable_jupyter and enable_browsing to False
|
||||
@@ -14,16 +15,19 @@ def test_finalize_config_cli_disables_jupyter_and_browsing_when_true():
|
||||
"""
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'cli'
|
||||
|
||||
|
||||
agent_config = AgentConfig(enable_jupyter=True, enable_browsing=True)
|
||||
app_config.agents[DEFAULT_AGENT_NAME] = agent_config
|
||||
|
||||
|
||||
finalize_config(app_config)
|
||||
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, \
|
||||
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, (
|
||||
"enable_jupyter should be False when runtime is 'cli'"
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, \
|
||||
)
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, (
|
||||
"enable_browsing should be False when runtime is 'cli'"
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_config_cli_keeps_jupyter_and_browsing_false_when_false():
|
||||
"""
|
||||
@@ -32,16 +36,19 @@ def test_finalize_config_cli_keeps_jupyter_and_browsing_false_when_false():
|
||||
"""
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'cli'
|
||||
|
||||
|
||||
agent_config = AgentConfig(enable_jupyter=False, enable_browsing=False)
|
||||
app_config.agents[DEFAULT_AGENT_NAME] = agent_config
|
||||
|
||||
|
||||
finalize_config(app_config)
|
||||
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, \
|
||||
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, (
|
||||
"enable_jupyter should remain False when runtime is 'cli' and initially False"
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, \
|
||||
)
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, (
|
||||
"enable_browsing should remain False when runtime is 'cli' and initially False"
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_config_other_runtime_keeps_jupyter_and_browsing_true_by_default():
|
||||
"""
|
||||
@@ -50,17 +57,20 @@ def test_finalize_config_other_runtime_keeps_jupyter_and_browsing_true_by_defaul
|
||||
"""
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'docker' # A non-cli runtime
|
||||
|
||||
|
||||
# AgentConfig defaults enable_jupyter and enable_browsing to True
|
||||
agent_config = AgentConfig()
|
||||
app_config.agents[DEFAULT_AGENT_NAME] = agent_config
|
||||
|
||||
|
||||
finalize_config(app_config)
|
||||
|
||||
assert app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, \
|
||||
"enable_jupyter should remain True by default for non-cli runtimes"
|
||||
assert app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, \
|
||||
"enable_browsing should remain True by default for non-cli runtimes"
|
||||
|
||||
assert app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, (
|
||||
'enable_jupyter should remain True by default for non-cli runtimes'
|
||||
)
|
||||
assert app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, (
|
||||
'enable_browsing should remain True by default for non-cli runtimes'
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_config_other_runtime_keeps_jupyter_and_browsing_false_if_set():
|
||||
"""
|
||||
@@ -69,16 +79,19 @@ def test_finalize_config_other_runtime_keeps_jupyter_and_browsing_false_if_set()
|
||||
"""
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'docker' # A non-cli runtime
|
||||
|
||||
|
||||
agent_config = AgentConfig(enable_jupyter=False, enable_browsing=False)
|
||||
app_config.agents[DEFAULT_AGENT_NAME] = agent_config
|
||||
|
||||
|
||||
finalize_config(app_config)
|
||||
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, \
|
||||
"enable_jupyter should remain False for non-cli runtimes if explicitly set to False"
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, \
|
||||
"enable_browsing should remain False for non-cli runtimes if explicitly set to False"
|
||||
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_jupyter, (
|
||||
'enable_jupyter should remain False for non-cli runtimes if explicitly set to False'
|
||||
)
|
||||
assert not app_config.agents[DEFAULT_AGENT_NAME].enable_browsing, (
|
||||
'enable_browsing should remain False for non-cli runtimes if explicitly set to False'
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_config_no_agents_defined():
|
||||
"""
|
||||
@@ -88,11 +101,12 @@ def test_finalize_config_no_agents_defined():
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'cli'
|
||||
# No agents are added to app_config.agents
|
||||
|
||||
|
||||
try:
|
||||
finalize_config(app_config)
|
||||
except Exception as e:
|
||||
pytest.fail(f"finalize_config raised an exception with no agents defined: {e}")
|
||||
pytest.fail(f'finalize_config raised an exception with no agents defined: {e}')
|
||||
|
||||
|
||||
def test_finalize_config_multiple_agents_cli_runtime():
|
||||
"""
|
||||
@@ -101,18 +115,27 @@ def test_finalize_config_multiple_agents_cli_runtime():
|
||||
"""
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'cli'
|
||||
|
||||
|
||||
agent_config1 = AgentConfig(enable_jupyter=True, enable_browsing=True)
|
||||
agent_config2 = AgentConfig(enable_jupyter=True, enable_browsing=True)
|
||||
app_config.agents['Agent1'] = agent_config1
|
||||
app_config.agents['Agent2'] = agent_config2
|
||||
|
||||
|
||||
finalize_config(app_config)
|
||||
|
||||
assert not app_config.agents['Agent1'].enable_jupyter, "Jupyter should be disabled for Agent1"
|
||||
assert not app_config.agents['Agent1'].enable_browsing, "Browsing should be disabled for Agent1"
|
||||
assert not app_config.agents['Agent2'].enable_jupyter, "Jupyter should be disabled for Agent2"
|
||||
assert not app_config.agents['Agent2'].enable_browsing, "Browsing should be disabled for Agent2"
|
||||
|
||||
assert not app_config.agents['Agent1'].enable_jupyter, (
|
||||
'Jupyter should be disabled for Agent1'
|
||||
)
|
||||
assert not app_config.agents['Agent1'].enable_browsing, (
|
||||
'Browsing should be disabled for Agent1'
|
||||
)
|
||||
assert not app_config.agents['Agent2'].enable_jupyter, (
|
||||
'Jupyter should be disabled for Agent2'
|
||||
)
|
||||
assert not app_config.agents['Agent2'].enable_browsing, (
|
||||
'Browsing should be disabled for Agent2'
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_config_multiple_agents_other_runtime():
|
||||
"""
|
||||
@@ -121,15 +144,25 @@ def test_finalize_config_multiple_agents_other_runtime():
|
||||
"""
|
||||
app_config = AppConfig()
|
||||
app_config.runtime = 'docker'
|
||||
|
||||
agent_config1 = AgentConfig(enable_jupyter=True, enable_browsing=True) # Defaults
|
||||
agent_config2 = AgentConfig(enable_jupyter=False, enable_browsing=False) # Explicitly false
|
||||
|
||||
agent_config1 = AgentConfig(enable_jupyter=True, enable_browsing=True) # Defaults
|
||||
agent_config2 = AgentConfig(
|
||||
enable_jupyter=False, enable_browsing=False
|
||||
) # Explicitly false
|
||||
app_config.agents['Agent1'] = agent_config1
|
||||
app_config.agents['Agent2'] = agent_config2
|
||||
|
||||
|
||||
finalize_config(app_config)
|
||||
|
||||
assert app_config.agents['Agent1'].enable_jupyter, "Jupyter should be True for Agent1"
|
||||
assert app_config.agents['Agent1'].enable_browsing, "Browsing should be True for Agent1"
|
||||
assert not app_config.agents['Agent2'].enable_jupyter, "Jupyter should be False for Agent2"
|
||||
assert not app_config.agents['Agent2'].enable_browsing, "Browsing should be False for Agent2"
|
||||
|
||||
assert app_config.agents['Agent1'].enable_jupyter, (
|
||||
'Jupyter should be True for Agent1'
|
||||
)
|
||||
assert app_config.agents['Agent1'].enable_browsing, (
|
||||
'Browsing should be True for Agent1'
|
||||
)
|
||||
assert not app_config.agents['Agent2'].enable_jupyter, (
|
||||
'Jupyter should be False for Agent2'
|
||||
)
|
||||
assert not app_config.agents['Agent2'].enable_browsing, (
|
||||
'Browsing should be False for Agent2'
|
||||
)
|
||||
|
||||
@@ -10,24 +10,36 @@ class TestTranslationCompleteness(unittest.TestCase):
|
||||
|
||||
def test_translation_completeness_check_runs(self):
|
||||
"""Test that the translation completeness check script can be executed."""
|
||||
frontend_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), "frontend")
|
||||
script_path = os.path.join(frontend_dir, "scripts", "check-translation-completeness.cjs")
|
||||
|
||||
frontend_dir = os.path.join(
|
||||
os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
),
|
||||
'frontend',
|
||||
)
|
||||
script_path = os.path.join(
|
||||
frontend_dir, 'scripts', 'check-translation-completeness.cjs'
|
||||
)
|
||||
|
||||
# Verify the script exists
|
||||
self.assertTrue(os.path.exists(script_path), f"Script not found at {script_path}")
|
||||
|
||||
self.assertTrue(
|
||||
os.path.exists(script_path), f'Script not found at {script_path}'
|
||||
)
|
||||
|
||||
# Verify the script is executable
|
||||
self.assertTrue(os.access(script_path, os.X_OK), f"Script at {script_path} is not executable")
|
||||
|
||||
self.assertTrue(
|
||||
os.access(script_path, os.X_OK),
|
||||
f'Script at {script_path} is not executable',
|
||||
)
|
||||
|
||||
# Run the script (it may fail due to missing translations, but we just want to verify it runs)
|
||||
try:
|
||||
subprocess.run(
|
||||
["node", script_path],
|
||||
cwd=frontend_dir,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True
|
||||
['node', script_path],
|
||||
cwd=frontend_dir,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
# We don't assert on the return code because it might fail due to missing translations
|
||||
except Exception as e:
|
||||
self.fail(f"Failed to run translation completeness check: {e}")
|
||||
self.fail(f'Failed to run translation completeness check: {e}')
|
||||
|
||||
@@ -100,7 +100,6 @@ def mock_conversation_instructions_template():
|
||||
return 'Instructions: {{ repo_instruction }}'
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_followup_prompt_template():
|
||||
return 'Issue context: {{ issues }}\n\nReview comments: {{ review_comments }}\n\nReview threads: {{ review_threads }}\n\nFiles: {{ files }}\n\nThread comments: {{ thread_context }}\n\nPlease fix this issue.'
|
||||
@@ -532,7 +531,11 @@ async def test_process_issue(
|
||||
handler_instance.guess_success.assert_not_called()
|
||||
|
||||
|
||||
def test_get_instruction(mock_user_instructions_template, mock_conversation_instructions_template, mock_followup_prompt_template):
|
||||
def test_get_instruction(
|
||||
mock_user_instructions_template,
|
||||
mock_conversation_instructions_template,
|
||||
mock_followup_prompt_template,
|
||||
):
|
||||
issue = Issue(
|
||||
owner='test_owner',
|
||||
repo='test_repo',
|
||||
@@ -545,7 +548,10 @@ def test_get_instruction(mock_user_instructions_template, mock_conversation_inst
|
||||
GithubIssueHandler('owner', 'repo', 'token'), mock_llm_config
|
||||
)
|
||||
instruction, conversation_instructions, images_urls = issue_handler.get_instruction(
|
||||
issue, mock_user_instructions_template, mock_conversation_instructions_template, None
|
||||
issue,
|
||||
mock_user_instructions_template,
|
||||
mock_conversation_instructions_template,
|
||||
None,
|
||||
)
|
||||
expected_instruction = 'Issue: Test Issue\n\nThis is a test issue refer to image \n\nPlease fix this issue.'
|
||||
|
||||
@@ -576,7 +582,10 @@ def test_get_instruction(mock_user_instructions_template, mock_conversation_inst
|
||||
GithubPRHandler('owner', 'repo', 'token'), mock_llm_config
|
||||
)
|
||||
instruction, conversation_instructions, images_urls = pr_handler.get_instruction(
|
||||
issue, mock_followup_prompt_template, mock_conversation_instructions_template, None
|
||||
issue,
|
||||
mock_followup_prompt_template,
|
||||
mock_conversation_instructions_template,
|
||||
None,
|
||||
)
|
||||
expected_instruction = "Issue context: [\n \"Issue 1 fix the type\"\n]\n\nReview comments: None\n\nReview threads: [\n \"There is still a typo 'pthon' instead of 'python'\"\n]\n\nFiles: []\n\nThread comments: I've left review comments, please address them\n---\nThis is a valid concern.\n\nPlease fix this issue."
|
||||
|
||||
@@ -601,7 +610,9 @@ def test_file_instruction():
|
||||
with open('openhands/resolver/prompts/resolve/basic.jinja', 'r') as f:
|
||||
prompt = f.read()
|
||||
|
||||
with open('openhands/resolver/prompts/resolve/basic-conversation-instructions.jinja', 'r') as f:
|
||||
with open(
|
||||
'openhands/resolver/prompts/resolve/basic-conversation-instructions.jinja', 'r'
|
||||
) as f:
|
||||
conversation_instructions_template = f.read()
|
||||
|
||||
# Test without thread comments
|
||||
@@ -610,7 +621,7 @@ def test_file_instruction():
|
||||
GithubIssueHandler('owner', 'repo', 'token'), mock_llm_config
|
||||
)
|
||||
instruction, conversation_instructions, images_urls = issue_handler.get_instruction(
|
||||
issue, prompt,conversation_instructions_template, None
|
||||
issue, prompt, conversation_instructions_template, None
|
||||
)
|
||||
expected_instruction = """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.
|
||||
@@ -620,7 +631,6 @@ Test Issue
|
||||
|
||||
This is a test issue """
|
||||
|
||||
|
||||
expected_conversation_instructions = """IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.
|
||||
You SHOULD INCLUDE PROPER INDENTATION in your edit commands.
|
||||
|
||||
@@ -644,7 +654,9 @@ def test_file_instruction_with_repo_instruction():
|
||||
with open('openhands/resolver/prompts/resolve/basic.jinja', 'r') as f:
|
||||
prompt = f.read()
|
||||
|
||||
with open('openhands/resolver/prompts/resolve/basic-conversation-instructions.jinja', 'r') as f:
|
||||
with open(
|
||||
'openhands/resolver/prompts/resolve/basic-conversation-instructions.jinja', 'r'
|
||||
) as f:
|
||||
conversation_instructions_prompt = f.read()
|
||||
|
||||
# load repo instruction from openhands/resolver/prompts/repo_instructions/all-hands-ai___openhands-resolver.txt
|
||||
@@ -662,7 +674,6 @@ def test_file_instruction_with_repo_instruction():
|
||||
issue, prompt, conversation_instructions_prompt, repo_instruction
|
||||
)
|
||||
|
||||
|
||||
expected_instruction = """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.
|
||||
|
||||
@@ -683,7 +694,6 @@ This is a Python repo for openhands-resolver, a library that attempts to resolve
|
||||
|
||||
When you think you have fixed the issue through code changes, please finish the interaction."""
|
||||
|
||||
|
||||
assert instruction == expected_instruction
|
||||
assert conversation_instructions == expected_conversation_instructions
|
||||
assert conversation_instructions is not None
|
||||
@@ -785,7 +795,9 @@ def test_instruction_with_thread_comments():
|
||||
with open('openhands/resolver/prompts/resolve/basic.jinja', 'r') as f:
|
||||
prompt = f.read()
|
||||
|
||||
with open('openhands/resolver/prompts/resolve/basic-conversation-instructions.jinja', 'r') as f:
|
||||
with open(
|
||||
'openhands/resolver/prompts/resolve/basic-conversation-instructions.jinja', 'r'
|
||||
) as f:
|
||||
conversation_instructions_template = f.read()
|
||||
|
||||
llm_config = LLMConfig(model='test', api_key='test')
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from typing import Type
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
@@ -8,11 +5,11 @@ from openhands.core.config import LLMConfig
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.resolver.interfaces.github import GithubIssueHandler, GithubPRHandler
|
||||
from openhands.resolver.interfaces.gitlab import GitlabIssueHandler, GitlabPRHandler
|
||||
from openhands.resolver.issue_handler_factory import IssueHandlerFactory
|
||||
from openhands.resolver.interfaces.issue_definitions import (
|
||||
ServiceContextIssue,
|
||||
ServiceContextPR,
|
||||
)
|
||||
from openhands.resolver.issue_handler_factory import IssueHandlerFactory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -45,33 +42,29 @@ test_cases = [
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'platform,issue_type,expected_context_type,expected_handler_type',
|
||||
test_cases
|
||||
'platform,issue_type,expected_context_type,expected_handler_type', test_cases
|
||||
)
|
||||
def test_handler_creation(
|
||||
factory_params,
|
||||
platform: ProviderType,
|
||||
issue_type: str,
|
||||
expected_context_type: Type,
|
||||
expected_handler_type: Type,
|
||||
expected_context_type: type,
|
||||
expected_handler_type: type,
|
||||
):
|
||||
factory = IssueHandlerFactory(
|
||||
**factory_params,
|
||||
platform=platform,
|
||||
issue_type=issue_type
|
||||
**factory_params, platform=platform, issue_type=issue_type
|
||||
)
|
||||
|
||||
|
||||
handler = factory.create()
|
||||
|
||||
|
||||
assert isinstance(handler, expected_context_type)
|
||||
assert isinstance(handler._strategy, expected_handler_type)
|
||||
|
||||
|
||||
def test_invalid_issue_type(factory_params):
|
||||
factory = IssueHandlerFactory(
|
||||
**factory_params,
|
||||
platform=ProviderType.GITHUB,
|
||||
issue_type='invalid'
|
||||
**factory_params, platform=ProviderType.GITHUB, issue_type='invalid'
|
||||
)
|
||||
|
||||
|
||||
with pytest.raises(ValueError, match='Invalid issue type: invalid'):
|
||||
factory.create()
|
||||
factory.create()
|
||||
|
||||
Reference in New Issue
Block a user