mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
37 Commits
add-apply-
...
fix-git-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
985df5e24c | ||
|
|
bef6b1afee | ||
|
|
ad85e3249a | ||
|
|
822ce86150 | ||
|
|
305caf1257 | ||
|
|
25d9cf2890 | ||
|
|
17b1a21296 | ||
|
|
97bcb2162d | ||
|
|
8401641f7e | ||
|
|
e2343c0927 | ||
|
|
277064720c | ||
|
|
ef3e0c8dfe | ||
|
|
315d391414 | ||
|
|
95ef8965b7 | ||
|
|
ab9fb50c4f | ||
|
|
f866da6bf2 | ||
|
|
7229a16b45 | ||
|
|
19105a2a13 | ||
|
|
fe486ad1f1 | ||
|
|
0ec6ed20cb | ||
|
|
794381c22b | ||
|
|
0c581ea946 | ||
|
|
f7f4fcf98f | ||
|
|
ab004478f6 | ||
|
|
340606e68a | ||
|
|
daec23b5d7 | ||
|
|
587b4c311a | ||
|
|
7a86402c9c | ||
|
|
06d283dfa0 | ||
|
|
a6a4246e30 | ||
|
|
4830b9a67d | ||
|
|
d4489d62d7 | ||
|
|
e41c020073 | ||
|
|
3f44c8436f | ||
|
|
b740944075 | ||
|
|
5618a3eebb | ||
|
|
2bbc37d552 |
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
@@ -183,7 +183,11 @@ jobs:
|
||||
|
||||
# Run the tests with detailed output
|
||||
cd tests/e2e
|
||||
poetry run python -m pytest test_e2e_workflow.py::test_github_token_configuration test_e2e_workflow.py::test_conversation_start -v --no-header --capture=no --timeout=600
|
||||
poetry run python -m pytest \
|
||||
test_settings.py::test_github_token_configuration \
|
||||
test_conversation.py::test_conversation_start \
|
||||
test_browsing_catchphrase.py::test_browsing_catchphrase \
|
||||
-v --no-header --capture=no --timeout=900
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
|
||||
10
.github/workflows/lint-fix.yml
vendored
10
.github/workflows/lint-fix.yml
vendored
@@ -29,6 +29,12 @@ jobs:
|
||||
run: |
|
||||
cd frontend
|
||||
npm install --frozen-lockfile
|
||||
- name: Generate i18n and route types
|
||||
run: |
|
||||
cd frontend
|
||||
npm run make-i18n
|
||||
npx react-router typegen || true
|
||||
|
||||
- name: Fix frontend lint issues
|
||||
run: |
|
||||
cd frontend
|
||||
@@ -45,7 +51,7 @@ jobs:
|
||||
git config --local user.email "openhands@all-hands.dev"
|
||||
git config --local user.name "OpenHands Bot"
|
||||
git add -A
|
||||
git commit -m "🤖 Auto-fix frontend linting issues"
|
||||
git commit -m "🤖 Auto-fix frontend linting issues" --no-verify
|
||||
git push
|
||||
|
||||
# Python lint fixes
|
||||
@@ -87,5 +93,5 @@ jobs:
|
||||
git config --local user.email "openhands@all-hands.dev"
|
||||
git config --local user.name "OpenHands Bot"
|
||||
git add -A
|
||||
git commit -m "🤖 Auto-fix Python linting issues"
|
||||
git commit -m "🤖 Auto-fix Python linting issues" --no-verify
|
||||
git push
|
||||
|
||||
@@ -87,6 +87,8 @@ VSCode Extension:
|
||||
|
||||
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
|
||||
|
||||
If you need to add labels when opening a PR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
These details may or may not be useful for your current task.
|
||||
|
||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@@ -2,55 +2,102 @@
|
||||
title: Backend Architecture
|
||||
---
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<img src="https://github.com/All-Hands-AI/OpenHands/assets/16201837/97d747e3-29d8-4ccb-8d34-6ad1adb17f38" alt="OpenHands System Architecture Diagram Jul 4 2024" />
|
||||
<p><em>OpenHands System Architecture Diagram (July 4, 2024)</em></p>
|
||||
</div>
|
||||
|
||||
This is a high-level overview of the system architecture. The system is divided into two main components: the frontend and the backend. The frontend is responsible for handling user interactions and displaying the results. The backend is responsible for handling the business logic and executing the agents.
|
||||
|
||||
# Frontend architecture
|
||||
# System overview
|
||||
|
||||

|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["User"] --> FE["Frontend (SPA)"]
|
||||
FE -- "HTTP/WS" --> BE["OpenHands Backend"]
|
||||
BE --> ES["EventStream"]
|
||||
BE --> ST["Storage"]
|
||||
BE --> RT["Runtime Interface"]
|
||||
BE --> LLM["LLM Providers"]
|
||||
|
||||
subgraph Runtime
|
||||
direction TB
|
||||
RT --> DRT["Docker Runtime"]
|
||||
RT --> LRT["Local Runtime"]
|
||||
RT --> RRT["Remote Runtime"]
|
||||
DRT --> AES["Action Execution Server"]
|
||||
LRT --> AES
|
||||
RRT --> AES
|
||||
AES --> Bash["Bash Session"]
|
||||
AES --> Jupyter["Jupyter Plugin"]
|
||||
AES --> Browser["BrowserEnv"]
|
||||
end
|
||||
```
|
||||
|
||||
This Overview is simplified to show the main components and their interactions. For a more detailed view of the backend architecture, see the Backend Architecture section below.
|
||||
|
||||
# Backend Architecture
|
||||
|
||||
_**Disclaimer**: The backend architecture is a work in progress and is subject to change. The following diagram shows the current architecture of the backend based on the commit that is shown in the footer of the diagram._
|
||||
|
||||

|
||||
```mermaid
|
||||
classDiagram
|
||||
class Agent {
|
||||
<<abstract>>
|
||||
+sandbox_plugins: list[PluginRequirement]
|
||||
}
|
||||
class CodeActAgent {
|
||||
+tools
|
||||
}
|
||||
Agent <|-- CodeActAgent
|
||||
|
||||
class EventStream
|
||||
class Observation
|
||||
class Action
|
||||
Action --> Observation
|
||||
Agent --> EventStream
|
||||
|
||||
class Runtime {
|
||||
+connect()
|
||||
+send_action_for_execution()
|
||||
}
|
||||
class ActionExecutionClient {
|
||||
+_send_action_server_request()
|
||||
}
|
||||
class DockerRuntime
|
||||
class LocalRuntime
|
||||
class RemoteRuntime
|
||||
Runtime <|-- ActionExecutionClient
|
||||
ActionExecutionClient <|-- DockerRuntime
|
||||
ActionExecutionClient <|-- LocalRuntime
|
||||
ActionExecutionClient <|-- RemoteRuntime
|
||||
|
||||
class ActionExecutionServer {
|
||||
+/execute_action
|
||||
+/alive
|
||||
}
|
||||
class BashSession
|
||||
class JupyterPlugin
|
||||
class BrowserEnv
|
||||
ActionExecutionServer --> BashSession
|
||||
ActionExecutionServer --> JupyterPlugin
|
||||
ActionExecutionServer --> BrowserEnv
|
||||
|
||||
Agent --> Runtime
|
||||
Runtime ..> ActionExecutionServer : REST
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Updating this Diagram</summary>
|
||||
<div>
|
||||
The generation of the backend architecture diagram is partially automated.
|
||||
The diagram is generated from the type hints in the code using the py2puml
|
||||
tool. The diagram is then manually reviewed, adjusted and exported to PNG
|
||||
and SVG.
|
||||
We maintain architecture diagrams inline with Mermaid in this MDX.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Running python environment in which openhands is executable
|
||||
(according to the instructions in the README.md file in the root of the repository)
|
||||
- [py2puml](https://github.com/lucsorel/py2puml) installed
|
||||
|
||||
## Steps
|
||||
|
||||
1. Autogenerate the diagram by running the following command from the root of the repository:
|
||||
`py2puml openhands openhands > docs/architecture/backend_architecture.puml`
|
||||
|
||||
2. Open the generated file in a PlantUML editor, e.g. Visual Studio Code with the PlantUML extension or [PlantText](https://www.planttext.com/)
|
||||
|
||||
3. Review the generated PUML and make all necessary adjustments to the diagram (add missing parts, fix mistakes, improve positioning).
|
||||
_py2puml creates the diagram based on the type hints in the code, so missing or incorrect type hints may result in an incomplete or incorrect diagram._
|
||||
|
||||
4. Review the diff between the new and the previous diagram and manually check if the changes are correct.
|
||||
_Make sure not to remove parts that were manually added to the diagram in the past and are still relevant._
|
||||
|
||||
5. Add the commit hash of the commit that was used to generate the diagram to the diagram footer.
|
||||
|
||||
6. Export the diagram as PNG and SVG files and replace the existing diagrams in the `docs/architecture` directory. This can be done with (e.g. [PlantText](https://www.planttext.com/))
|
||||
Guidance:
|
||||
- Edit the Mermaid blocks directly (flowchart/classDiagram).
|
||||
- Quote labels and edge text for GitHub preview compatibility.
|
||||
- Keep relationships concise and reflect stable abstractions (agents, runtime client/server, plugins).
|
||||
- Verify accuracy against code:
|
||||
- openhands/runtime/impl/action_execution/action_execution_client.py
|
||||
- openhands/runtime/impl/docker/docker_runtime.py
|
||||
- openhands/runtime/impl/local/local_runtime.py
|
||||
- openhands/runtime/action_execution_server.py
|
||||
- openhands/runtime/plugins/*
|
||||
- Build docs locally or view on GitHub to confirm diagrams render.
|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -52,7 +52,7 @@ graph TD
|
||||
2. Image Building: OpenHands builds a new Docker image (the "OH runtime image") based on the user-provided image. This new image includes OpenHands-specific code, primarily the "runtime client"
|
||||
3. Container Launch: When OpenHands starts, it launches a Docker container using the OH runtime image
|
||||
4. Action Execution Server Initialization: The action execution server initializes an `ActionExecutor` inside the container, setting up necessary components like a bash shell and loading any specified plugins
|
||||
5. Communication: The OpenHands backend (`openhands/runtime/impl/eventstream/eventstream_runtime.py`) communicates with the action execution server over RESTful API, sending actions and receiving observations
|
||||
5. Communication: The OpenHands backend (client: `openhands/runtime/impl/action_execution/action_execution_client.py`; runtimes: `openhands/runtime/impl/docker/docker_runtime.py`, `openhands/runtime/impl/local/local_runtime.py`) communicates with the action execution server over RESTful API, sending actions and receiving observations
|
||||
6. Action Execution: The runtime client receives actions from the backend, executes them in the sandboxed environment, and sends back observations
|
||||
7. Observation Return: The action execution server sends execution results back to the OpenHands backend as observations
|
||||
|
||||
@@ -72,7 +72,7 @@ Check out the [relevant code](https://github.com/All-Hands-AI/OpenHands/blob/mai
|
||||
### Image Tagging System
|
||||
|
||||
OpenHands uses a three-tag system for its runtime images to balance reproducibility with flexibility.
|
||||
Tags may be in one of 2 formats:
|
||||
The tags are:
|
||||
|
||||
- **Versioned Tag**: `oh_v{openhands_version}_{base_image}` (e.g.: `oh_v0.9.9_nikolaik_s_python-nodejs_t_python3.12-nodejs22`)
|
||||
- **Lock Tag**: `oh_v{openhands_version}_{16_digit_lock_hash}` (e.g.: `oh_v0.9.9_1234567890abcdef`)
|
||||
@@ -119,18 +119,52 @@ This tagging approach allows OpenHands to efficiently manage both development an
|
||||
2. The system can quickly rebuild images when minor changes occur (by leveraging recent compatible images)
|
||||
3. The **lock** tag (e.g., `runtime:oh_v0.9.3_1234567890abcdef`) always points to the latest build for a particular base image, dependency, and OpenHands version combination
|
||||
|
||||
## Volume mounts: named volumes and overlay
|
||||
|
||||
OpenHands supports both bind mounts and Docker named volumes in SandboxConfig.volumes:
|
||||
|
||||
- Bind mount: "/abs/host/path:/container/path[:mode]"
|
||||
- Named volume: "volume:<name>:/container/path[:mode]" or any non-absolute host spec treated as a named volume
|
||||
|
||||
Overlay mode (copy-on-write layer) is supported for bind mounts by appending ":overlay" to the mode (e.g., ":ro,overlay").
|
||||
To enable overlay COW, set SANDBOX_VOLUME_OVERLAYS to a writable host directory; per-container upper/work dirs are created under it. If SANDBOX_VOLUME_OVERLAYS is unset, overlay mounts are skipped.
|
||||
|
||||
Implementation references:
|
||||
- openhands/runtime/impl/docker/docker_runtime.py (named volumes in _build_docker_run_args; overlay mounts in _process_overlay_mounts)
|
||||
- openhands/core/config/sandbox_config.py (volumes field)
|
||||
|
||||
|
||||
## Runtime Plugin System
|
||||
|
||||
The OpenHands Runtime supports a plugin system that allows for extending functionality and customizing the runtime environment. Plugins are initialized when the runtime client starts up.
|
||||
The OpenHands Runtime supports a plugin system that allows for extending functionality and customizing the runtime environment. Plugins are initialized when the action execution server starts up inside the runtime.
|
||||
|
||||
Check [an example of Jupyter plugin here](https://github.com/All-Hands-AI/OpenHands/blob/ecf4aed28b0cf7c18d4d8ff554883ba182fc6bdd/openhands/runtime/plugins/jupyter/__init__.py#L21-L55) if you want to implement your own plugin.
|
||||
## Ports and URLs
|
||||
|
||||
*More details about the Plugin system are still under construction - contributions are welcomed!*
|
||||
- Host port allocation uses file-locked ranges for stability and concurrency:
|
||||
- Main runtime port: find_available_port_with_lock on configured range
|
||||
- VSCode port: SandboxConfig.sandbox.vscode_port if provided, else find_available_port_with_lock in VSCODE_PORT_RANGE
|
||||
- App ports: two additional ranges for plugin/web apps
|
||||
- DOCKER_HOST_ADDR (if set) adjusts how URLs are formed for LocalRuntime/Docker environments.
|
||||
- VSCode URL is exposed with a connection token from the action execution server endpoint /vscode/connection_token and rendered as:
|
||||
- Docker/Local: http://localhost:{port}/?tkn={token}&folder={workspace_mount_path_in_sandbox}
|
||||
- RemoteRuntime: scheme://vscode-{host}/?tkn={token}&folder={workspace_mount_path_in_sandbox}
|
||||
|
||||
References:
|
||||
- openhands/runtime/impl/docker/docker_runtime.py (port ranges, locking, DOCKER_HOST_ADDR, vscode_url)
|
||||
- openhands/runtime/impl/local/local_runtime.py (vscode_url factory)
|
||||
- openhands/runtime/impl/remote/remote_runtime.py (vscode_url mapping)
|
||||
- openhands/runtime/action_execution_server.py (/vscode/connection_token)
|
||||
|
||||
|
||||
Examples:
|
||||
- Jupyter: openhands/runtime/plugins/jupyter/__init__.py (JupyterPlugin, Kernel Gateway)
|
||||
- VS Code: openhands/runtime/plugins/vscode/* (VSCodePlugin, exposes tokenized URL)
|
||||
- Agent Skills: openhands/runtime/plugins/agent_skills/*
|
||||
|
||||
Key aspects of the plugin system:
|
||||
|
||||
1. Plugin Definition: Plugins are defined as Python classes that inherit from a base `Plugin` class
|
||||
2. Plugin Registration: Available plugins are registered in an `ALL_PLUGINS` dictionary
|
||||
2. Plugin Registration: Available plugins are registered in `openhands/runtime/plugins/__init__.py` via `ALL_PLUGINS`
|
||||
3. Plugin Specification: Plugins are associated with `Agent.sandbox_plugins: list[PluginRequirement]`. Users can specify which plugins to load when initializing the runtime
|
||||
4. Initialization: Plugins are initialized asynchronously when the runtime client starts
|
||||
5. Usage: The runtime client can use initialized plugins to extend its capabilities (e.g., the JupyterPlugin for running IPython cells)
|
||||
4. Initialization: Plugins are initialized asynchronously when the runtime starts and are accessible to actions
|
||||
5. Usage: Plugins extend capabilities (e.g., Jupyter for IPython cells); the server exposes any web endpoints (ports) via host port mapping
|
||||
|
||||
@@ -65,7 +65,7 @@ To send follow-up messages for the same conversation, mention `@openhands` in a
|
||||
|
||||
Conversation is started by mentioning `@openhands`.
|
||||
|
||||

|
||||

|
||||
|
||||
### See agent response and send follow up messages
|
||||
|
||||
|
||||
45
evaluation/benchmarks/nocode_bench/README.md
Normal file
45
evaluation/benchmarks/nocode_bench/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Evaluate OpenHands on NoCode-bench
|
||||
|
||||
## LLM Setup
|
||||
|
||||
Please follow [here](../../README.md#setup).
|
||||
|
||||
|
||||
## Docker image download
|
||||
|
||||
Evaluating OpenHands on NoCode-bench need instance-level docker image.
|
||||
Please follow the instructions of NoCode-bench image setup to build or download all instance-level dokcer [here](https://github.com/NoCode-bench/NoCode-bench).
|
||||
|
||||
## Generate patch
|
||||
|
||||
Please follow the instructions [here](../swe_bench/README.md#running-locally-with-docker)
|
||||
For example,
|
||||
```bash
|
||||
bash ./evaluation/benchmarks/nocode_bench/scripts/run_infer_nc.sh llm.claude HEAD CodeActAgent 114 100 10 NoCode-bench/NoCode-bench_Verified test
|
||||
```
|
||||
The results will be generated in evaluation/evaluation_outputs/outputs/XXX/CodeActAgent/YYY/output.jsonl.
|
||||
|
||||
## Runing evaluation
|
||||
|
||||
First, install [NoCode-bench](https://github.com/NoCode-bench/NoCode-bench).
|
||||
|
||||
Second, convert the output.jsonl to patch.jsonl with [script](scripts/eval/convert.py).
|
||||
|
||||
```bash
|
||||
python evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py
|
||||
```
|
||||
|
||||
Finally, evaluate with NoCode-bench.
|
||||
|
||||
```bash
|
||||
export PYTHONPATH=$PYTHONPATH:$(pwd)
|
||||
python ./evaluation/eval.py \
|
||||
--predictions_path ./all_preds.jsonl \ # <path_to_your_predictions>
|
||||
--log_dir ./evaluation/logs \ # <path_to_your_log_dir>
|
||||
--bench_tasks NoCode-bench/NoCode-bench_Verified \ # <dataset_name>
|
||||
--max_workers 110 \ # <number_of_workers>
|
||||
--output_file eval_result.txt \ # <path_to_your_output_file>
|
||||
--image_level repo \ # <cache_image_level>
|
||||
--timeout 600 \ # <timeout_in_seconds>
|
||||
--proxy None # <proxy_if_needed>
|
||||
```
|
||||
0
evaluation/benchmarks/nocode_bench/__init__.py
Normal file
0
evaluation/benchmarks/nocode_bench/__init__.py
Normal file
52
evaluation/benchmarks/nocode_bench/binary_patch_utils.py
Normal file
52
evaluation/benchmarks/nocode_bench/binary_patch_utils.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Utilities for handling binary files and patch generation in SWE-bench evaluation.
|
||||
"""
|
||||
|
||||
|
||||
def remove_binary_diffs(patch_text):
|
||||
"""
|
||||
Remove binary file diffs from a git patch.
|
||||
|
||||
Args:
|
||||
patch_text (str): The git patch text
|
||||
|
||||
Returns:
|
||||
str: The cleaned patch text with binary diffs removed
|
||||
"""
|
||||
lines = patch_text.splitlines()
|
||||
cleaned_lines = []
|
||||
block = []
|
||||
is_binary_block = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith('diff --git '):
|
||||
if block and not is_binary_block:
|
||||
cleaned_lines.extend(block)
|
||||
block = [line]
|
||||
is_binary_block = False
|
||||
elif 'Binary files' in line:
|
||||
is_binary_block = True
|
||||
block.append(line)
|
||||
else:
|
||||
block.append(line)
|
||||
|
||||
if block and not is_binary_block:
|
||||
cleaned_lines.extend(block)
|
||||
return '\n'.join(cleaned_lines)
|
||||
|
||||
|
||||
def remove_binary_files_from_git():
|
||||
"""
|
||||
Generate a bash command to remove binary files from git staging.
|
||||
|
||||
Returns:
|
||||
str: A bash command that removes binary files from git staging
|
||||
"""
|
||||
return """
|
||||
for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do
|
||||
if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then
|
||||
git rm -f "$file" 2>/dev/null || rm -f "$file"
|
||||
echo "Removed: $file"
|
||||
fi
|
||||
done
|
||||
""".strip()
|
||||
545
evaluation/benchmarks/nocode_bench/consistants.py
Normal file
545
evaluation/benchmarks/nocode_bench/consistants.py
Normal file
@@ -0,0 +1,545 @@
|
||||
DOCPATH_PATTERNS = [
|
||||
r'docs/',
|
||||
r'^CHANGES\.rst$',
|
||||
r'doc/',
|
||||
r'ChangeLog',
|
||||
r'^changelog/',
|
||||
r'^CHANGES$',
|
||||
]
|
||||
|
||||
MATPLOTLIB_CONFIG = {
|
||||
k: {
|
||||
'python': '3.11',
|
||||
'conda_env': 'matplotlib_35',
|
||||
'install': 'python -m pip install -e .',
|
||||
'test_cmd': 'pytest -rA --color=no',
|
||||
}
|
||||
for k in ['3.5', '3.6', '3.7', '3.8', '3.9']
|
||||
}
|
||||
MATPLOTLIB_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.8',
|
||||
'conda_env': 'matplotlib_31',
|
||||
'install': 'python -m pip install -e .',
|
||||
'test_cmd': 'pytest -rA --color=no',
|
||||
}
|
||||
for k in ['3.1', '3.2', '3.3', '3.4']
|
||||
}
|
||||
)
|
||||
MATPLOTLIB_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.5',
|
||||
'install': 'python setup.py build; python setup.py install',
|
||||
'conda_env': 'matplotlib_11',
|
||||
'nonroot': True,
|
||||
'test_cmd': 'pytest -rA --color=no',
|
||||
}
|
||||
for k in ['2.0', '2.1', '2.2', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5']
|
||||
}
|
||||
)
|
||||
for k in ['3.8', '3.9']:
|
||||
MATPLOTLIB_CONFIG[k]['install'] = (
|
||||
'python -m pip install --no-build-isolation -e ".[dev]"'
|
||||
)
|
||||
|
||||
|
||||
SYMPY_CONFIG = {}
|
||||
SYMPY_CONFIG.update(
|
||||
{
|
||||
'1.0': {
|
||||
'conda_env': 'sympy_10',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'bin/test -C -v',
|
||||
# testfile -k testname
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
REQUESTS_CONFIG = {}
|
||||
REQUESTS_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'requests_227',
|
||||
'install': 'pip install -r requirements-dev.txt',
|
||||
'test_cmd': 'pytest -rA',
|
||||
}
|
||||
for k in ['2.27']
|
||||
}
|
||||
)
|
||||
REQUESTS_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'requests_226',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest -rA',
|
||||
}
|
||||
for k in ['2.26']
|
||||
}
|
||||
)
|
||||
|
||||
PYTEST_CONFIG = {}
|
||||
PYTEST_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'pytest_33',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest -v --color=no',
|
||||
}
|
||||
for k in ['4.4', '4.1', '3.7', '3.4', '3.3']
|
||||
}
|
||||
)
|
||||
|
||||
PYLINT_CONFIG = {}
|
||||
PYLINT_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'pylint_210',
|
||||
'install': 'pip install -r requirements_test.txt',
|
||||
'test_cmd': 'pytest -rA --color=no',
|
||||
}
|
||||
for k in [
|
||||
'2.10',
|
||||
'2.11',
|
||||
'2.13',
|
||||
'2.14',
|
||||
'2.15',
|
||||
'2.16',
|
||||
'2.17',
|
||||
'3.0',
|
||||
'3.1',
|
||||
'3.2',
|
||||
'3.3',
|
||||
]
|
||||
}
|
||||
)
|
||||
PYLINT_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'pylint_210',
|
||||
'pre_install': [
|
||||
r"sed -i 's/setuptools==[0-9.]\+/setuptools==58.0.0/' requirements_test_min.txt"
|
||||
],
|
||||
'install': 'pip install -r requirements_test.txt',
|
||||
'test_cmd': 'pytest -rA --color=no',
|
||||
}
|
||||
for k in ['3.0', '3.1', '3.2', '3.3']
|
||||
}
|
||||
)
|
||||
|
||||
ASTROPY_CONFIG = {}
|
||||
ASTROPY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'astropy_11',
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['1.1', '1.2', '1.3', '2.0']
|
||||
}
|
||||
)
|
||||
ASTROPY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'astropy_30',
|
||||
'pre_install': """echo '[pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning' > pytest.ini""",
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['3.0', '3.1', '3.2']
|
||||
}
|
||||
)
|
||||
ASTROPY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'astropy_40',
|
||||
'pre_install': [
|
||||
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml"""
|
||||
],
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['4.0']
|
||||
}
|
||||
)
|
||||
ASTROPY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'astropy_41',
|
||||
'pre_install': [
|
||||
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml""",
|
||||
"""sed -i 's/^qt_no_exception_capture = 1$/; qt_no_exception_capture = 1/' setup.cfg""",
|
||||
r"""sed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.tomlsed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.toml""",
|
||||
],
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['4.1']
|
||||
}
|
||||
)
|
||||
ASTROPY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'astropy_42',
|
||||
'pre_install': [
|
||||
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml""",
|
||||
r"""sed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.tomlsed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.toml""",
|
||||
],
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['4.2', '4.3', '5.0', '5.1']
|
||||
}
|
||||
)
|
||||
|
||||
ASTROPY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'astropy_52',
|
||||
'pre_install': [
|
||||
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml"""
|
||||
],
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['5.2', '5.3', '6.0', '6.1', '7.0']
|
||||
}
|
||||
)
|
||||
|
||||
DJANGO_CONFIG = {}
|
||||
DJANGO_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'install': 'pip install -e .',
|
||||
'conda_env': 'django_22',
|
||||
'test_cmd': 'python tests/runtests.py --verbosity 2',
|
||||
}
|
||||
for k in ['1.9', '2.2']
|
||||
}
|
||||
)
|
||||
DJANGO_CONFIG.update(
|
||||
{
|
||||
'3.2': {
|
||||
'install': 'pip install -e .',
|
||||
'conda_env': 'django_32',
|
||||
'test_cmd': 'python tests/runtests.py --verbosity 2',
|
||||
},
|
||||
'4.2': {
|
||||
'install': 'pip install -e .',
|
||||
'conda_env': 'django_42',
|
||||
'test_cmd': 'python tests/runtests.py --verbosity 2',
|
||||
},
|
||||
'5.1': {
|
||||
'install': 'pip install -e .',
|
||||
'conda_env': 'django_51',
|
||||
'test_cmd': 'python tests/runtests.py --verbosity 2',
|
||||
},
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG = {}
|
||||
SPHINX_CONFIG.update(
|
||||
{ # 1.x 版本问题,实际无用
|
||||
k: {
|
||||
'conda_env': 'sphinx_20',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': ["sed -i 's/pytest/pytest -rA/' tox.ini"],
|
||||
'test_cmd': 'tox --current-env -epy37 -v --',
|
||||
}
|
||||
for k in ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_20',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy37 -v --',
|
||||
}
|
||||
for k in ['2.0', '2.1', '2.2', '2.3', '2.4']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_30',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
|
||||
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
|
||||
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
|
||||
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py",
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy37 -v --',
|
||||
}
|
||||
for k in ['3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '4.0']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_30',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
|
||||
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
|
||||
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
|
||||
(
|
||||
"grep -q 'sphinxcontrib-htmlhelp>=2.0.0' setup.py && "
|
||||
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py || "
|
||||
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py"
|
||||
),
|
||||
(
|
||||
"grep -q 'sphinxcontrib-serializinghtml>=1.1.5' setup.py && "
|
||||
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py || "
|
||||
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py"
|
||||
),
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy37 -v --',
|
||||
}
|
||||
for k in ['4.1']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_30',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
|
||||
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
|
||||
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
|
||||
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py",
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy37 -v --',
|
||||
}
|
||||
for k in ['4.2', '4.3', '4.4']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_30',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy37 -v --',
|
||||
}
|
||||
for k in ['4.5', '5.0', '5.1', '5.2']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_60',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy39 -v --',
|
||||
}
|
||||
for k in ['6.0', '6.2', '7.0', '7.1']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_72',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
'apt-get update && apt-get install -y graphviz',
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy39 -v --',
|
||||
}
|
||||
for k in ['7.2', '7.3', '7.4']
|
||||
}
|
||||
)
|
||||
SPHINX_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'sphinx_80',
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': [
|
||||
"sed -i 's/pytest/pytest -rA/' tox.ini",
|
||||
],
|
||||
'test_cmd': 'tox --current-env -epy310 -v --',
|
||||
}
|
||||
for k in ['8.0', '8.1']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
SKLEARN_CONFIG = {}
|
||||
SKLEARN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'skl_020',
|
||||
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0.20', '0.21', '0.22']
|
||||
}
|
||||
)
|
||||
SKLEARN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'skl_100',
|
||||
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0.23', '0.24', '1.00', '1.01', '1.02']
|
||||
}
|
||||
)
|
||||
SKLEARN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'skl_104',
|
||||
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['1.03', '1.04', '1.05']
|
||||
}
|
||||
)
|
||||
|
||||
SEABORN_CONFIG = {}
|
||||
SEABORN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'seaborn_010',
|
||||
'install': 'pip install -e .[dev]',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0.3', '0.4', '0.5', '0.6', '0.11', '0.12', '0.13', '0.14']
|
||||
}
|
||||
)
|
||||
|
||||
XARRAY_CONFIG = {}
|
||||
XARRAY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'xarray_0014',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0014', '0015', '0016']
|
||||
}
|
||||
)
|
||||
XARRAY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'xarray_0017',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0017', '0018', '0019', '0020', '0021']
|
||||
}
|
||||
)
|
||||
XARRAY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'xarray_2203',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['2203', '2206', '2209', '2210', '2211', '2212']
|
||||
}
|
||||
)
|
||||
XARRAY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'xarray_2303',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in [
|
||||
'2303',
|
||||
'2304',
|
||||
'2305',
|
||||
'2306',
|
||||
'2308',
|
||||
'2309',
|
||||
'2310',
|
||||
'2311',
|
||||
'2312',
|
||||
]
|
||||
}
|
||||
)
|
||||
XARRAY_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'xarray_2401',
|
||||
'install': 'pip install -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['2401', '2402', '2403', '2405', '2407', '2409', '2410', '2411']
|
||||
}
|
||||
)
|
||||
|
||||
SKLEARN_CONFIG = {}
|
||||
SKLEARN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'skl_020',
|
||||
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0.20', '0.21', '0.22']
|
||||
}
|
||||
)
|
||||
SKLEARN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'skl_100',
|
||||
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['0.23', '0.24', '1.00', '1.01', '1.02']
|
||||
}
|
||||
)
|
||||
SKLEARN_CONFIG.update(
|
||||
{
|
||||
k: {
|
||||
'conda_env': 'skl_104',
|
||||
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'test_cmd': 'pytest --color=no -rA',
|
||||
}
|
||||
for k in ['1.03', '1.04', '1.05', '1.06', '1.07']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
MAP_REPO_TO_CONFIG = {
|
||||
'pydata/xarray': XARRAY_CONFIG,
|
||||
'mwaskom/seaborn': SEABORN_CONFIG,
|
||||
'scikit-learn/scikit-learn': SKLEARN_CONFIG,
|
||||
'sphinx-doc/sphinx': SPHINX_CONFIG,
|
||||
'django/django': DJANGO_CONFIG,
|
||||
'astropy/astropy': ASTROPY_CONFIG,
|
||||
'pylint-dev/pylint': PYLINT_CONFIG,
|
||||
'pytest-dev/pytest': PYTEST_CONFIG,
|
||||
'psf/requests': REQUESTS_CONFIG,
|
||||
'sympy/sympy': SYMPY_CONFIG,
|
||||
'matplotlib/matplotlib': MATPLOTLIB_CONFIG,
|
||||
}
|
||||
65
evaluation/benchmarks/nocode_bench/prompts/nc.j2
Normal file
65
evaluation/benchmarks/nocode_bench/prompts/nc.j2
Normal file
@@ -0,0 +1,65 @@
|
||||
<uploaded_files>
|
||||
/workspace/{{ workspace_dir_name }}
|
||||
</uploaded_files>
|
||||
|
||||
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
|
||||
|
||||
<doc_change>
|
||||
{{ instance.problem_statement }}
|
||||
</doc_change>
|
||||
|
||||
Can you help me add the new features to the repository based on the changes in the <doc_change>?
|
||||
I've already taken care of all changes to any of the test files described in the <doc_change>. This means you DON'T have to modify the testing logic or any of the tests in any way!
|
||||
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
|
||||
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to implement the new features required by the documentation updates.
|
||||
|
||||
Follow these phases to resolve the issue:
|
||||
|
||||
Phase 1. READING: read the requirements and reword it in clearer terms
|
||||
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
|
||||
1.2 Hightlight method names, variables, file names, stack traces, and technical details, particularly those related to new features.
|
||||
1.3 Explain the new feature requirements in clear terms.
|
||||
1.4 Specify functional scope and expected behavior of new features.
|
||||
1.5 Hightlight any best practices to take into account when developing and testing the new feature.
|
||||
|
||||
Phase 2. RUNNING: install and run the functionality in the repository to validate the new features
|
||||
2.1 Follow the readme.
|
||||
2.2 Install the environment and anything needed.
|
||||
2.2 Iterate and figure out how to validate the newly added features.
|
||||
|
||||
Phase 3. EXPLORATION: find the files related to the new features and possible implementation solutions
|
||||
3.1 Use `grep` to search for relevant methods, classes, keywords and feature requirements.
|
||||
3.2 Identify all files related to the new features.
|
||||
3.3 Propose the methods and files to implement the new features and explain why.
|
||||
3.4 From the possible file locations, select the most likely location to implement the new features.
|
||||
|
||||
Phase 4. TEST CREATION: before implementing any new features, create a script to validate the feature's correctness.
|
||||
4.1 Look at existing test files in the repository to understand the test format/structure.
|
||||
4.2 Create a minimal validation script to verify the newly added features.
|
||||
4.3 Run the validation script to confirm the new features are successfully added and working as expected.
|
||||
4.4 Adjust the validation script as necessary to ensure the new features fully meet the requirements.
|
||||
|
||||
Phase 5. FEATURE ANALYSIS: state clearly the new feature and how to implement it
|
||||
5.1 State clearly what the new feature is.
|
||||
5.2 State clearly where the feature should be implemented.
|
||||
5.3 State clearly how the test validates the new feature.
|
||||
5.4 State clearly the best practices to take into account when implementing the new feature.
|
||||
5.5 State clearly how to implement the new feature.
|
||||
|
||||
Phase 6. FEATURE IMPLEMENTATION: edit the source code to implement your chosen solution for the new feature
|
||||
6.1 Make minimal, focused changes to implement the new feature.
|
||||
|
||||
Phase 7. VERIFICATION: Test your new feature thoroughly.
|
||||
7.1 Run your validation script to verify the new feature works as expected.
|
||||
7.2 Add edge cases to your test script to ensure comprehensive coverage of the new feature.
|
||||
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
|
||||
|
||||
Phase 8. FINAL REVIEW: Carefully re-read the feature requirements and compare your changes with the base commit {{ instance.base_commit }}
|
||||
8.1 Ensure you've fully implemented all required features.
|
||||
8.2 Run any tests in the repository related to:
|
||||
8.2.1 The new features you are adding
|
||||
8.2.2 The files you modified
|
||||
8.2.3 The functions you changed
|
||||
8.3 If any tests fail, revise your implementation until all tests pass and the new feature works as expected.
|
||||
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
39
evaluation/benchmarks/nocode_bench/resource/mapping.py
Normal file
39
evaluation/benchmarks/nocode_bench/resource/mapping.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Mapping instance_id to resource_factor.
|
||||
|
||||
Different instances may have different resource requirements.
|
||||
e.g., some instances may require more memory/CPU to run inference.
|
||||
This file tracks the resource requirements of different instances.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_RUNTIME_RESOURCE_FACTOR = int(
|
||||
os.environ.get('DEFAULT_RUNTIME_RESOURCE_FACTOR', 1)
|
||||
)
|
||||
|
||||
# dataset to resource mapping
|
||||
_global_resource_mapping: dict[str, dict[str, float]] = {}
|
||||
|
||||
|
||||
def get_resource_mapping(dataset_name: str) -> dict[str, float]:
|
||||
if dataset_name not in _global_resource_mapping:
|
||||
file_path = os.path.join(CUR_DIR, f'{dataset_name}.json')
|
||||
if not os.path.exists(file_path):
|
||||
logger.info(f'Resource mapping for {dataset_name} not found.')
|
||||
return None
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
_global_resource_mapping[dataset_name] = json.load(f)
|
||||
logger.debug(f'Loaded resource mapping for {dataset_name}')
|
||||
return _global_resource_mapping[dataset_name]
|
||||
|
||||
|
||||
def get_instance_resource_factor(dataset_name: str, instance_id: str) -> int:
|
||||
resource_mapping = get_resource_mapping(dataset_name)
|
||||
if resource_mapping is None:
|
||||
return DEFAULT_RUNTIME_RESOURCE_FACTOR
|
||||
return int(resource_mapping.get(instance_id, DEFAULT_RUNTIME_RESOURCE_FACTOR))
|
||||
909
evaluation/benchmarks/nocode_bench/run_infer_nc.py
Normal file
909
evaluation/benchmarks/nocode_bench/run_infer_nc.py
Normal file
@@ -0,0 +1,909 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import toml
|
||||
from datasets import load_dataset
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import openhands.agenthub
|
||||
from evaluation.benchmarks.nocode_bench.binary_patch_utils import (
|
||||
remove_binary_diffs,
|
||||
remove_binary_files_from_git,
|
||||
)
|
||||
from evaluation.benchmarks.nocode_bench.consistants import MAP_REPO_TO_CONFIG
|
||||
from evaluation.benchmarks.nocode_bench.resource.mapping import (
|
||||
get_instance_resource_factor,
|
||||
)
|
||||
from evaluation.benchmarks.nocode_bench.scripts.utils.evaluation_utils import (
|
||||
run_evaluation_nocode_bench,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
EvalException,
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
assert_and_raise,
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
update_llm_config_for_completions_logging,
|
||||
)
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
OpenHandsConfig,
|
||||
get_evaluation_parser,
|
||||
get_llm_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
|
||||
from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
|
||||
from openhands.events.observation import (
|
||||
CmdOutputObservation,
|
||||
ErrorObservation,
|
||||
FileReadObservation,
|
||||
)
|
||||
from openhands.events.serialization.event import event_from_dict, event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
from openhands.utils.shutdown_listener import sleep_if_should_continue
|
||||
|
||||
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
|
||||
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
|
||||
ENABLE_LLM_EDITOR = os.environ.get('ENABLE_LLM_EDITOR', 'false').lower() == 'true'
|
||||
BenchMode = Literal['swe', 'swt', 'swt-ci']
|
||||
|
||||
# Global variable to track dataset type
|
||||
DATASET_TYPE = 'nc_bench'
|
||||
|
||||
|
||||
def set_dataset_type(dataset_name: str) -> str:
|
||||
"""Set dataset type based on dataset name."""
|
||||
global DATASET_TYPE
|
||||
DATASET_TYPE = 'nc_bench'
|
||||
|
||||
logger.info(f'Dataset type set to: {DATASET_TYPE}')
|
||||
|
||||
|
||||
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
|
||||
'CodeActAgent': codeact_user_response,
|
||||
}
|
||||
|
||||
|
||||
def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
|
||||
return f'{instance.repo.split("/")[-1]}'
|
||||
|
||||
|
||||
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
metadata.details['mode']
|
||||
|
||||
# Determine the template file based on mode and LLM
|
||||
|
||||
template_name = 'nc.j2'
|
||||
|
||||
# Set up Jinja2 environment
|
||||
# Assuming templates are in 'evaluation/benchmarks/swe_bench/prompts' relative to this script
|
||||
prompts_dir = os.path.join(os.path.dirname(__file__), 'prompts')
|
||||
env = Environment(loader=FileSystemLoader(prompts_dir))
|
||||
template = env.get_template(template_name)
|
||||
|
||||
# Prepare context for rendering
|
||||
context = {
|
||||
'instance': instance,
|
||||
'workspace_dir_name': workspace_dir_name,
|
||||
'metadata': metadata, # Pass metadata if needed in templates
|
||||
}
|
||||
|
||||
context['test_instructions'] = '' # Ensure it's defined for other modes
|
||||
|
||||
# Render the instruction
|
||||
instruction = template.render(context)
|
||||
|
||||
if RUN_WITH_BROWSING:
|
||||
instruction += (
|
||||
'<IMPORTANT!>\nYou SHOULD NEVER attempt to browse the web. </IMPORTANT!>\n'
|
||||
)
|
||||
|
||||
if 'image_assets' in instance:
|
||||
assets = json.loads(instance['image_assets'])
|
||||
assert 'problem_statement' in assets, (
|
||||
'problem_statement is required in image_assets'
|
||||
)
|
||||
image_urls = assets['problem_statement']
|
||||
return MessageAction(content=instruction, image_urls=image_urls)
|
||||
return MessageAction(content=instruction)
|
||||
|
||||
|
||||
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
|
||||
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
|
||||
)
|
||||
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
|
||||
|
||||
|
||||
def get_instance_docker_image(
|
||||
instance_id: str,
|
||||
swebench_official_image: bool = False,
|
||||
) -> str:
|
||||
if swebench_official_image:
|
||||
# Official NoCode-Bench image
|
||||
image_name = f'ncbench_{instance_id}:latest'.lower()
|
||||
logger.debug(f'Using official NoCode-Bench image: {image_name}')
|
||||
return image_name
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def get_config(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
) -> OpenHandsConfig:
|
||||
# We use a different instance image for the each instance of NoCode-bench eval
|
||||
use_swebench_official_image = True
|
||||
|
||||
base_container_image = get_instance_docker_image(
|
||||
instance['instance_id'],
|
||||
swebench_official_image=use_swebench_official_image,
|
||||
)
|
||||
logger.info(
|
||||
f'Using instance container image: {base_container_image}. '
|
||||
f'Please make sure this image exists. '
|
||||
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
|
||||
)
|
||||
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
sandbox_config.enable_auto_lint = True
|
||||
sandbox_config.use_host_network = False
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
sandbox_config.platform = 'linux/amd64'
|
||||
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
|
||||
config = OpenHandsConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
|
||||
)
|
||||
)
|
||||
# get 'draft_editor' config if exists
|
||||
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
|
||||
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=False,
|
||||
enable_browsing=RUN_WITH_BROWSING,
|
||||
enable_llm_editor=ENABLE_LLM_EDITOR,
|
||||
enable_mcp=False,
|
||||
condenser=metadata.condenser_config,
|
||||
enable_prompt_extensions=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
|
||||
def make_serializable(obj):
|
||||
if isinstance(obj, pd.Series):
|
||||
obj = obj.to_dict()
|
||||
if isinstance(obj, dict):
|
||||
return {k: make_serializable(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [make_serializable(v) for v in obj]
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(make_serializable(v) for v in obj)
|
||||
elif isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
elif isinstance(obj, pd.Timestamp):
|
||||
return str(obj)
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
def initialize_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required
|
||||
metadata: EvalMetadata,
|
||||
):
|
||||
"""Initialize the runtime for the agent.
|
||||
|
||||
This function is called before the runtime is used to run the agent.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
obs: CmdOutputObservation
|
||||
|
||||
# Set instance id and git configuration
|
||||
action = CmdRunAction(
|
||||
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc && git config --global core.pager "" && git config --global diff.binary false"""
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to export SWE_INSTANCE_ID and configure git: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to export USER: {str(obs)}')
|
||||
|
||||
# inject the init script
|
||||
script_dir = os.path.dirname(__file__)
|
||||
|
||||
# inject the instance info
|
||||
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to create /swe_util/eval_data/instances: {str(obs)}',
|
||||
)
|
||||
|
||||
swe_instance_json_name = 'swe-bench-instance.json'
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Construct the full path for the desired file name within the temporary directory
|
||||
temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
|
||||
# Write to the file with the desired name within the temporary directory
|
||||
|
||||
with open(temp_file_path, 'w') as f:
|
||||
if not isinstance(instance, dict):
|
||||
instance_dict = make_serializable(instance)
|
||||
else:
|
||||
instance_dict = dict(instance)
|
||||
|
||||
if DATASET_TYPE == 'nc_bench':
|
||||
config = MAP_REPO_TO_CONFIG.get(instance['repo'], {}).get(
|
||||
instance['version'], []
|
||||
)
|
||||
docker_conda_env_name = config['conda_env']
|
||||
instance_dict['conda_env'] = docker_conda_env_name
|
||||
|
||||
json.dump([instance_dict], f)
|
||||
|
||||
# Copy the file to the desired location
|
||||
runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
|
||||
|
||||
# inject the instance swe entry
|
||||
entry_script_path = 'instance_nc_entry.sh'
|
||||
|
||||
runtime.copy_to(
|
||||
str(os.path.join(script_dir, f'scripts/setup/{entry_script_path}')),
|
||||
'/swe_util/',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='cat ~/.bashrc')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command='source ~/.bashrc')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if isinstance(obs, ErrorObservation):
|
||||
logger.error(f'Failed to source ~/.bashrc: {str(obs)}')
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command=f'source /swe_util/{entry_script_path}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to source /swe_util/{entry_script_path}: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='git reset --hard')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to git reset --hard: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(
|
||||
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
|
||||
)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
|
||||
|
||||
if DATASET_TYPE != 'Multimodal' and DATASET_TYPE != 'SWE-bench-Live':
|
||||
# Only for non-multimodal datasets, we need to activate the testbed environment for Python
|
||||
# SWE-Bench multimodal datasets and SWE-bench-Live are not using the testbed environment
|
||||
action = CmdRunAction(command='which python')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Expected to find python interpreter, but got: {str(obs)}',
|
||||
)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
|
||||
|
||||
def complete_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
|
||||
) -> dict[str, Any]:
|
||||
"""Complete the runtime for the agent.
|
||||
|
||||
This function is called before the runtime is used to run the agent.
|
||||
If you need to do something in the sandbox to get the correctness metric after
|
||||
the agent has run, modify this function.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Completion Fn')
|
||||
logger.info('-' * 30)
|
||||
obs: CmdOutputObservation
|
||||
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
|
||||
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
if obs.exit_code == -1:
|
||||
# The previous command is still running
|
||||
# We need to kill previous command
|
||||
logger.info('The previous command is still running, trying to kill it...')
|
||||
action = CmdRunAction(command='C-c')
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
# Then run the command again
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
if obs.exit_code == -1:
|
||||
# The previous command is still running
|
||||
# We need to kill previous command
|
||||
logger.info('The previous command is still running, trying to ctrl+z it...')
|
||||
action = CmdRunAction(command='C-z')
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
# Then run the command again
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
)
|
||||
|
||||
action = CmdRunAction(command='git config --global core.pager ""')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to git config --global core.pager "": {str(obs)}',
|
||||
)
|
||||
|
||||
# First check for any git repositories in subdirectories
|
||||
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to find git repositories: {str(obs)}',
|
||||
)
|
||||
|
||||
git_dirs = [p for p in obs.content.strip().split('\n') if p]
|
||||
if git_dirs:
|
||||
# Remove all .git directories in subdirectories
|
||||
for git_dir in git_dirs:
|
||||
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to remove git directory {git_dir}: {str(obs)}',
|
||||
)
|
||||
|
||||
# add all files
|
||||
action = CmdRunAction(command='git add -A')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to git add -A: {str(obs)}',
|
||||
)
|
||||
|
||||
# Remove binary files from git staging
|
||||
action = CmdRunAction(command=remove_binary_files_from_git())
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to remove binary files: {str(obs)}',
|
||||
)
|
||||
|
||||
n_retries = 0
|
||||
git_patch = None
|
||||
while n_retries < 5:
|
||||
action = CmdRunAction(
|
||||
command=f'git diff --no-color --cached {instance["base_commit"]} > patch.diff'
|
||||
)
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
n_retries += 1
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
if obs.exit_code == 0:
|
||||
# Read the patch file
|
||||
action = FileReadAction(path='patch.diff')
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if isinstance(obs, FileReadObservation):
|
||||
git_patch = obs.content
|
||||
break
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
# Fall back to cat "patch.diff" to get the patch
|
||||
assert 'File could not be decoded as utf-8' in obs.content
|
||||
action = CmdRunAction(command='cat patch.diff')
|
||||
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
assert isinstance(obs, CmdOutputObservation) and obs.exit_code == 0
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
git_patch = obs.content
|
||||
break
|
||||
else:
|
||||
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
|
||||
else:
|
||||
logger.info('Failed to get git diff, retrying...')
|
||||
sleep_if_should_continue(10)
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
logger.error(f'Error occurred: {obs.content}. Retrying...')
|
||||
sleep_if_should_continue(10)
|
||||
else:
|
||||
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
|
||||
|
||||
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
|
||||
|
||||
# Remove binary diffs from the patch
|
||||
git_patch = remove_binary_diffs(git_patch)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Completion Fn')
|
||||
logger.info('-' * 30)
|
||||
return {'git_patch': git_patch}
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
reset_logger: bool = True,
|
||||
runtime_failure_count: int = 0,
|
||||
) -> EvalOutput:
|
||||
config = get_config(instance, metadata)
|
||||
|
||||
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
|
||||
if reset_logger:
|
||||
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
|
||||
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
|
||||
else:
|
||||
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
|
||||
|
||||
# Increase resource_factor with increasing attempt_id
|
||||
if runtime_failure_count > 0:
|
||||
config.sandbox.remote_runtime_resource_factor = min(
|
||||
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
|
||||
8,
|
||||
)
|
||||
logger.warning(
|
||||
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
|
||||
)
|
||||
|
||||
metadata = copy.deepcopy(metadata)
|
||||
metadata.details['runtime_failure_count'] = runtime_failure_count
|
||||
metadata.details['remote_runtime_resource_factor'] = (
|
||||
config.sandbox.remote_runtime_resource_factor
|
||||
)
|
||||
|
||||
runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
try:
|
||||
initialize_runtime(runtime, instance, metadata)
|
||||
|
||||
message_action = get_instruction(instance, metadata)
|
||||
|
||||
# Here's how you can run the agent (similar to the `main` function) and get the final task state
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=message_action,
|
||||
runtime=runtime,
|
||||
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
|
||||
metadata.agent_class
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# if fatal error, throw EvalError to trigger re-run
|
||||
if is_fatal_evaluation_error(state.last_error):
|
||||
raise EvalException('Fatal error detected: ' + state.last_error)
|
||||
|
||||
# ======= THIS IS SWE-Bench specific =======
|
||||
# Get git patch
|
||||
if DATASET_TYPE == 'SWE-bench-Live':
|
||||
from evaluation.benchmarks.swe_bench.live_utils import (
|
||||
complete_runtime as complete_runtime_fn,
|
||||
)
|
||||
else:
|
||||
complete_runtime_fn = complete_runtime
|
||||
return_val = complete_runtime_fn(runtime, instance)
|
||||
git_patch = return_val['git_patch']
|
||||
logger.info(
|
||||
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
|
||||
)
|
||||
finally:
|
||||
runtime.close()
|
||||
# ==========================================
|
||||
|
||||
# ======= Attempt to evaluate the agent's edits =======
|
||||
# we use eval_infer.sh to evaluate the agent's edits, not here
|
||||
# because the agent may alter the environment / testcases
|
||||
test_result = {
|
||||
'git_patch': git_patch,
|
||||
}
|
||||
|
||||
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
|
||||
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
|
||||
histories = [event_to_dict(event) for event in state.history]
|
||||
metrics = get_metrics(state)
|
||||
|
||||
# Save the output
|
||||
instruction = message_action.content
|
||||
if message_action.image_urls:
|
||||
instruction += (
|
||||
'\n\n<image_urls>' + '\n'.join(message_action.image_urls) + '</image_urls>'
|
||||
)
|
||||
output = EvalOutput(
|
||||
instance_id=instance.instance_id,
|
||||
instruction=instruction,
|
||||
instance=instance.to_dict(), # SWE Bench specific
|
||||
test_result=test_result,
|
||||
metadata=metadata,
|
||||
history=histories,
|
||||
metrics=metrics,
|
||||
error=state.last_error if state and state.last_error else None,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
|
||||
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
data = toml.load(file)
|
||||
if 'selected_ids' in data:
|
||||
selected_ids = data['selected_ids']
|
||||
logger.info(
|
||||
f'Filtering {len(selected_ids)} tasks from "selected_ids"...'
|
||||
)
|
||||
subset = dataset[dataset[filter_column].isin(selected_ids)]
|
||||
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
|
||||
return subset
|
||||
if 'selected_repos' in data:
|
||||
# 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]
|
||||
assert isinstance(selected_repos, list)
|
||||
logger.info(
|
||||
f'Filtering {selected_repos} tasks from "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"...')
|
||||
return dataset[~dataset[filter_column].isin(skip_ids)]
|
||||
return dataset
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_evaluation_parser()
|
||||
parser.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
default='NoCode-bench/NoCode-bench_Verified',
|
||||
help='data set to evaluate on, either full-test or lite-test',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--split',
|
||||
type=str,
|
||||
default='test',
|
||||
help='split to evaluate on',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--mode',
|
||||
type=str,
|
||||
default='swe',
|
||||
choices=['swe', 'swt', 'swt-ci'],
|
||||
help="mode to run the evaluation, either 'swe', 'swt', or 'swt-ci'",
|
||||
)
|
||||
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
|
||||
# so we don't need to manage file uploading to OpenHands's repo
|
||||
|
||||
dataset = load_dataset(args.dataset, args.split)
|
||||
|
||||
# Set the global dataset type based on dataset name
|
||||
set_dataset_type(args.dataset)
|
||||
|
||||
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
logger.info(
|
||||
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
|
||||
)
|
||||
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
llm_config.log_completions = True
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
# Get condenser config from environment variable
|
||||
condenser_name = os.environ.get('EVAL_CONDENSER')
|
||||
if condenser_name:
|
||||
condenser_config = get_condenser_config_arg(condenser_name)
|
||||
if condenser_config is None:
|
||||
raise ValueError(
|
||||
f'Could not find Condenser config: EVAL_CONDENSER={condenser_name}'
|
||||
)
|
||||
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.'
|
||||
)
|
||||
|
||||
details = {'mode': args.mode}
|
||||
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
|
||||
|
||||
dataset_descrption = (
|
||||
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
|
||||
)
|
||||
metadata = make_metadata(
|
||||
llm_config,
|
||||
dataset_descrption,
|
||||
args.agent_cls,
|
||||
args.max_iterations,
|
||||
args.eval_note,
|
||||
args.eval_output_dir,
|
||||
details=details,
|
||||
condenser_config=condenser_config,
|
||||
)
|
||||
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
print(f'### OUTPUT FILE: {output_file} ###')
|
||||
|
||||
# Run evaluation in iterative mode:
|
||||
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
|
||||
ITERATIVE_EVAL_MODE = (
|
||||
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
|
||||
)
|
||||
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
|
||||
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
|
||||
)
|
||||
|
||||
if not ITERATIVE_EVAL_MODE:
|
||||
# load the dataset
|
||||
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS2PASS'][instances['PASS2PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS2PASS', 'FAIL2PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
run_evaluation_nocode_bench(
|
||||
instances,
|
||||
metadata,
|
||||
output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8
|
||||
* 60
|
||||
* 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
else:
|
||||
critic = AgentFinishedCritic()
|
||||
|
||||
def get_cur_output_file_path(attempt: int) -> str:
|
||||
return (
|
||||
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
|
||||
)
|
||||
|
||||
eval_ids = None
|
||||
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
|
||||
cur_output_file = get_cur_output_file_path(attempt)
|
||||
logger.info(
|
||||
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
|
||||
)
|
||||
|
||||
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
|
||||
# so hopefully we get slightly different results
|
||||
if attempt > 1 and metadata.llm_config.temperature == 0:
|
||||
logger.info(
|
||||
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
|
||||
)
|
||||
metadata.llm_config.temperature = 0.1
|
||||
|
||||
# Load instances - at first attempt, we evaluate all instances
|
||||
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
|
||||
instances = prepare_dataset(
|
||||
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
|
||||
)
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS2PASS'][instances['PASS2PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS2PASS', 'FAIL2PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
# Run evaluation - but save them to cur_output_file
|
||||
logger.info(
|
||||
f'Evaluating {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
run_evaluation_nocode_bench(
|
||||
instances,
|
||||
metadata,
|
||||
cur_output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8
|
||||
* 60
|
||||
* 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
|
||||
# When eval is done, we update eval_ids to the instances that failed the current attempt
|
||||
instances_failed = []
|
||||
logger.info(
|
||||
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
try:
|
||||
history = [
|
||||
event_from_dict(event) for event in instance['history']
|
||||
]
|
||||
critic_result = critic.evaluate(
|
||||
history, instance['test_result'].get('git_patch', '')
|
||||
)
|
||||
if not critic_result.success:
|
||||
instances_failed.append(instance['instance_id'])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Error loading history for instance {instance["instance_id"]}: {e}'
|
||||
)
|
||||
instances_failed.append(instance['instance_id'])
|
||||
logger.info(
|
||||
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
|
||||
)
|
||||
eval_ids = instances_failed
|
||||
|
||||
# If no instances failed, we break
|
||||
if len(instances_failed) == 0:
|
||||
break
|
||||
|
||||
# Then we should aggregate the results from all attempts into the original output file
|
||||
# and remove the intermediate files
|
||||
logger.info(
|
||||
'Aggregating results from all attempts into the original output file...'
|
||||
)
|
||||
fout = open(output_file, 'w')
|
||||
added_instance_ids = set()
|
||||
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
|
||||
cur_output_file = get_cur_output_file_path(attempt)
|
||||
if not os.path.exists(cur_output_file):
|
||||
logger.warning(
|
||||
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
|
||||
)
|
||||
continue
|
||||
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
|
||||
if (
|
||||
instance['instance_id'] not in added_instance_ids
|
||||
and instance['test_result'].get('git_patch', '').strip()
|
||||
):
|
||||
fout.write(line)
|
||||
added_instance_ids.add(instance['instance_id'])
|
||||
logger.info(
|
||||
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
|
||||
)
|
||||
fout.close()
|
||||
logger.info(
|
||||
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
|
||||
)
|
||||
33
evaluation/benchmarks/nocode_bench/scripts/eval/convert.py
Normal file
33
evaluation/benchmarks/nocode_bench/scripts/eval/convert.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
def main(output_jsonl: str):
|
||||
with open(output_jsonl, 'r') as f:
|
||||
for line in f:
|
||||
try:
|
||||
output = json.loads(line)
|
||||
pred = {
|
||||
'instance_id': output['instance_id'],
|
||||
'model_name_or_path': output['metadata']['llm_config']['model'],
|
||||
'model_patch': output['test_result']['git_patch'],
|
||||
}
|
||||
except Exception as e:
|
||||
print(
|
||||
f'Error while reading output of instance {output["instance_id"]}: {e}'
|
||||
)
|
||||
|
||||
print(json.dumps(pred))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'--output_jsonl',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Path to the prediction file (.../outputs.jsonl)',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.output_jsonl)
|
||||
104
evaluation/benchmarks/nocode_bench/scripts/eval/verify_costs.py
Normal file
104
evaluation/benchmarks/nocode_bench/scripts/eval/verify_costs.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import argparse
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
def verify_instance_costs(row: pd.Series) -> float:
|
||||
"""
|
||||
Verifies that the accumulated_cost matches the sum of individual costs in metrics.
|
||||
Also checks for duplicate consecutive costs which might indicate buggy counting.
|
||||
If the consecutive costs are identical, the file is affected by this bug:
|
||||
https://github.com/All-Hands-AI/OpenHands/issues/5383
|
||||
|
||||
Args:
|
||||
row: DataFrame row containing instance data with metrics
|
||||
Returns:
|
||||
float: The verified total cost for this instance (corrected if needed)
|
||||
"""
|
||||
try:
|
||||
metrics = row.get('metrics')
|
||||
if not metrics:
|
||||
logger.warning(f'Instance {row["instance_id"]}: No metrics found')
|
||||
return 0.0
|
||||
|
||||
accumulated = metrics.get('accumulated_cost')
|
||||
costs = metrics.get('costs', [])
|
||||
|
||||
if accumulated is None:
|
||||
logger.warning(
|
||||
f'Instance {row["instance_id"]}: No accumulated_cost in metrics'
|
||||
)
|
||||
return 0.0
|
||||
|
||||
# Check for duplicate consecutive costs and systematic even-odd pairs
|
||||
has_duplicate = False
|
||||
all_pairs_match = True
|
||||
|
||||
# Check each even-odd pair (0-1, 2-3, etc.)
|
||||
for i in range(0, len(costs) - 1, 2):
|
||||
if abs(costs[i]['cost'] - costs[i + 1]['cost']) < 1e-6:
|
||||
has_duplicate = True
|
||||
logger.debug(
|
||||
f'Instance {row["instance_id"]}: Possible buggy double-counting detected! '
|
||||
f'Steps {i} and {i + 1} have identical costs: {costs[i]["cost"]:.2f}'
|
||||
)
|
||||
else:
|
||||
all_pairs_match = False
|
||||
break
|
||||
|
||||
# Calculate total cost, accounting for buggy double counting if detected
|
||||
if len(costs) >= 2 and has_duplicate and all_pairs_match:
|
||||
paired_steps_cost = sum(
|
||||
cost_entry['cost']
|
||||
for cost_entry in costs[: -1 if len(costs) % 2 else None]
|
||||
)
|
||||
real_paired_cost = paired_steps_cost / 2
|
||||
|
||||
unpaired_cost = costs[-1]['cost'] if len(costs) % 2 else 0
|
||||
total_cost = real_paired_cost + unpaired_cost
|
||||
|
||||
else:
|
||||
total_cost = sum(cost_entry['cost'] for cost_entry in costs)
|
||||
|
||||
if not abs(total_cost - accumulated) < 1e-6:
|
||||
logger.warning(
|
||||
f'Instance {row["instance_id"]}: Cost mismatch: '
|
||||
f'accumulated: {accumulated:.2f}, sum of costs: {total_cost:.2f}, '
|
||||
)
|
||||
|
||||
return total_cost
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Error verifying costs for instance {row.get("instance_id", "UNKNOWN")}: {e}'
|
||||
)
|
||||
return 0.0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Verify costs in SWE-bench output file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'input_filepath', type=str, help='Path to the output.jsonl file'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Load and verify the JSONL file
|
||||
df = pd.read_json(args.input_filepath, lines=True)
|
||||
logger.info(f'Loaded {len(df)} instances from {args.input_filepath}')
|
||||
|
||||
# Verify costs for each instance and sum up total
|
||||
total_cost = df.apply(verify_instance_costs, axis=1).sum()
|
||||
logger.info(f'Total verified cost across all instances: ${total_cost:.2f}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to process file: {e}')
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
146
evaluation/benchmarks/nocode_bench/scripts/run_infer_nc.sh
Normal file
146
evaluation/benchmarks/nocode_bench/scripts/run_infer_nc.sh
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
MODEL_CONFIG=$1
|
||||
COMMIT_HASH=$2
|
||||
AGENT=$3
|
||||
EVAL_LIMIT=$4
|
||||
MAX_ITER=$5
|
||||
NUM_WORKERS=$6
|
||||
DATASET=$7
|
||||
SPLIT=$8
|
||||
N_RUNS=$9
|
||||
MODE=${10}
|
||||
|
||||
|
||||
if [ -z "$NUM_WORKERS" ]; then
|
||||
NUM_WORKERS=1
|
||||
echo "Number of workers not specified, use default $NUM_WORKERS"
|
||||
fi
|
||||
checkout_eval_branch
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
echo "Agent not specified, use default CodeActAgent"
|
||||
AGENT="CodeActAgent"
|
||||
fi
|
||||
|
||||
if [ -z "$MAX_ITER" ]; then
|
||||
echo "MAX_ITER not specified, use default 100"
|
||||
MAX_ITER=100
|
||||
fi
|
||||
|
||||
if [ -z "$RUN_WITH_BROWSING" ]; then
|
||||
echo "RUN_WITH_BROWSING not specified, use default false"
|
||||
RUN_WITH_BROWSING=false
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$DATASET" ]; then
|
||||
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
|
||||
DATASET="princeton-nlp/SWE-bench_Lite"
|
||||
fi
|
||||
|
||||
if [ -z "$SPLIT" ]; then
|
||||
echo "SPLIT not specified, use default test"
|
||||
SPLIT="test"
|
||||
fi
|
||||
|
||||
if [ -z "$MODE" ]; then
|
||||
MODE="swe"
|
||||
echo "MODE not specified, use default $MODE"
|
||||
fi
|
||||
|
||||
if [ -n "$EVAL_CONDENSER" ]; then
|
||||
echo "Using Condenser Config: $EVAL_CONDENSER"
|
||||
else
|
||||
echo "No Condenser Config provided via EVAL_CONDENSER, use default (NoOpCondenser)."
|
||||
fi
|
||||
|
||||
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
|
||||
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
|
||||
|
||||
get_openhands_version
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "DATASET: $DATASET"
|
||||
echo "SPLIT: $SPLIT"
|
||||
echo "MAX_ITER: $MAX_ITER"
|
||||
echo "NUM_WORKERS: $NUM_WORKERS"
|
||||
echo "COMMIT_HASH: $COMMIT_HASH"
|
||||
echo "MODE: $MODE"
|
||||
echo "EVAL_CONDENSER: $EVAL_CONDENSER"
|
||||
|
||||
# Default to NOT use Hint
|
||||
if [ -z "$USE_HINT_TEXT" ]; then
|
||||
export USE_HINT_TEXT=false
|
||||
fi
|
||||
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
|
||||
EVAL_NOTE="$OPENHANDS_VERSION"
|
||||
# if not using Hint, add -no-hint to the eval note
|
||||
if [ "$USE_HINT_TEXT" = false ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-no-hint"
|
||||
fi
|
||||
|
||||
if [ "$RUN_WITH_BROWSING" = true ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-with-browsing"
|
||||
fi
|
||||
|
||||
if [ -n "$EXP_NAME" ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
|
||||
fi
|
||||
# if mode != swe, add mode to the eval note
|
||||
if [ "$MODE" != "swe" ]; then
|
||||
EVAL_NOTE="${EVAL_NOTE}-${MODE}"
|
||||
fi
|
||||
# Add condenser config to eval note if provided
|
||||
if [ -n "$EVAL_CONDENSER" ]; then
|
||||
EVAL_NOTE="${EVAL_NOTE}-${EVAL_CONDENSER}"
|
||||
fi
|
||||
|
||||
function run_eval() {
|
||||
local eval_note="${1}"
|
||||
COMMAND="poetry run python evaluation/benchmarks/nocode_bench/run_infer_nc.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--max-iterations $MAX_ITER \
|
||||
--eval-num-workers $NUM_WORKERS \
|
||||
--eval-note $eval_note \
|
||||
--dataset $DATASET \
|
||||
--split $SPLIT \
|
||||
--mode $MODE"
|
||||
|
||||
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
}
|
||||
|
||||
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
|
||||
if [ -z "$N_RUNS" ]; then
|
||||
N_RUNS=1
|
||||
echo "N_RUNS not specified, use default $N_RUNS"
|
||||
fi
|
||||
|
||||
# Skip runs if the run number is in the SKIP_RUNS list
|
||||
# read from env variable SKIP_RUNS as a comma separated list of run numbers
|
||||
SKIP_RUNS=(${SKIP_RUNS//,/ })
|
||||
for i in $(seq 1 $N_RUNS); do
|
||||
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
|
||||
echo "Skipping run $i"
|
||||
continue
|
||||
fi
|
||||
current_eval_note="$EVAL_NOTE-run_$i"
|
||||
echo "EVAL_NOTE: $current_eval_note"
|
||||
run_eval $current_eval_note
|
||||
done
|
||||
|
||||
checkout_original_branch
|
||||
@@ -0,0 +1,54 @@
|
||||
"""This script compares gold patches with OpenHands-generated patches and check whether
|
||||
OpenHands found the right (set of) files to modify.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def extract_modified_files(patch):
|
||||
modified_files = set()
|
||||
file_pattern = re.compile(r'^diff --git a/(.*?) b/')
|
||||
|
||||
for line in patch.split('\n'):
|
||||
match = file_pattern.match(line)
|
||||
if match:
|
||||
modified_files.add(match.group(1))
|
||||
|
||||
return modified_files
|
||||
|
||||
|
||||
def process_report(oh_output_file):
|
||||
succ = 0
|
||||
fail = 0
|
||||
for line in open(oh_output_file):
|
||||
line = json.loads(line)
|
||||
instance_id = line['instance_id']
|
||||
gold_patch = line['swe_instance']['patch']
|
||||
generated_patch = line['git_patch']
|
||||
gold_modified_files = extract_modified_files(gold_patch)
|
||||
# swe-bench lite only: a gold patch always contains exactly one file
|
||||
assert len(gold_modified_files) == 1
|
||||
generated_modified_files = extract_modified_files(generated_patch)
|
||||
|
||||
# Check if all files in gold_patch are also in generated_patch
|
||||
all_files_in_generated = gold_modified_files.issubset(generated_modified_files)
|
||||
if all_files_in_generated:
|
||||
succ += 1
|
||||
else:
|
||||
fail += 1
|
||||
print(
|
||||
f'{instance_id}: file mismatch, gold = {gold_modified_files}, generated = {generated_modified_files}'
|
||||
)
|
||||
print(
|
||||
f'\nSUMMARY: {succ} out of {succ + fail} instances found correct files to edit, success rate = {succ / float(succ + fail)}'
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--oh_output_file', help='Path to the OH output file')
|
||||
args = parser.parse_args()
|
||||
|
||||
process_report(args.oh_output_file)
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ~/.bashrc
|
||||
SWEUTIL_DIR=/swe_util
|
||||
|
||||
if [ -z "$SWE_INSTANCE_ID" ]; then
|
||||
echo "Error: SWE_INSTANCE_ID is not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
item=$(jq --arg INSTANCE_ID "$SWE_INSTANCE_ID" '.[] | select(.instance_id == $INSTANCE_ID)' $SWEUTIL_DIR/eval_data/instances/swe-bench-instance.json)
|
||||
|
||||
|
||||
if [[ -z "$item" ]]; then
|
||||
echo "No item found for the provided instance ID."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME=$(echo "$item" | jq -r '.repo | split("/")[-1]')
|
||||
WORKSPACE_NAME="$REPO_NAME"
|
||||
|
||||
|
||||
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
|
||||
|
||||
|
||||
# Clear the workspace
|
||||
if [ -d /workspace ]; then
|
||||
rm -rf /workspace/*
|
||||
else
|
||||
mkdir /workspace
|
||||
fi
|
||||
# Copy repo to workspace
|
||||
if [ -d /workspace/$WORKSPACE_NAME ]; then
|
||||
rm -rf /workspace/$WORKSPACE_NAME
|
||||
fi
|
||||
mkdir -p /workspace
|
||||
|
||||
SRC_DIR="/root/$REPO_NAME"
|
||||
DEST_DIR="/workspace/$WORKSPACE_NAME"
|
||||
|
||||
cp -r "$SRC_DIR" "$DEST_DIR"
|
||||
|
||||
|
||||
|
||||
echo ">> Extracting conda environment name..."
|
||||
CONDA_ENV_NAME=$(echo "$item" | jq -r '.conda_env // empty')
|
||||
|
||||
# Activate instance-specific environment
|
||||
if [ -d /opt/miniconda3 ]; then
|
||||
. /opt/miniconda3/etc/profile.d/conda.sh
|
||||
conda activate $CONDA_ENV_NAME
|
||||
fi
|
||||
@@ -0,0 +1,154 @@
|
||||
import json
|
||||
import multiprocessing as mp
|
||||
from typing import Awaitable, Callable, TextIO
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pydantic import SecretStr
|
||||
from tqdm import tqdm
|
||||
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
_process_instance_wrapper,
|
||||
_process_instance_wrapper_mp,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
def update_progress_nc(
|
||||
result: EvalOutput,
|
||||
pbar: tqdm,
|
||||
output_fp: TextIO,
|
||||
):
|
||||
"""Update the progress bar and write the result to the output file."""
|
||||
pbar.update(1)
|
||||
pbar.set_description(f'Instance {result.instance_id}')
|
||||
pbar.set_postfix_str(f'Test Result: {str(result.test_result)[:300]}...')
|
||||
logger.info(
|
||||
f'Finished evaluation for instance {result.instance_id}: '
|
||||
f'{str(result.test_result)[:300]}...\n'
|
||||
)
|
||||
|
||||
def make_serializable(obj):
|
||||
if isinstance(obj, pd.Series):
|
||||
return make_serializable(obj.to_dict())
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return {k: make_serializable(v) for k, v in obj.items()}
|
||||
|
||||
elif isinstance(obj, (list, tuple, set)):
|
||||
converted = [make_serializable(v) for v in obj]
|
||||
if isinstance(obj, list):
|
||||
return converted
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(converted)
|
||||
else: # set
|
||||
return converted
|
||||
|
||||
elif isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
|
||||
elif isinstance(obj, np.generic):
|
||||
return obj.item()
|
||||
|
||||
elif isinstance(obj, pd.Timestamp):
|
||||
return obj.isoformat()
|
||||
|
||||
elif SecretStr is not None and isinstance(obj, SecretStr):
|
||||
return str(obj)
|
||||
|
||||
else:
|
||||
return obj
|
||||
|
||||
try:
|
||||
raw_data = result.model_dump(mode='python', round_trip=False)
|
||||
safe_data = make_serializable(raw_data)
|
||||
output_fp.write(json.dumps(safe_data, ensure_ascii=False) + '\n')
|
||||
output_fp.flush()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to write full result: {e}')
|
||||
|
||||
fallback = {
|
||||
'instance_id': result.instance_id,
|
||||
'model_patch': result.test_result.get('git_patch', ''),
|
||||
}
|
||||
try:
|
||||
output_fp.write(json.dumps(fallback, ensure_ascii=False) + '\n')
|
||||
output_fp.flush()
|
||||
logger.info(
|
||||
f'Wrote fallback result for instance {result.instance_id}: only instance_id and model_patch.'
|
||||
)
|
||||
except Exception as e2:
|
||||
logger.error(f'Failed to write fallback result: {e2}')
|
||||
|
||||
|
||||
def cleanup():
|
||||
print('Cleaning up child processes...')
|
||||
for process in mp.active_children():
|
||||
print(f'Terminating child process: {process.name}')
|
||||
process.terminate()
|
||||
process.join()
|
||||
|
||||
|
||||
def run_evaluation_nocode_bench(
|
||||
dataset: pd.DataFrame,
|
||||
metadata: EvalMetadata | None,
|
||||
output_file: str,
|
||||
num_workers: int,
|
||||
process_instance_func: Callable[
|
||||
[pd.Series, EvalMetadata, bool], Awaitable[EvalOutput]
|
||||
],
|
||||
max_retries: int = 5, # number of retries for each instance
|
||||
timeout_seconds: int | None = None,
|
||||
):
|
||||
use_multiprocessing = num_workers > 1
|
||||
|
||||
if metadata is not None:
|
||||
logger.info(
|
||||
f'Evaluation started with Agent {metadata.agent_class}:\n'
|
||||
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.\n'
|
||||
)
|
||||
else:
|
||||
logger.warning('Running evaluation without metadata.')
|
||||
logger.info(f'Evaluation started with {num_workers} workers.')
|
||||
|
||||
total_instances = len(dataset)
|
||||
pbar = tqdm(total=total_instances, desc='Instances processed')
|
||||
output_fp = open(output_file, 'a')
|
||||
|
||||
try:
|
||||
if use_multiprocessing:
|
||||
with mp.Pool(num_workers) as pool:
|
||||
args_iter = (
|
||||
(
|
||||
process_instance_func,
|
||||
instance,
|
||||
metadata,
|
||||
True,
|
||||
max_retries,
|
||||
timeout_seconds,
|
||||
)
|
||||
for _, instance in dataset.iterrows()
|
||||
)
|
||||
results = pool.imap_unordered(_process_instance_wrapper_mp, args_iter)
|
||||
for result in results:
|
||||
update_progress_nc(result, pbar, output_fp)
|
||||
else:
|
||||
for _, instance in dataset.iterrows():
|
||||
result = _process_instance_wrapper(
|
||||
process_instance_func=process_instance_func,
|
||||
instance=instance,
|
||||
metadata=metadata,
|
||||
use_mp=False,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
update_progress_nc(result, pbar, output_fp)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print('\nKeyboardInterrupt received. Cleaning up...\n')
|
||||
cleanup()
|
||||
|
||||
output_fp.close()
|
||||
logger.info('\nEvaluation finished.\n')
|
||||
2
evaluation/regression/.gitignore
vendored
2
evaluation/regression/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
outputs
|
||||
@@ -1,70 +0,0 @@
|
||||
# OpenHands - Regression Test Framework
|
||||
|
||||
OpenHands project is an open-source software engineering AI that can solve various software engineering tasks. This repository contains the regression test framework for OpenHands project.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run the tests for OpenHands project, you can use the provided test runner script. Follow these steps:
|
||||
|
||||
1. Ensure you have Python 3.6 or higher installed on your system.
|
||||
2. Install the required dependencies by running the following command in your terminal:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Navigate to the root directory of the project.
|
||||
4. Run the test suite using the test runner script with the required arguments:
|
||||
```
|
||||
python evaluation/regression/run_tests.py --OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx --model=gpt-4o
|
||||
```
|
||||
Replace `sk-xxxxxxxxxxxxxxxxxxxxxx` with your actual OpenAI API key. The default model is `gpt-4o`, but you can specify a different model if needed.
|
||||
|
||||
The test runner will discover and execute all the test cases in the `cases/` directory, and display the results of the test suite, including the status of each individual test case and the overall summary.
|
||||
|
||||
## Test Case Structure
|
||||
|
||||
The test cases for OpenHands project are organized in the `cases/` directory. Each test case has the following structure:
|
||||
|
||||
```
|
||||
cases/
|
||||
├── hello-world/
|
||||
│ ├── task.txt
|
||||
│ ├── outputs/
|
||||
│ │ └── codeact_agent/
|
||||
│ │ └── workspace/
|
||||
│ │ ├── hello_world.sh
|
||||
│ └── test_hello_world.py
|
||||
├── create_web_app/
|
||||
│ ├── task.txt
|
||||
│ ├── outputs/
|
||||
│ │ └── codeact_agent/
|
||||
│ │ └── workspace/
|
||||
│ │ ├── app.py
|
||||
│ │ ├── requirements.txt
|
||||
│ │ ├── static/
|
||||
│ │ └── templates/
|
||||
│ └── test_create_web_app.py
|
||||
└── ...
|
||||
```
|
||||
|
||||
- `task.txt`: This file contains the task description provided by the user.
|
||||
- `outputs/`: This directory contains the output generated by OpenHands for each agent.
|
||||
- `outputs/*/workspace/`: This directory contains the actual output files generated by OpenHands.
|
||||
- `test_*.py`: These are the test scripts that validate the output of OpenHands.
|
||||
|
||||
## Adding New Test Cases
|
||||
|
||||
To add a new test case to the regression test framework, follow the same steps as described in the previous sections.
|
||||
|
||||
## Customizing the Test Cases
|
||||
|
||||
The test cases can be customized by modifying the fixtures defined in the `conftest.py` file. The available fixtures are:
|
||||
|
||||
- `test_cases_dir`: The directory containing the test cases.
|
||||
- `task_file`: The path to the `task.txt` file for the current test case.
|
||||
- `workspace_dir`: The path to the `workspace/` directory for the current test case.
|
||||
- `model`: The model selected start the generation.
|
||||
- `run_test_case`: A fixture that runs OpenHands and generates the workspace for the current test case.
|
||||
|
||||
You can modify these fixtures to change the behavior of the test cases or add new ones as needed.
|
||||
|
||||
If you have any questions or need further assistance, feel free to reach out to the project maintainers.
|
||||
@@ -1 +0,0 @@
|
||||
Write an API server in node express which responds with a random number, and a frontend in React that displays the next number from the API
|
||||
@@ -1 +0,0 @@
|
||||
Write a simple hello world server in node Express
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "hello world"
|
||||
@@ -1 +0,0 @@
|
||||
Rewrite the script so that it prints the user's name, using the first argument. If there's no name, default to "world"
|
||||
@@ -1 +0,0 @@
|
||||
Write a bash script named "hello_world.sh" that prints "Hello, World!"
|
||||
@@ -1,20 +0,0 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from conftest import agents
|
||||
|
||||
|
||||
@pytest.mark.parametrize('agent', agents())
|
||||
def test_hello_world(task_file, run_test_case, agent):
|
||||
"""Test case for the "Hello, World!" Bash script using different agents."""
|
||||
# Run the test case for the specified agent
|
||||
workspace_dir = run_test_case(agent, 'hello-world')
|
||||
|
||||
# Validate the generated workspace
|
||||
assert os.path.exists(workspace_dir)
|
||||
assert os.path.isfile(os.path.join(workspace_dir, 'hello_world.sh'))
|
||||
|
||||
# Execute the hello_world.sh script
|
||||
os.chdir(workspace_dir)
|
||||
output = os.popen('bash hello_world.sh').read()
|
||||
assert output == 'Hello, World!\n'
|
||||
@@ -1,2 +0,0 @@
|
||||
def string_length(s):
|
||||
return len(s)
|
||||
@@ -1,2 +0,0 @@
|
||||
def to_lowercase(s):
|
||||
return s.lower()
|
||||
@@ -1,2 +0,0 @@
|
||||
def reverse_string(s):
|
||||
return s[::-1]
|
||||
@@ -1,7 +0,0 @@
|
||||
import random
|
||||
|
||||
|
||||
def scramble_string(s):
|
||||
s_list = list(s)
|
||||
random.shuffle(s_list)
|
||||
return ''.join(s_list)
|
||||
@@ -1,8 +0,0 @@
|
||||
def spongebob_case(s):
|
||||
result = ''
|
||||
for i, char in enumerate(s):
|
||||
if i % 2 == 0:
|
||||
result += char.lower()
|
||||
else:
|
||||
result += char.upper()
|
||||
return result
|
||||
@@ -1,2 +0,0 @@
|
||||
def to_uppercase(s):
|
||||
return s.upper()
|
||||
@@ -1,55 +0,0 @@
|
||||
import sys
|
||||
|
||||
|
||||
def print_help():
|
||||
help_text = """
|
||||
Usage: python string_cli.py <command> <string>
|
||||
|
||||
Commands:
|
||||
reverse - Reverses the input string.
|
||||
uppercase - Converts the input string to uppercase.
|
||||
lowercase - Converts the input string to lowercase.
|
||||
spongebob - Converts the input string to spongebob case.
|
||||
length - Returns the length of the input string.
|
||||
scramble - Randomly scrambles the characters in the input string.
|
||||
"""
|
||||
print(help_text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2 and sys.argv[1] == '--help':
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
elif len(sys.argv) < 3:
|
||||
print('Usage: python string_cli.py <command> <string>')
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
input_string = sys.argv[2]
|
||||
|
||||
if command == 'reverse':
|
||||
from commands.reverse import reverse_string
|
||||
|
||||
print(reverse_string(input_string))
|
||||
elif command == 'uppercase':
|
||||
from commands.uppercase import to_uppercase
|
||||
|
||||
print(to_uppercase(input_string))
|
||||
elif command == 'lowercase':
|
||||
from commands.lowercase import to_lowercase
|
||||
|
||||
print(to_lowercase(input_string))
|
||||
elif command == 'spongebob':
|
||||
from commands.spongebob import spongebob_case
|
||||
|
||||
print(spongebob_case(input_string))
|
||||
elif command == 'length':
|
||||
from commands.length import string_length
|
||||
|
||||
print(string_length(input_string))
|
||||
elif command == 'scramble':
|
||||
from commands.scramble import scramble_string
|
||||
|
||||
print(scramble_string(input_string))
|
||||
else:
|
||||
print('Invalid command!')
|
||||
@@ -1 +0,0 @@
|
||||
Please rewrite the entire CLI in node.js
|
||||
@@ -1,2 +0,0 @@
|
||||
def string_length(s):
|
||||
return len(s)
|
||||
@@ -1,2 +0,0 @@
|
||||
def to_lowercase(s):
|
||||
return s.lower()
|
||||
@@ -1,2 +0,0 @@
|
||||
def reverse_string(s):
|
||||
return s[::-1]
|
||||
@@ -1,7 +0,0 @@
|
||||
import random
|
||||
|
||||
|
||||
def scramble_string(s):
|
||||
s_list = list(s)
|
||||
random.shuffle(s_list)
|
||||
return ''.join(s_list)
|
||||
@@ -1,8 +0,0 @@
|
||||
def spongebob_case(s):
|
||||
result = ''
|
||||
for i, char in enumerate(s):
|
||||
if i % 2 == 0:
|
||||
result += char.lower()
|
||||
else:
|
||||
result += char.upper()
|
||||
return result
|
||||
@@ -1,2 +0,0 @@
|
||||
def to_uppercase(s):
|
||||
return s.upper()
|
||||
@@ -1,36 +0,0 @@
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 3:
|
||||
print('Usage: python string_cli.py <command> <string>')
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
input_string = sys.argv[2]
|
||||
|
||||
if command == 'reverse':
|
||||
from commands.reverse import reverse_string
|
||||
|
||||
print(reverse_string(input_string))
|
||||
elif command == 'uppercase':
|
||||
from commands.uppercase import to_uppercase
|
||||
|
||||
print(to_uppercase(input_string))
|
||||
elif command == 'lowercase':
|
||||
from commands.lowercase import to_lowercase
|
||||
|
||||
print(to_lowercase(input_string))
|
||||
elif command == 'spongebob':
|
||||
from commands.spongebob import spongebob_case
|
||||
|
||||
print(spongebob_case(input_string))
|
||||
elif command == 'length':
|
||||
from commands.length import string_length
|
||||
|
||||
print(string_length(input_string))
|
||||
elif command == 'scramble':
|
||||
from commands.scramble import scramble_string
|
||||
|
||||
print(scramble_string(input_string))
|
||||
else:
|
||||
print('Invalid command!')
|
||||
@@ -1 +0,0 @@
|
||||
Please add a --help option to the CLI, with a detailed description of each command
|
||||
@@ -1 +0,0 @@
|
||||
Write a python CLI for string manipulation. The CLI should accept a command, and a string. The commands should include `reverse`, `uppercase`, `lowercase`, `spongebob`, `length`, and `scramble`. The logic for each command should live in its own file.
|
||||
@@ -1 +0,0 @@
|
||||
Write a simple TODO list application in React
|
||||
@@ -1,21 +0,0 @@
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
|
||||
class HelloWorldHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'Hello World\n')
|
||||
|
||||
|
||||
def run(server_class=HTTPServer, handler_class=HelloWorldHandler, port=8000):
|
||||
server_address = ('', port)
|
||||
httpd = server_class(server_address, handler_class)
|
||||
print(f'Starting httpd on port {port}...')
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('starting server...')
|
||||
run()
|
||||
@@ -1 +0,0 @@
|
||||
Make sure the server works and responds appropriately
|
||||
@@ -1,171 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
CASES_DIR = os.path.join(SCRIPT_DIR, 'cases')
|
||||
AGENTHUB_DIR = os.path.join(SCRIPT_DIR, '../', 'agenthub')
|
||||
|
||||
|
||||
def agents():
|
||||
"""Retrieves a list of available agents.
|
||||
|
||||
Returns:
|
||||
A list of agent names.
|
||||
"""
|
||||
agents = []
|
||||
for agent in os.listdir(AGENTHUB_DIR):
|
||||
if os.path.isdir(os.path.join(AGENTHUB_DIR, agent)) and agent.endswith(
|
||||
'_agent'
|
||||
):
|
||||
agents.append(agent)
|
||||
return agents
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_cases_dir():
|
||||
"""Fixture that provides the directory path for test cases.
|
||||
|
||||
Returns:
|
||||
The directory path for test cases.
|
||||
"""
|
||||
return CASES_DIR
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def task_file(test_cases_dir, request):
|
||||
"""Fixture that provides the path to the task file for a test case.
|
||||
|
||||
Args:
|
||||
test_cases_dir: The directory path for test cases.
|
||||
request: The pytest request object.
|
||||
|
||||
Returns:
|
||||
The path to the task file for the test case.
|
||||
"""
|
||||
test_case_dir = os.path.dirname(request.module.__file__)
|
||||
task_file_path = os.path.join(test_case_dir, 'task.txt')
|
||||
return task_file_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace_dir(test_cases_dir, request):
|
||||
"""Fixture that provides the workspace directory for a test case.
|
||||
|
||||
Args:
|
||||
test_cases_dir: The directory path for test cases.
|
||||
request: The pytest request object.
|
||||
|
||||
Returns:
|
||||
The workspace directory for the test case.
|
||||
"""
|
||||
test_case_dir = os.path.dirname(request.module.__file__)
|
||||
workspace_dir = os.path.join(test_case_dir, 'workspace')
|
||||
return workspace_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model(request):
|
||||
"""Fixture that provides the model name.
|
||||
|
||||
Args:
|
||||
request: The pytest request object.
|
||||
|
||||
Returns:
|
||||
The model name, defaulting to "gpt-3.5-turbo".
|
||||
"""
|
||||
return request.config.getoption('model', default='gpt-3.5-turbo')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_test_case(test_cases_dir, workspace_dir, request):
|
||||
"""Fixture that provides a function to run a test case.
|
||||
|
||||
Args:
|
||||
test_cases_dir: The directory path for test cases.
|
||||
workspace_dir: The workspace directory for the test case.
|
||||
request: The pytest request object.
|
||||
|
||||
Returns:
|
||||
A function that runs a test case for a given agent and case.
|
||||
"""
|
||||
|
||||
def _run_test_case(agent, case):
|
||||
"""Runs a test case for a given agent.
|
||||
|
||||
Args:
|
||||
agent: The name of the agent to run the test case for.
|
||||
case: The name of the test case to run.
|
||||
|
||||
Returns:
|
||||
The path to the workspace directory for the agent and test case.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the test case execution fails (non-zero return code).
|
||||
|
||||
Steps:
|
||||
"""
|
||||
case_dir = os.path.join(test_cases_dir, case)
|
||||
task = open(os.path.join(case_dir, 'task.txt'), 'r').read().strip()
|
||||
outputs_dir = os.path.join(case_dir, 'outputs')
|
||||
agent_dir = os.path.join(outputs_dir, agent)
|
||||
|
||||
if not os.path.exists(agent_dir):
|
||||
os.makedirs(agent_dir)
|
||||
|
||||
shutil.rmtree(os.path.join(agent_dir, 'workspace'), ignore_errors=True)
|
||||
if os.path.isdir(os.path.join(case_dir, 'start')):
|
||||
os.copytree(
|
||||
os.path.join(case_dir, 'start'), os.path.join(agent_dir, 'workspace')
|
||||
)
|
||||
else:
|
||||
os.makedirs(os.path.join(agent_dir, 'workspace'))
|
||||
agents_ref = {
|
||||
'codeact_agent': 'CodeActAgent',
|
||||
}
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
'python3',
|
||||
f'{SCRIPT_DIR}/../../openhands/main.py',
|
||||
'-d',
|
||||
f'{os.path.join(agent_dir, "workspace")}',
|
||||
'-c',
|
||||
f'{agents_ref[agent]}',
|
||||
'-t',
|
||||
f'{task}',
|
||||
'-m',
|
||||
'gpt-3.5-turbo',
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
logging.info(f'Stdout: {stdout}')
|
||||
logging.error(f'Stderr: {stderr}')
|
||||
|
||||
assert process.returncode == 0
|
||||
return os.path.join(agent_dir, 'workspace')
|
||||
|
||||
return _run_test_case
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Configuration hook for pytest.
|
||||
|
||||
Args:
|
||||
config: The pytest configuration object.
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(f'test_results_{now.strftime("%Y%m%d_%H%M%S")}.log'),
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
@@ -1,32 +0,0 @@
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.config import load_openhands_config
|
||||
|
||||
config = load_openhands_config()
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""Main entry point of the script.
|
||||
|
||||
This script runs pytest with specific arguments and configuration.
|
||||
|
||||
Usage:
|
||||
python script_name.py [--OPENAI_API_KEY=<api_key>] [--model=<model_name>]
|
||||
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='This script runs pytest with specific arguments and configuration.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--OPENAI_API_KEY', type=str, required=True, help='Your OpenAI API key'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--model', type=str, required=True, help='The model name to use'
|
||||
)
|
||||
|
||||
parser_args = parser.parse_args()
|
||||
config.config['OPENAI_API_KEY'] = parser_args.OPENAI_API_KEY
|
||||
args = ['-v', 'evaluation/regression/cases', f'-o model={parser_args.model}']
|
||||
|
||||
pytest.main(args)
|
||||
@@ -0,0 +1,135 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { TaskTrackingObservationContent } from "#/components/features/chat/task-tracking-observation-content";
|
||||
import { TaskTrackingObservation } from "#/types/core/observations";
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"TASK_TRACKING_OBSERVATION$TASK_LIST": "Task List",
|
||||
"TASK_TRACKING_OBSERVATION$TASK_ID": "ID",
|
||||
"TASK_TRACKING_OBSERVATION$TASK_NOTES": "Notes",
|
||||
"TASK_TRACKING_OBSERVATION$RESULT": "Result",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("TaskTrackingObservationContent", () => {
|
||||
const mockEvent: TaskTrackingObservation = {
|
||||
id: 123,
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "agent",
|
||||
observation: "task_tracking",
|
||||
content: "Task tracking operation completed successfully",
|
||||
cause: 122,
|
||||
message: "Task tracking operation completed successfully",
|
||||
extras: {
|
||||
command: "plan",
|
||||
task_list: [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Implement feature A",
|
||||
status: "todo",
|
||||
notes: "This is a test task",
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
title: "Fix bug B",
|
||||
status: "in_progress",
|
||||
},
|
||||
{
|
||||
id: "task-3",
|
||||
title: "Deploy to production",
|
||||
status: "done",
|
||||
notes: "Completed successfully",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("does not render command section", () => {
|
||||
render(<TaskTrackingObservationContent event={mockEvent} />);
|
||||
|
||||
expect(screen.queryByText("Command")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("plan")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders task list when command is 'plan' and tasks exist", () => {
|
||||
render(<TaskTrackingObservationContent event={mockEvent} />);
|
||||
|
||||
expect(screen.getByText("Task List (3 items)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Implement feature A")).toBeInTheDocument();
|
||||
expect(screen.getByText("Fix bug B")).toBeInTheDocument();
|
||||
expect(screen.getByText("Deploy to production")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays correct status icons and badges", () => {
|
||||
render(<TaskTrackingObservationContent event={mockEvent} />);
|
||||
|
||||
// Check for status text (the icons are emojis)
|
||||
expect(screen.getByText("todo")).toBeInTheDocument();
|
||||
expect(screen.getByText("in progress")).toBeInTheDocument();
|
||||
expect(screen.getByText("done")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays task IDs and notes", () => {
|
||||
render(<TaskTrackingObservationContent event={mockEvent} />);
|
||||
|
||||
expect(screen.getByText("ID: task-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("ID: task-2")).toBeInTheDocument();
|
||||
expect(screen.getByText("ID: task-3")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Notes: This is a test task")).toBeInTheDocument();
|
||||
expect(screen.getByText("Notes: Completed successfully")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders result section when content exists", () => {
|
||||
render(<TaskTrackingObservationContent event={mockEvent} />);
|
||||
|
||||
expect(screen.getByText("Result")).toBeInTheDocument();
|
||||
expect(screen.getByText("Task tracking operation completed successfully")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render task list when command is not 'plan'", () => {
|
||||
const eventWithoutPlan = {
|
||||
...mockEvent,
|
||||
extras: {
|
||||
...mockEvent.extras,
|
||||
command: "view",
|
||||
},
|
||||
};
|
||||
|
||||
render(<TaskTrackingObservationContent event={eventWithoutPlan} />);
|
||||
|
||||
expect(screen.queryByText("Task List")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render task list when task list is empty", () => {
|
||||
const eventWithEmptyTasks = {
|
||||
...mockEvent,
|
||||
extras: {
|
||||
...mockEvent.extras,
|
||||
task_list: [],
|
||||
},
|
||||
};
|
||||
|
||||
render(<TaskTrackingObservationContent event={eventWithEmptyTasks} />);
|
||||
|
||||
expect(screen.queryByText("Task List")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render result section when content is empty", () => {
|
||||
const eventWithoutContent = {
|
||||
...mockEvent,
|
||||
content: "",
|
||||
};
|
||||
|
||||
render(<TaskTrackingObservationContent event={eventWithoutContent} />);
|
||||
|
||||
expect(screen.queryByText("Result")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,8 @@ describe("LikertScale", () => {
|
||||
expect(screen.getByText(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION)).toBeInTheDocument();
|
||||
expect(screen.getByText(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES)).toBeInTheDocument();
|
||||
expect(screen.getByText(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST)).toBeInTheDocument();
|
||||
expect(screen.getByText(I18nKey.FEEDBACK$REASON_DIDNT_FINISH_JOB)).toBeInTheDocument();
|
||||
expect(screen.getByText(I18nKey.FEEDBACK$REASON_OTHER)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
@@ -36,6 +36,8 @@ vi.mock("react-i18next", async () => {
|
||||
"SETTINGS$NAV_API_KEYS": "API Keys",
|
||||
"SETTINGS$NAV_LLM": "LLM",
|
||||
"SETTINGS$NAV_USER": "User",
|
||||
"SETTINGS$NAV_SECRETS": "Secrets",
|
||||
"SETTINGS$NAV_MCP": "MCP",
|
||||
"SETTINGS$TITLE": "Settings"
|
||||
};
|
||||
return translations[key] || key;
|
||||
@@ -47,8 +49,33 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useConfig hook
|
||||
const { mockUseConfig } = vi.hoisted(() => ({
|
||||
mockUseConfig: vi.fn(),
|
||||
}));
|
||||
vi.mock("#/hooks/query/use-config", () => ({
|
||||
useConfig: mockUseConfig,
|
||||
}));
|
||||
|
||||
describe("Settings Billing", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
beforeEach(() => {
|
||||
// Set default config to OSS mode
|
||||
mockUseConfig.mockReturnValue({
|
||||
data: {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
const RoutesStub = createRoutesStub([
|
||||
{
|
||||
@@ -79,19 +106,7 @@ describe("Settings Billing", () => {
|
||||
});
|
||||
|
||||
it("should not render the credits tab if OSS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
// OSS mode is set by default in beforeEach
|
||||
renderSettingsScreen();
|
||||
|
||||
const navbar = await screen.findByTestId("settings-navbar");
|
||||
@@ -100,17 +115,20 @@ describe("Settings Billing", () => {
|
||||
});
|
||||
|
||||
it("should render the credits tab if SaaS mode and billing is enabled", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
mockUseConfig.mockReturnValue({
|
||||
data: {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -121,17 +139,20 @@ describe("Settings Billing", () => {
|
||||
|
||||
it("should render the billing settings if clicking the credits item", async () => {
|
||||
const user = userEvent.setup();
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
mockUseConfig.mockReturnValue({
|
||||
data: {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -19,6 +19,9 @@ vi.mock("react-i18next", async () => {
|
||||
SETTINGS$NAV_CREDITS: "Credits",
|
||||
SETTINGS$NAV_API_KEYS: "API Keys",
|
||||
SETTINGS$NAV_LLM: "LLM",
|
||||
SETTINGS$NAV_SECRETS: "Secrets",
|
||||
SETTINGS$NAV_MCP: "MCP",
|
||||
SETTINGS$NAV_USER: "User",
|
||||
SETTINGS$TITLE: "Settings",
|
||||
};
|
||||
return translations[key] || key;
|
||||
@@ -119,22 +122,21 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
it("should render the saas navbar", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return app mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
const saasConfig = { APP_MODE: "saas" };
|
||||
|
||||
// Clear any existing query data and set the config
|
||||
mockQueryClient.clear();
|
||||
mockQueryClient.setQueryData(["config"], saasConfig);
|
||||
|
||||
const sectionsToInclude = [
|
||||
"user",
|
||||
"integrations",
|
||||
"application",
|
||||
"credits", // The nav item shows "credits" text but routes to /billing
|
||||
"secrets",
|
||||
"api keys",
|
||||
];
|
||||
const sectionsToExclude = ["llm"];
|
||||
|
||||
// Clear any existing query data
|
||||
mockQueryClient.clear();
|
||||
const sectionsToExclude = ["llm", "mcp"];
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -151,8 +153,6 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
expect(sectionElement).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
getConfigSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not be able to access saas-only routes in oss mode", async () => {
|
||||
|
||||
51
frontend/__tests__/utils/browser-tab.test.ts
Normal file
51
frontend/__tests__/utils/browser-tab.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
import { browserTab } from "#/utils/browser-tab";
|
||||
|
||||
// These tests exercise the browser-tab notification flasher behavior.
|
||||
// Specifically we verify that when the document title changes externally
|
||||
// while a notification is active, the flasher updates its internal
|
||||
// baseline so it restores/toggles to the new title instead of an old one.
|
||||
|
||||
describe("browserTab notifications", () => {
|
||||
const MESSAGE = "Agent ready";
|
||||
const INITIAL = "Conversation 123 | OpenHands";
|
||||
const RENAMED = "My renamed title | OpenHands";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
// reset title for each test
|
||||
document.title = INITIAL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
browserTab.stopNotification();
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("updates baseline when title changes during an active notification and restores to the new title", () => {
|
||||
// Start flashing
|
||||
browserTab.startNotification(MESSAGE);
|
||||
|
||||
// Tick once: should switch to the message
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(document.title).toBe(MESSAGE);
|
||||
|
||||
// Simulate an external rename while flashing (e.g., user edits title)
|
||||
document.title = RENAMED;
|
||||
|
||||
// Next tick: flasher observes the external change and updates baseline
|
||||
vi.advanceTimersByTime(1000);
|
||||
// On this tick, we toggle back to the message
|
||||
expect(document.title).toBe(MESSAGE);
|
||||
|
||||
// Next tick should toggle to the updated baseline (renamed title)
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(document.title).toBe(RENAMED);
|
||||
|
||||
// Stop flashing: title should remain the updated baseline
|
||||
browserTab.stopNotification();
|
||||
expect(document.title).toBe(RENAMED);
|
||||
});
|
||||
});
|
||||
283
frontend/package-lock.json
generated
283
frontend/package-lock.json
generated
@@ -18,9 +18,9 @@
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/stripe-js": "^7.8.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tanstack/react-query": "^5.85.3",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -30,14 +30,14 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next": "^25.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.29",
|
||||
"jose": "^6.0.12",
|
||||
"lucide-react": "^0.539.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.259.0",
|
||||
"posthog-js": "^1.260.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -60,8 +60,8 @@
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.54.2",
|
||||
@@ -69,7 +69,7 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.2.0",
|
||||
@@ -226,13 +226,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"license": "MIT",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
|
||||
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -459,12 +458,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"license": "MIT",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
|
||||
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.0"
|
||||
"@babel/types": "^7.28.2"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -616,17 +614,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"license": "MIT",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
|
||||
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/types": "^7.28.2",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3405,6 +3402,15 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -5899,26 +5905,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
||||
"integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
|
||||
"integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"enhanced-resolve": "^5.18.1",
|
||||
"jiti": "^2.4.2",
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.5.1",
|
||||
"lightningcss": "1.30.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.11"
|
||||
"tailwindcss": "4.1.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
|
||||
"integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
|
||||
"integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.4",
|
||||
"tar": "^7.4.3"
|
||||
@@ -5927,28 +5931,27 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.11",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.11",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.11",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.11",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.11",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.12",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.12",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.12",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.12",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.12",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
|
||||
"integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
|
||||
"integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -5958,13 +5961,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
|
||||
"integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
|
||||
"integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -5974,13 +5976,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
|
||||
"integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
|
||||
"integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -5990,13 +5991,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
|
||||
"integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
|
||||
"integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -6006,13 +6006,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
|
||||
"integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
|
||||
"integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -6022,13 +6021,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
|
||||
"integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
|
||||
"integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -6038,13 +6036,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
|
||||
"integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
|
||||
"integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -6054,13 +6051,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
|
||||
"integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
|
||||
"integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -6070,13 +6066,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
|
||||
"integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
|
||||
"integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -6086,9 +6081,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
|
||||
"integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
|
||||
"integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -6100,14 +6095,13 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@emnapi/wasi-threads": "^1.0.2",
|
||||
"@napi-rs/wasm-runtime": "^0.2.11",
|
||||
"@tybys/wasm-util": "^0.9.0",
|
||||
"@emnapi/core": "^1.4.5",
|
||||
"@emnapi/runtime": "^1.4.5",
|
||||
"@emnapi/wasi-threads": "^1.0.4",
|
||||
"@napi-rs/wasm-runtime": "^0.2.12",
|
||||
"@tybys/wasm-util": "^0.10.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6115,17 +6109,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.5",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"@emnapi/wasi-threads": "1.0.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.5",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -6134,7 +6128,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -6143,18 +6137,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.12",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -6169,13 +6163,12 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
|
||||
"integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
|
||||
"integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -6185,13 +6178,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
|
||||
"integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
|
||||
"integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -6201,16 +6193,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz",
|
||||
"integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz",
|
||||
"integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.11",
|
||||
"@tailwindcss/oxide": "4.1.11",
|
||||
"@tailwindcss/node": "4.1.12",
|
||||
"@tailwindcss/oxide": "4.1.12",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.11"
|
||||
"tailwindcss": "4.1.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
@@ -6230,14 +6221,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
|
||||
"integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz",
|
||||
"integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.11",
|
||||
"@tailwindcss/oxide": "4.1.11",
|
||||
"tailwindcss": "4.1.11"
|
||||
"@tailwindcss/node": "4.1.12",
|
||||
"@tailwindcss/oxide": "4.1.12",
|
||||
"tailwindcss": "4.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
@@ -6261,22 +6251,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.83.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz",
|
||||
"integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==",
|
||||
"license": "MIT",
|
||||
"version": "5.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.3.tgz",
|
||||
"integrity": "sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.85.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.0.tgz",
|
||||
"integrity": "sha512-t1HMfToVMGfwEJRya6GG7gbK0luZJd+9IySFNePL1BforU1F3LqQ3tBC2Rpvr88bOrlU6PXyMLgJD0Yzn4ztUw==",
|
||||
"license": "MIT",
|
||||
"version": "5.85.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.3.tgz",
|
||||
"integrity": "sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.83.1"
|
||||
"@tanstack/query-core": "5.85.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6334,17 +6322,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz",
|
||||
"integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==",
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz",
|
||||
"integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "^4.4.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"picocolors": "^1.1.1",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
@@ -11134,9 +11120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.3.4",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.4.tgz",
|
||||
"integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==",
|
||||
"version": "25.3.6",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.6.tgz",
|
||||
"integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -11151,7 +11137,6 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -14817,10 +14802,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.259.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.259.0.tgz",
|
||||
"integrity": "sha512-6usLnJshky8fQ82ask7PIJh4BSFOU0VkRbFg8Zanm/HIlYMG1VOdRWlToA63JXeO7Bzm9TuREq1wFm5U2VEVCg==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"version": "1.260.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.260.1.tgz",
|
||||
"integrity": "sha512-DD8ZSRpdScacMqtqUIvMFme8lmOWkOvExG8VvjONE7Cm3xpRH5xXpfrwMJE4bayTGWKMx4ij6SfphK6dm/o2ug==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
@@ -17029,10 +17013,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||
"license": "MIT"
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
|
||||
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/stripe-js": "^7.8.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tanstack/react-query": "^5.85.3",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -29,14 +29,14 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next": "^25.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.29",
|
||||
"jose": "^6.0.12",
|
||||
"lucide-react": "^0.539.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.259.0",
|
||||
"posthog-js": "^1.260.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -84,8 +84,8 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.54.2",
|
||||
@@ -93,7 +93,7 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.2.0",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ThinkAction,
|
||||
OpenHandsAction,
|
||||
FinishAction,
|
||||
TaskTrackingAction,
|
||||
} from "#/types/core/actions";
|
||||
import { getDefaultEventContent, MAX_CONTENT_LENGTH } from "./shared";
|
||||
import i18n from "#/i18n";
|
||||
@@ -79,6 +80,38 @@ const getThinkActionContent = (event: ThinkAction): string =>
|
||||
|
||||
const getFinishActionContent = (event: FinishAction): string =>
|
||||
event.args.final_thought.trim();
|
||||
|
||||
const getTaskTrackingActionContent = (event: TaskTrackingAction): string => {
|
||||
let content = `**Command:** \`${event.args.command}\``;
|
||||
|
||||
if (
|
||||
event.args.command === "plan" &&
|
||||
event.args.task_list &&
|
||||
event.args.task_list.length > 0
|
||||
) {
|
||||
content += `\n\n**Task List (${event.args.task_list.length} ${event.args.task_list.length === 1 ? "item" : "items"}):**\n`;
|
||||
|
||||
event.args.task_list.forEach((task, index) => {
|
||||
const statusIcon =
|
||||
{
|
||||
todo: "⏳",
|
||||
in_progress: "🔄",
|
||||
done: "✅",
|
||||
}[task.status] || "❓";
|
||||
|
||||
content += `\n${index + 1}. ${statusIcon} **[${task.status.toUpperCase().replace("_", " ")}]** ${task.title}`;
|
||||
content += `\n *ID: ${task.id}*`;
|
||||
if (task.notes) {
|
||||
content += `\n *Notes: ${task.notes}*`;
|
||||
}
|
||||
});
|
||||
} else if (event.args.command === "plan") {
|
||||
content += "\n\n**Task List:** Empty";
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const getNoContentActionContent = (): string => "";
|
||||
|
||||
export const getActionContent = (event: OpenHandsAction): string => {
|
||||
@@ -102,6 +135,8 @@ export const getActionContent = (event: OpenHandsAction): string => {
|
||||
return getThinkActionContent(event);
|
||||
case "finish":
|
||||
return getFinishActionContent(event);
|
||||
case "task_tracking":
|
||||
return getTaskTrackingActionContent(event);
|
||||
default:
|
||||
return getDefaultEventContent(event);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BrowseObservation,
|
||||
OpenHandsObservation,
|
||||
RecallObservation,
|
||||
TaskTrackingObservation,
|
||||
} from "#/types/core/observations";
|
||||
import { getObservationResult } from "./get-observation-result";
|
||||
import { getDefaultEventContent, MAX_CONTENT_LENGTH } from "./shared";
|
||||
@@ -102,6 +103,40 @@ const getRecallObservationContent = (event: RecallObservation): string => {
|
||||
return content;
|
||||
};
|
||||
|
||||
const getTaskTrackingObservationContent = (
|
||||
event: TaskTrackingObservation,
|
||||
): string => {
|
||||
const { command, task_list: taskList } = event.extras;
|
||||
let content = `**Command:** \`${command}\``;
|
||||
|
||||
if (command === "plan" && taskList.length > 0) {
|
||||
content += `\n\n**Task List (${taskList.length} ${taskList.length === 1 ? "item" : "items"}):**\n`;
|
||||
|
||||
taskList.forEach((task, index) => {
|
||||
const statusIcon =
|
||||
{
|
||||
todo: "⏳",
|
||||
in_progress: "🔄",
|
||||
done: "✅",
|
||||
}[task.status] || "❓";
|
||||
|
||||
content += `\n${index + 1}. ${statusIcon} **[${task.status.toUpperCase().replace("_", " ")}]** ${task.title}`;
|
||||
content += `\n *ID: ${task.id}*`;
|
||||
if (task.notes) {
|
||||
content += `\n *Notes: ${task.notes}*`;
|
||||
}
|
||||
});
|
||||
} else if (command === "plan") {
|
||||
content += "\n\n**Task List:** Empty";
|
||||
}
|
||||
|
||||
if (event.content && event.content.trim()) {
|
||||
content += `\n\n**Result:** ${event.content.trim()}`;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export const getObservationContent = (event: OpenHandsObservation): string => {
|
||||
switch (event.observation) {
|
||||
case "read":
|
||||
@@ -118,6 +153,8 @@ export const getObservationContent = (event: OpenHandsObservation): string => {
|
||||
return getBrowseObservationContent(event);
|
||||
case "recall":
|
||||
return getRecallObservationContent(event);
|
||||
case "task_tracking":
|
||||
return getTaskTrackingObservationContent(event);
|
||||
default:
|
||||
return getDefaultEventContent(event);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
|
||||
|
||||
const ACTION_NO_RENDER_LIST: OpenHandsEventType[] = ["recall"];
|
||||
|
||||
const OBSERVATION_NO_RENDER_LIST: OpenHandsEventType[] = ["think"];
|
||||
|
||||
export const shouldRenderEvent = (
|
||||
event: OpenHandsAction | OpenHandsObservation,
|
||||
) => {
|
||||
@@ -35,7 +37,10 @@ export const shouldRenderEvent = (
|
||||
return false;
|
||||
}
|
||||
|
||||
return !COMMON_NO_RENDER_LIST.includes(event.observation);
|
||||
const noRenderList = COMMON_NO_RENDER_LIST.concat(
|
||||
OBSERVATION_NO_RENDER_LIST,
|
||||
);
|
||||
return !noRenderList.includes(event.observation);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import {
|
||||
@@ -10,12 +11,14 @@ import {
|
||||
isFinishAction,
|
||||
isRejectObservation,
|
||||
isMcpObservation,
|
||||
isTaskTrackingObservation,
|
||||
} from "#/types/core/guards";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { ImageCarousel } from "../images/image-carousel";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { ErrorMessage } from "./error-message";
|
||||
import { MCPObservationContent } from "./mcp-observation-content";
|
||||
import { TaskTrackingObservationContent } from "./task-tracking-observation-content";
|
||||
import { getObservationResult } from "./event-content-helpers/get-observation-result";
|
||||
import { getEventContent } from "./event-content-helpers/get-event-content";
|
||||
import { GenericEventMessage } from "./generic-event-message";
|
||||
@@ -58,6 +61,7 @@ export function EventMessage({
|
||||
actions,
|
||||
isInLast10Actions,
|
||||
}: EventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowConfirmationButtons =
|
||||
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
|
||||
|
||||
@@ -113,7 +117,7 @@ export function EventMessage({
|
||||
}
|
||||
|
||||
if (hasObservationPair && isOpenHandsAction(event)) {
|
||||
if (hasThoughtProperty(event.args)) {
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
@@ -209,11 +213,41 @@ export function EventMessage({
|
||||
);
|
||||
}
|
||||
|
||||
if (isTaskTrackingObservation(event)) {
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOpenHandsAction(event) && hasThoughtProperty(event.args) && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
|
||||
@@ -13,14 +13,16 @@ interface GenericEventMessageProps {
|
||||
title: React.ReactNode;
|
||||
details: string | React.ReactNode;
|
||||
success?: ObservationResultStatus;
|
||||
initiallyExpanded?: boolean;
|
||||
}
|
||||
|
||||
export function GenericEventMessage({
|
||||
title,
|
||||
details,
|
||||
success,
|
||||
initiallyExpanded = false,
|
||||
}: GenericEventMessageProps) {
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const [showDetails, setShowDetails] = React.useState(initiallyExpanded);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border-l-2 pl-2 my-2 py-2 border-neutral-300 text-sm w-full">
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TaskTrackingObservation } from "#/types/core/observations";
|
||||
|
||||
interface TaskTrackingObservationContentProps {
|
||||
event: TaskTrackingObservation;
|
||||
}
|
||||
|
||||
export function TaskTrackingObservationContent({
|
||||
event,
|
||||
}: TaskTrackingObservationContentProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { command, task_list: taskList } = event.extras;
|
||||
const shouldShowTaskList = command === "plan" && taskList.length > 0;
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "todo":
|
||||
return "⏳";
|
||||
case "in_progress":
|
||||
return "🔄";
|
||||
case "done":
|
||||
return "✅";
|
||||
default:
|
||||
return "❓";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClassName = (status: string) => {
|
||||
if (status === "done") {
|
||||
return "bg-green-800 text-green-200";
|
||||
}
|
||||
if (status === "in_progress") {
|
||||
return "bg-yellow-800 text-yellow-200";
|
||||
}
|
||||
return "bg-gray-700 text-gray-300";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Task List section - only show for 'plan' command */}
|
||||
{shouldShowTaskList && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-300">
|
||||
{t("TASK_TRACKING_OBSERVATION$TASK_LIST")} ({taskList.length}{" "}
|
||||
{taskList.length === 1 ? "item" : "items"})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<div className="space-y-3">
|
||||
{taskList.map((task, index) => (
|
||||
<div key={task.id} className="border-l-2 border-gray-600 pl-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">
|
||||
{getStatusIcon(task.status)}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm text-gray-400">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded uppercase font-semibold ${getStatusClassName(
|
||||
task.status,
|
||||
)}`}
|
||||
>
|
||||
{task.status.replace("_", " ")}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="font-medium text-white mb-1">
|
||||
{task.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
{t("TASK_TRACKING_OBSERVATION$TASK_ID")}: {task.id}
|
||||
</p>
|
||||
{task.notes && (
|
||||
<p className="text-sm text-gray-300 italic">
|
||||
{t("TASK_TRACKING_OBSERVATION$TASK_NOTES")}:{" "}
|
||||
{task.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result message - only show if there's meaningful content */}
|
||||
{event.content && event.content.trim() && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-300">
|
||||
{t("TASK_TRACKING_OBSERVATION$RESULT")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 shadow-inner">
|
||||
<pre className="whitespace-pre-wrap text-sm">
|
||||
{event.content.trim()}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export function LikertScale({
|
||||
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
|
||||
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
|
||||
t(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST),
|
||||
t(I18nKey.FEEDBACK$REASON_DIDNT_FINISH_JOB),
|
||||
t(I18nKey.FEEDBACK$REASON_OTHER),
|
||||
];
|
||||
|
||||
|
||||
@@ -37,8 +37,14 @@ export enum I18nKey {
|
||||
EVENT$UNKNOWN_EVENT = "EVENT$UNKNOWN_EVENT",
|
||||
OBSERVATION$COMMAND_NO_OUTPUT = "OBSERVATION$COMMAND_NO_OUTPUT",
|
||||
OBSERVATION$MCP_NO_OUTPUT = "OBSERVATION$MCP_NO_OUTPUT",
|
||||
OBSERVATION$TASK_TRACKING_NO_OUTPUT = "OBSERVATION$TASK_TRACKING_NO_OUTPUT",
|
||||
MCP_OBSERVATION$ARGUMENTS = "MCP_OBSERVATION$ARGUMENTS",
|
||||
MCP_OBSERVATION$OUTPUT = "MCP_OBSERVATION$OUTPUT",
|
||||
TASK_TRACKING_OBSERVATION$TASK_LIST = "TASK_TRACKING_OBSERVATION$TASK_LIST",
|
||||
TASK_TRACKING_OBSERVATION$OUTPUT = "TASK_TRACKING_OBSERVATION$OUTPUT",
|
||||
TASK_TRACKING_OBSERVATION$TASK_ID = "TASK_TRACKING_OBSERVATION$TASK_ID",
|
||||
TASK_TRACKING_OBSERVATION$TASK_NOTES = "TASK_TRACKING_OBSERVATION$TASK_NOTES",
|
||||
TASK_TRACKING_OBSERVATION$RESULT = "TASK_TRACKING_OBSERVATION$RESULT",
|
||||
OBSERVATION$ERROR_PREFIX = "OBSERVATION$ERROR_PREFIX",
|
||||
TASK$ADDRESSING_TASK = "TASK$ADDRESSING_TASK",
|
||||
SECRETS$SECRET_VALUE_REQUIRED = "SECRETS$SECRET_VALUE_REQUIRED",
|
||||
@@ -483,6 +489,7 @@ export enum I18nKey {
|
||||
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
|
||||
ACTION_MESSAGE$SYSTEM = "ACTION_MESSAGE$SYSTEM",
|
||||
ACTION_MESSAGE$CONDENSATION = "ACTION_MESSAGE$CONDENSATION",
|
||||
ACTION_MESSAGE$TASK_TRACKING = "ACTION_MESSAGE$TASK_TRACKING",
|
||||
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
|
||||
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
|
||||
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
|
||||
@@ -492,6 +499,8 @@ export enum I18nKey {
|
||||
OBSERVATION_MESSAGE$MCP = "OBSERVATION_MESSAGE$MCP",
|
||||
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
|
||||
OBSERVATION_MESSAGE$THINK = "OBSERVATION_MESSAGE$THINK",
|
||||
OBSERVATION_MESSAGE$TASK_TRACKING_PLAN = "OBSERVATION_MESSAGE$TASK_TRACKING_PLAN",
|
||||
OBSERVATION_MESSAGE$TASK_TRACKING_VIEW = "OBSERVATION_MESSAGE$TASK_TRACKING_VIEW",
|
||||
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
|
||||
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
|
||||
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
|
||||
@@ -651,6 +660,7 @@ export enum I18nKey {
|
||||
FEEDBACK$REASON_FORGOT_CONTEXT = "FEEDBACK$REASON_FORGOT_CONTEXT",
|
||||
FEEDBACK$REASON_UNNECESSARY_CHANGES = "FEEDBACK$REASON_UNNECESSARY_CHANGES",
|
||||
FEEDBACK$REASON_SHOULD_ASK_FIRST = "FEEDBACK$REASON_SHOULD_ASK_FIRST",
|
||||
FEEDBACK$REASON_DIDNT_FINISH_JOB = "FEEDBACK$REASON_DIDNT_FINISH_JOB",
|
||||
FEEDBACK$REASON_OTHER = "FEEDBACK$REASON_OTHER",
|
||||
FEEDBACK$THANK_YOU_FOR_FEEDBACK = "FEEDBACK$THANK_YOU_FOR_FEEDBACK",
|
||||
FEEDBACK$FAILED_TO_SUBMIT = "FEEDBACK$FAILED_TO_SUBMIT",
|
||||
|
||||
@@ -591,6 +591,22 @@
|
||||
"de": "[MCP-Tool wurde ohne Ausgabe ausgeführt]",
|
||||
"uk": "[Інструмент MCP завершив виконання без виводу]"
|
||||
},
|
||||
"OBSERVATION$TASK_TRACKING_NO_OUTPUT": {
|
||||
"en": "[Task tracking completed with no output]",
|
||||
"ja": "[タスクトラッキングは出力なしで完了しました]",
|
||||
"zh-CN": "[任务跟踪完成,没有输出]",
|
||||
"zh-TW": "[任務跟踪完成,沒有輸出]",
|
||||
"ko-KR": "[작업 추적이 출력 없이 완료되었습니다]",
|
||||
"no": "[Oppgavesporing fullført uten utdata]",
|
||||
"it": "[Tracciamento attività completato senza output]",
|
||||
"pt": "[Rastreamento de tarefas concluído sem saída]",
|
||||
"es": "[Seguimiento de tareas completado sin salida]",
|
||||
"ar": "[اكتمل تتبع المهام بدون مخرجات]",
|
||||
"fr": "[Suivi des tâches terminé sans sortie]",
|
||||
"tr": "[Görev takibi çıktı olmadan tamamlandı]",
|
||||
"de": "[Aufgabenverfolgung ohne Ausgabe abgeschlossen]",
|
||||
"uk": "[Відстеження завдань завершено без виводу]"
|
||||
},
|
||||
"MCP_OBSERVATION$ARGUMENTS": {
|
||||
"en": "Arguments",
|
||||
"ja": "引数",
|
||||
@@ -623,6 +639,86 @@
|
||||
"de": "Ausgabe",
|
||||
"uk": "Вивід"
|
||||
},
|
||||
"TASK_TRACKING_OBSERVATION$TASK_LIST": {
|
||||
"en": "Task List",
|
||||
"ja": "タスクリスト",
|
||||
"zh-CN": "任务列表",
|
||||
"zh-TW": "任務列表",
|
||||
"ko-KR": "작업 목록",
|
||||
"no": "Oppgaveliste",
|
||||
"it": "Elenco attività",
|
||||
"pt": "Lista de tarefas",
|
||||
"es": "Lista de tareas",
|
||||
"ar": "قائمة المهام",
|
||||
"fr": "Liste des tâches",
|
||||
"tr": "Görev listesi",
|
||||
"de": "Aufgabenliste",
|
||||
"uk": "Список завдань"
|
||||
},
|
||||
"TASK_TRACKING_OBSERVATION$OUTPUT": {
|
||||
"en": "Output",
|
||||
"ja": "出力",
|
||||
"zh-CN": "输出",
|
||||
"zh-TW": "輸出",
|
||||
"ko-KR": "출력",
|
||||
"no": "Utdata",
|
||||
"it": "Output",
|
||||
"pt": "Saída",
|
||||
"es": "Salida",
|
||||
"ar": "المخرجات",
|
||||
"fr": "Sortie",
|
||||
"tr": "Çıktı",
|
||||
"de": "Ausgabe",
|
||||
"uk": "Вивід"
|
||||
},
|
||||
"TASK_TRACKING_OBSERVATION$TASK_ID": {
|
||||
"en": "ID",
|
||||
"ja": "ID",
|
||||
"zh-CN": "ID",
|
||||
"zh-TW": "ID",
|
||||
"ko-KR": "ID",
|
||||
"no": "ID",
|
||||
"it": "ID",
|
||||
"pt": "ID",
|
||||
"es": "ID",
|
||||
"ar": "المعرف",
|
||||
"fr": "ID",
|
||||
"tr": "ID",
|
||||
"de": "ID",
|
||||
"uk": "ID"
|
||||
},
|
||||
"TASK_TRACKING_OBSERVATION$TASK_NOTES": {
|
||||
"en": "Notes",
|
||||
"ja": "メモ",
|
||||
"zh-CN": "备注",
|
||||
"zh-TW": "備註",
|
||||
"ko-KR": "메모",
|
||||
"no": "Notater",
|
||||
"it": "Note",
|
||||
"pt": "Notas",
|
||||
"es": "Notas",
|
||||
"ar": "ملاحظات",
|
||||
"fr": "Notes",
|
||||
"tr": "Notlar",
|
||||
"de": "Notizen",
|
||||
"uk": "Примітки"
|
||||
},
|
||||
"TASK_TRACKING_OBSERVATION$RESULT": {
|
||||
"en": "Result",
|
||||
"ja": "結果",
|
||||
"zh-CN": "结果",
|
||||
"zh-TW": "結果",
|
||||
"ko-KR": "결과",
|
||||
"no": "Resultat",
|
||||
"it": "Risultato",
|
||||
"pt": "Resultado",
|
||||
"es": "Resultado",
|
||||
"ar": "النتيجة",
|
||||
"fr": "Résultat",
|
||||
"tr": "Sonuç",
|
||||
"de": "Ergebnis",
|
||||
"uk": "Результат"
|
||||
},
|
||||
"OBSERVATION$ERROR_PREFIX": {
|
||||
"en": "error:",
|
||||
"ja": "エラー:",
|
||||
@@ -7727,6 +7823,22 @@
|
||||
"tr": "Yoğunlaşma",
|
||||
"uk": "Конденсація"
|
||||
},
|
||||
"ACTION_MESSAGE$TASK_TRACKING": {
|
||||
"en": "Managing tasks",
|
||||
"zh-CN": "管理任务",
|
||||
"zh-TW": "管理任務",
|
||||
"ko-KR": "작업 관리",
|
||||
"ja": "タスク管理",
|
||||
"no": "Administrerer oppgaver",
|
||||
"ar": "إدارة المهام",
|
||||
"de": "Aufgaben verwalten",
|
||||
"fr": "Gestion des tâches",
|
||||
"it": "Gestione delle attività",
|
||||
"pt": "Gerenciando tarefas",
|
||||
"es": "Gestionando tareas",
|
||||
"tr": "Görevleri yönetiyor",
|
||||
"uk": "Керування завданнями"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$RUN": {
|
||||
"en": "Ran <cmd>{{command}}</cmd>",
|
||||
"zh-CN": "运行 <cmd>{{command}}</cmd>",
|
||||
@@ -7871,6 +7983,38 @@
|
||||
"de": "Gedanke",
|
||||
"uk": "Думка"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$TASK_TRACKING_PLAN": {
|
||||
"en": "Agent updated the plan",
|
||||
"zh-CN": "代理更新了计划",
|
||||
"zh-TW": "代理更新了計劃",
|
||||
"ko-KR": "에이전트가 계획을 업데이트했습니다",
|
||||
"ja": "エージェントがプランを更新しました",
|
||||
"no": "Agent oppdaterte planen",
|
||||
"ar": "قام الوكيل بتحديث الخطة",
|
||||
"de": "Agent hat den Plan aktualisiert",
|
||||
"fr": "L'agent a mis à jour le plan",
|
||||
"it": "L'agente ha aggiornato il piano",
|
||||
"pt": "O agente atualizou o plano",
|
||||
"es": "El agente actualizó el plan",
|
||||
"tr": "Ajan planı güncelledi",
|
||||
"uk": "Агент оновив план"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$TASK_TRACKING_VIEW": {
|
||||
"en": "Agent checked the current plan",
|
||||
"zh-CN": "代理检查了当前计划",
|
||||
"zh-TW": "代理檢查了當前計劃",
|
||||
"ko-KR": "에이전트가 현재 계획을 확인했습니다",
|
||||
"ja": "エージェントが現在のプランを確認しました",
|
||||
"no": "Agent sjekket gjeldende plan",
|
||||
"ar": "تحقق الوكيل من الخطة الحالية",
|
||||
"de": "Agent hat den aktuellen Plan überprüft",
|
||||
"fr": "L'agent a vérifié le plan actuel",
|
||||
"it": "L'agente ha controllato il piano attuale",
|
||||
"pt": "O agente verificou o plano atual",
|
||||
"es": "El agente verificó el plan actual",
|
||||
"tr": "Ajan mevcut planı kontrol etti",
|
||||
"uk": "Агент перевірив поточний план"
|
||||
},
|
||||
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
|
||||
"en": "Show details",
|
||||
"zh-CN": "显示详情",
|
||||
@@ -10415,6 +10559,22 @@
|
||||
"de": "Der Agent hätte mich vorher fragen sollen!",
|
||||
"uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!"
|
||||
},
|
||||
"FEEDBACK$REASON_DIDNT_FINISH_JOB": {
|
||||
"en": "The agent didn't finish the job",
|
||||
"ja": "エージェントは作業を完了しませんでした",
|
||||
"zh-CN": "代理没有完成工作",
|
||||
"zh-TW": "代理沒有完成工作",
|
||||
"ko-KR": "에이전트가 작업을 완료하지 않았습니다",
|
||||
"no": "Agenten fullførte ikke jobben",
|
||||
"it": "L'agente non ha completato il lavoro",
|
||||
"pt": "O agente não terminou o trabalho",
|
||||
"es": "El agente no terminó el trabajo",
|
||||
"ar": "لم يكمل الوكيل المهمة",
|
||||
"fr": "L'agent n'a pas terminé le travail",
|
||||
"tr": "Ajan işi bitirmedi",
|
||||
"de": "Der Agent hat die Aufgabe nicht beendet",
|
||||
"uk": "Агент не завершив роботу"
|
||||
},
|
||||
"FEEDBACK$REASON_OTHER": {
|
||||
"en": "Other",
|
||||
"ja": "その他",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
|
||||
remote_runtime_resource_factor:
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens_set: { github: null, gitlab: null, bitbucket: null },
|
||||
provider_tokens_set: {},
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
enable_proactive_conversation_starters:
|
||||
|
||||
@@ -52,6 +52,7 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
case ObservationType.RECALL:
|
||||
case ObservationType.ERROR:
|
||||
case ObservationType.MCP:
|
||||
case ObservationType.TASK_TRACKING:
|
||||
break; // We don't display the default message for these observations
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -44,6 +44,9 @@ enum ActionType {
|
||||
|
||||
// Interact with the MCP server.
|
||||
MCP = "call_tool_mcp",
|
||||
|
||||
// Views or updates the task list for task management.
|
||||
TASK_TRACKING = "task_tracking",
|
||||
}
|
||||
|
||||
export default ActionType;
|
||||
|
||||
@@ -162,6 +162,21 @@ export interface MCPAction extends OpenHandsActionEvent<"call_tool_mcp"> {
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaskTrackingAction
|
||||
extends OpenHandsActionEvent<"task_tracking"> {
|
||||
source: "agent";
|
||||
args: {
|
||||
command: string;
|
||||
task_list: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
status: "todo" | "in_progress" | "done";
|
||||
notes?: string;
|
||||
}>;
|
||||
thought: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenHandsAction =
|
||||
| UserMessageAction
|
||||
| AssistantMessageAction
|
||||
@@ -178,4 +193,5 @@ export type OpenHandsAction =
|
||||
| FileWriteAction
|
||||
| RejectAction
|
||||
| RecallAction
|
||||
| MCPAction;
|
||||
| MCPAction
|
||||
| TaskTrackingAction;
|
||||
|
||||
@@ -18,6 +18,7 @@ export type OpenHandsEventType =
|
||||
| "recall"
|
||||
| "mcp"
|
||||
| "call_tool_mcp"
|
||||
| "task_tracking"
|
||||
| "user_rejected";
|
||||
|
||||
export type OpenHandsSourceType = "agent" | "user" | "environment";
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SystemMessageAction,
|
||||
CommandAction,
|
||||
FinishAction,
|
||||
TaskTrackingAction,
|
||||
} from "./actions";
|
||||
import {
|
||||
AgentStateChangeObservation,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
ErrorObservation,
|
||||
MCPObservation,
|
||||
OpenHandsObservation,
|
||||
TaskTrackingObservation,
|
||||
} from "./observations";
|
||||
import { StatusUpdate } from "./variances";
|
||||
|
||||
@@ -87,6 +89,16 @@ export const isMcpObservation = (
|
||||
): event is MCPObservation =>
|
||||
isOpenHandsObservation(event) && event.observation === "mcp";
|
||||
|
||||
export const isTaskTrackingAction = (
|
||||
event: OpenHandsParsedEvent,
|
||||
): event is TaskTrackingAction =>
|
||||
isOpenHandsAction(event) && event.action === "task_tracking";
|
||||
|
||||
export const isTaskTrackingObservation = (
|
||||
event: OpenHandsParsedEvent,
|
||||
): event is TaskTrackingObservation =>
|
||||
isOpenHandsObservation(event) && event.observation === "task_tracking";
|
||||
|
||||
export const isStatusUpdate = (event: unknown): event is StatusUpdate =>
|
||||
typeof event === "object" &&
|
||||
event !== null &&
|
||||
|
||||
@@ -146,6 +146,20 @@ export interface UserRejectedObservation
|
||||
extras: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TaskTrackingObservation
|
||||
extends OpenHandsObservationEvent<"task_tracking"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
command: string;
|
||||
task_list: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
status: "todo" | "in_progress" | "done";
|
||||
notes?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenHandsObservation =
|
||||
| AgentStateChangeObservation
|
||||
| AgentThinkObservation
|
||||
@@ -160,4 +174,5 @@ export type OpenHandsObservation =
|
||||
| ErrorObservation
|
||||
| RecallObservation
|
||||
| MCPObservation
|
||||
| UserRejectedObservation;
|
||||
| UserRejectedObservation
|
||||
| TaskTrackingObservation;
|
||||
|
||||
@@ -40,6 +40,9 @@ enum ObservationType {
|
||||
|
||||
// A no-op observation
|
||||
NULL = "null",
|
||||
|
||||
// Result of a task tracking operation
|
||||
TASK_TRACKING = "task_tracking",
|
||||
}
|
||||
|
||||
export default ObservationType;
|
||||
|
||||
@@ -11,20 +11,23 @@ export const browserTab = {
|
||||
startNotification(message: string) {
|
||||
if (!isBrowser) return;
|
||||
|
||||
// Store original title if not already stored
|
||||
if (!originalTitle) {
|
||||
originalTitle = document.title;
|
||||
}
|
||||
// Always capture the current title as the baseline to restore to
|
||||
originalTitle = document.title;
|
||||
|
||||
// Clear any existing interval
|
||||
if (titleInterval) {
|
||||
this.stopNotification();
|
||||
}
|
||||
|
||||
// Alternate between original title and notification message
|
||||
// Alternate between the latest baseline title and the notification message.
|
||||
// If the title changes externally (e.g., user renames conversation),
|
||||
// update the baseline so we restore to the new value when stopping.
|
||||
titleInterval = window.setInterval(() => {
|
||||
document.title =
|
||||
document.title === originalTitle ? message : originalTitle;
|
||||
const current = document.title;
|
||||
if (current !== originalTitle && current !== message) {
|
||||
originalTitle = current;
|
||||
}
|
||||
document.title = current === message ? originalTitle : message;
|
||||
}, 1000);
|
||||
|
||||
// Set favicon to indicate notification
|
||||
|
||||
@@ -47,8 +47,8 @@ Then, for each issue you find:
|
||||
- Suggest a concrete improvement
|
||||
|
||||
Use the following structure in your output:
|
||||
[Line 42] :hammer_and_wrench: Unused import: The 'os' module is imported but never used. Remove it to clean up the code.
|
||||
[Lines 78–85] :mag: Readability: This nested if-else block is hard to follow. Consider refactoring into smaller functions or using early returns.
|
||||
[Line 102] :closed_lock_with_key: Security Risk: User input is directly concatenated into an SQL query. This could allow SQL injection. Use parameterized queries instead.
|
||||
[src/utils.py, Line 42] :hammer_and_wrench: Unused import: The 'os' module is imported but never used. Remove it to clean up the code.
|
||||
[src/database.py, Lines 78–85] :mag: Readability: This nested if-else block is hard to follow. Consider refactoring into smaller functions or using early returns.
|
||||
[src/auth.py, Line 102] :closed_lock_with_key: Security Risk: User input is directly concatenated into an SQL query. This could allow SQL injection. Use parameterized queries instead.
|
||||
|
||||
REMEMBER, DO NOT MODIFY THE CODE. ONLY PROVIDE FEEDBACK IN YOUR RESPONSE.
|
||||
|
||||
104
microagents/codereview-roasted.md
Normal file
104
microagents/codereview-roasted.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
triggers:
|
||||
- /codereview-roasted
|
||||
---
|
||||
|
||||
PERSONA:
|
||||
You are a critical code reviewer with the engineering mindset of Linus Torvalds. Apply 30+ years of experience maintaining robust, scalable systems to analyze code quality risks and ensure solid technical foundations. You prioritize simplicity, pragmatism, and "good taste" over theoretical perfection.
|
||||
|
||||
CORE PHILOSOPHY:
|
||||
1. **"Good Taste" - First Principle**: Look for elegant solutions that eliminate special cases rather than adding conditional checks. Good code has no edge cases.
|
||||
2. **"Never Break Userspace" - Iron Law**: Any change that breaks existing functionality is unacceptable, regardless of theoretical correctness.
|
||||
3. **Pragmatism**: Solve real problems, not imaginary ones. Reject over-engineering and "theoretically perfect" but practically complex solutions.
|
||||
4. **Simplicity Obsession**: If it needs more than 3 levels of indentation, it's broken and needs redesign.
|
||||
|
||||
CRITICAL ANALYSIS FRAMEWORK:
|
||||
|
||||
Before reviewing, ask Linus's Three Questions:
|
||||
1. Is this solving a real problem or an imagined one?
|
||||
2. Is there a simpler way?
|
||||
3. What will this break?
|
||||
|
||||
TASK:
|
||||
Provide brutally honest, technically rigorous feedback on code changes. Be direct and critical while remaining constructive. Focus on fundamental engineering principles over style preferences. DO NOT modify the code; only provide specific, actionable feedback.
|
||||
|
||||
CODE REVIEW SCENARIOS:
|
||||
|
||||
1. **Data Structure Analysis** (Highest Priority)
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
Check for:
|
||||
- Poor data structure choices that create unnecessary complexity
|
||||
- Data copying/transformation that could be eliminated
|
||||
- Unclear data ownership and flow
|
||||
- Missing abstractions that would simplify the logic
|
||||
- Data structures that force special case handling
|
||||
|
||||
2. **Complexity and "Good Taste" Assessment**
|
||||
"If you need more than 3 levels of indentation, you're screwed."
|
||||
Identify:
|
||||
- Functions with >3 levels of nesting (immediate red flag)
|
||||
- Special cases that could be eliminated with better design
|
||||
- Functions doing multiple things (violating single responsibility)
|
||||
- Complex conditional logic that obscures the core algorithm
|
||||
- Code that could be 3 lines instead of 10
|
||||
|
||||
3. **Pragmatic Problem Analysis**
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
Evaluate:
|
||||
- Is this solving a problem that actually exists in production?
|
||||
- Does the solution's complexity match the problem's severity?
|
||||
- Are we over-engineering for theoretical edge cases?
|
||||
- Could this be solved with existing, simpler mechanisms?
|
||||
|
||||
4. **Breaking Change Risk Assessment**
|
||||
"We don't break user space!"
|
||||
Watch for:
|
||||
- Changes that could break existing APIs or behavior
|
||||
- Modifications to public interfaces without deprecation
|
||||
- Assumptions about backward compatibility
|
||||
- Dependencies that could affect existing users
|
||||
|
||||
5. **Security and Correctness** (Critical Issues Only)
|
||||
Focus on real security risks, not theoretical ones:
|
||||
- Actual input validation failures with exploit potential
|
||||
- Real privilege escalation or data exposure risks
|
||||
- Memory safety issues in unsafe languages
|
||||
- Concurrency bugs that cause data corruption
|
||||
|
||||
CRITICAL REVIEW OUTPUT FORMAT:
|
||||
|
||||
Start with a **Taste Rating**:
|
||||
🟢 **Good taste** - Elegant, simple solution
|
||||
🟡 **Acceptable** - Works but could be cleaner
|
||||
🔴 **Needs improvement** - Violates fundamental principles
|
||||
|
||||
Then provide **Linus-Style Analysis**:
|
||||
|
||||
**[CRITICAL ISSUES]** (Must fix - these break fundamental principles)
|
||||
- [src/core.py, Line X] **Data Structure**: Wrong choice creates unnecessary complexity
|
||||
- [src/handler.py, Line Y] **Complexity**: >3 levels of nesting - redesign required
|
||||
- [src/api.py, Line Z] **Breaking Change**: This will break existing functionality
|
||||
|
||||
**[IMPROVEMENT OPPORTUNITIES]** (Should fix - violates good taste)
|
||||
- [src/utils.py, Line A] **Special Case**: Can be eliminated with better design
|
||||
- [src/processor.py, Line B] **Simplification**: These 10 lines can be 3
|
||||
- [src/feature.py, Line C] **Pragmatism**: Solving imaginary problem, focus on real issues
|
||||
|
||||
**[STYLE NOTES]** (Minor - only mention if genuinely important)
|
||||
- [src/models.py, Line D] **Naming**: Unclear intent, affects maintainability
|
||||
|
||||
**VERDICT:**
|
||||
✅ **Worth merging**: Core logic is sound, minor improvements suggested
|
||||
❌ **Needs rework**: Fundamental design issues must be addressed first
|
||||
|
||||
**KEY INSIGHT:**
|
||||
[One sentence summary of the most important architectural observation]
|
||||
|
||||
COMMUNICATION STYLE:
|
||||
- Be direct and technically precise
|
||||
- Focus on engineering fundamentals, not personal preferences
|
||||
- Explain the "why" behind each criticism
|
||||
- Suggest concrete, actionable improvements
|
||||
- Prioritize issues that affect real users over theoretical concerns
|
||||
|
||||
REMEMBER: DO NOT MODIFY THE CODE. PROVIDE CRITICAL BUT CONSTRUCTIVE FEEDBACK ONLY.
|
||||
@@ -26,6 +26,7 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
* Use the `create_pr` tool to create a pull request, if you haven't already
|
||||
* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* If you need to add labels when opening a PR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
|
||||
* After opening or updating a pull request, send the user a short message with a link to the pull request.
|
||||
* Do NOT mark a pull request as ready to review unless the user explicitly says so
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
|
||||
@@ -26,6 +26,7 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
* Use the `create_mr` tool to create a merge request, if you haven't already
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* If you need to add labels when opening a MR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
|
||||
* After opening or updating a merge request, send the user a short message with a link to the merge request.
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
```bash
|
||||
|
||||
@@ -18,7 +18,7 @@ from openhands.events.action import (
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.observation import BrowserOutputObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
from openhands.runtime.plugins import (
|
||||
PluginRequirement,
|
||||
)
|
||||
@@ -102,15 +102,15 @@ class BrowsingAgent(Agent):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
llm_registry: LLMRegistry,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the BrowsingAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
super().__init__(config, llm_registry)
|
||||
# define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
|
||||
# see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
|
||||
action_subsets = ['chat', 'bid']
|
||||
|
||||
@@ -3,6 +3,8 @@ import sys
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
@@ -21,6 +23,9 @@ from openhands.agenthub.codeact_agent.tools.llm_based_edit import LLMBasedFileEd
|
||||
from openhands.agenthub.codeact_agent.tools.str_replace_editor import (
|
||||
create_str_replace_editor_tool,
|
||||
)
|
||||
from openhands.agenthub.codeact_agent.tools.task_tracker import (
|
||||
create_task_tracker_tool,
|
||||
)
|
||||
from openhands.agenthub.codeact_agent.tools.think import ThinkTool
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
@@ -29,7 +34,6 @@ from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.message import Message
|
||||
from openhands.events.action import AgentFinishAction, MessageAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.llm_utils import check_tools
|
||||
from openhands.memory.condenser import Condenser
|
||||
from openhands.memory.condenser.condenser import Condensation, View
|
||||
@@ -71,18 +75,13 @@ class CodeActAgent(Agent):
|
||||
JupyterRequirement(),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
) -> None:
|
||||
def __init__(self, config: AgentConfig, llm_registry: LLMRegistry) -> None:
|
||||
"""Initializes a new instance of the CodeActAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- config (AgentConfig): The configuration for this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
super().__init__(config, llm_registry)
|
||||
self.pending_actions: deque['Action'] = deque()
|
||||
self.reset()
|
||||
self.tools = self._get_tools()
|
||||
@@ -90,7 +89,7 @@ class CodeActAgent(Agent):
|
||||
# Create a ConversationMemory instance
|
||||
self.conversation_memory = ConversationMemory(self.config, self.prompt_manager)
|
||||
|
||||
self.condenser = Condenser.from_config(self.config.condenser)
|
||||
self.condenser = Condenser.from_config(self.config.condenser, llm_registry)
|
||||
logger.debug(f'Using condenser: {type(self.condenser)}')
|
||||
|
||||
@property
|
||||
@@ -98,7 +97,7 @@ class CodeActAgent(Agent):
|
||||
if self._prompt_manager is None:
|
||||
self._prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
system_prompt_filename=self.config.system_prompt_filename,
|
||||
system_prompt_filename=self.config.resolved_system_prompt_filename,
|
||||
)
|
||||
|
||||
return self._prompt_manager
|
||||
@@ -136,6 +135,9 @@ class CodeActAgent(Agent):
|
||||
tools.append(BrowserTool)
|
||||
if self.config.enable_jupyter:
|
||||
tools.append(IPythonTool)
|
||||
if self.config.enable_plan_mode:
|
||||
# In plan mode, we use the task_tracker tool for task management
|
||||
tools.append(create_task_tracker_tool(use_short_tool_desc))
|
||||
if self.config.enable_llm_editor:
|
||||
tools.append(LLMBasedFileEditTool)
|
||||
elif self.config.enable_editor:
|
||||
|
||||
@@ -35,11 +35,13 @@ from openhands.events.action import (
|
||||
FileReadAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
TaskTrackingAction,
|
||||
)
|
||||
from openhands.events.action.agent import CondensationRequestAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
from openhands.llm.tool_names import TASK_TRACKER_TOOL_NAME
|
||||
|
||||
|
||||
def combine_thought(action: Action, thought: str) -> Action:
|
||||
@@ -220,6 +222,24 @@ def response_to_actions(
|
||||
)
|
||||
action = BrowseInteractiveAction(browser_actions=arguments['code'])
|
||||
|
||||
# ================================================
|
||||
# TaskTrackingAction
|
||||
# ================================================
|
||||
elif tool_call.function.name == TASK_TRACKER_TOOL_NAME:
|
||||
if 'command' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "command" in tool call {tool_call.function.name}'
|
||||
)
|
||||
if arguments['command'] == 'plan' and 'task_list' not in arguments:
|
||||
raise FunctionCallValidationError(
|
||||
f'Missing required argument "task_list" for "plan" command in tool call {tool_call.function.name}'
|
||||
)
|
||||
|
||||
action = TaskTrackingAction(
|
||||
command=arguments['command'],
|
||||
task_list=arguments.get('task_list', []),
|
||||
)
|
||||
|
||||
# ================================================
|
||||
# MCPAction (MCP)
|
||||
# ================================================
|
||||
|
||||
@@ -98,3 +98,11 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
- Confirm whether they want it as a separate file or just in the conversation
|
||||
- Ask if they want documentation files to be included in version control
|
||||
</DOCUMENTATION>
|
||||
|
||||
<PROCESS_MANAGEMENT>
|
||||
* When terminating processes:
|
||||
- Do NOT use general keywords with commands like `pkill -f server` or `pkill -f python` as this might accidentally kill other important servers or processes
|
||||
- Always use specific keywords that uniquely identify the target process
|
||||
- Prefer using `ps aux` to find the exact process ID (PID) first, then kill that specific PID
|
||||
- When possible, use more targeted approaches like finding the PID from a pidfile or using application-specific shutdown commands
|
||||
</PROCESS_MANAGEMENT>
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
{% include "system_prompt.j2" %}
|
||||
|
||||
<TASK_MANAGEMENT>
|
||||
* For complex, long-horizon tasks, create a TODO.md file to track progress:
|
||||
1. Start by creating a detailed plan in TODO.md with clear steps
|
||||
2. Check TODO.md before each new action to maintain context and track progress
|
||||
3. Update TODO.md as you complete steps or discover new requirements
|
||||
4. Mark completed items with ✓ or [x] to maintain a clear record of progress
|
||||
5. For each major step, add sub-tasks as needed to break down complex work
|
||||
6. If you discover the plan needs significant changes, propose updates and confirm with the user before proceeding and update TODO.md
|
||||
7. IMPORTANT: Do NOT add TODO.md to git commits or version control systems
|
||||
|
||||
* Example TODO.md format:
|
||||
```markdown
|
||||
# Task: [Brief description of the overall task]
|
||||
|
||||
## Plan
|
||||
- [ ] Step 1: [Description]
|
||||
- [ ] Sub-task 1.1
|
||||
- [ ] Sub-task 1.2
|
||||
- [ ] Step 2: [Description]
|
||||
- [x] Step 3: [Description] (Completed)
|
||||
|
||||
## Notes
|
||||
- Important discovery: [Details about something you learned]
|
||||
- Potential issue: [Description of a potential problem]
|
||||
```
|
||||
|
||||
* When working on a task:
|
||||
- Read the README to understand how the system works
|
||||
- Create TODO.md with every major step unchecked
|
||||
- Add TODO.md to .gitignore if it's not already ignored
|
||||
- Until every item in TODO.md is checked:
|
||||
a. Pick the next unchecked item and work on it
|
||||
b. Run appropriate tests to verify your work
|
||||
c. If issues arise, fix them until tests pass
|
||||
d. Once complete, check off the item in TODO.md
|
||||
e. Proceed to the next unchecked item
|
||||
* You have access to the `task_tracker` tool to help you organize and monitor development work. Use this tool REGULARLY to maintain task visibility and provide users with clear progress updates. This tool is ESSENTIAL for systematic planning and decomposing complex development work into manageable components. Failing to use this tool for planning may result in overlooked requirements - which is unacceptable.
|
||||
* It is crucial that you update task status to "done" immediately upon completion of each work item. Do not accumulate multiple finished tasks before updating their status.
|
||||
* For complex, multi-phase development work, use `task_tracker` to establish a comprehensive plan with well-defined steps:
|
||||
1. Begin by decomposing the overall objective into primary phases using `task_tracker`
|
||||
2. Include detailed work items as necessary to break complex activities into actionable units
|
||||
3. Update tasks to "in_progress" status when commencing work on them
|
||||
4. Update tasks to "done" status immediately after completing each item
|
||||
5. For each primary phase, incorporate additional work items as you identify new requirements
|
||||
6. If you determine the plan requires substantial modifications, suggest revisions and obtain user confirmation before proceeding
|
||||
* Example workflow for debugging and resolution:
|
||||
```
|
||||
User: "Execute the test suite and resolve any validation failures"
|
||||
Assistant: I'm going to use the task_tracker tool to organize the following work items:
|
||||
- Execute the test suite
|
||||
- Resolve any validation failures
|
||||
I'm now going to run the test suite using the terminal.
|
||||
[After running tests and discovering 8 validation failures]
|
||||
I found 8 validation failures that need attention. I'm going to use the task_tracker tool to add 8 specific items to the task list.
|
||||
[Updating first task to in_progress]
|
||||
Let me begin addressing the first validation issue...
|
||||
[After resolving first failure]
|
||||
The first validation issue has been resolved, let me mark that task as done and proceed to the second item...
|
||||
```
|
||||
* Example workflow for component development:
|
||||
```
|
||||
User: "Build a dashboard component that displays analytics data with interactive charts and filtering options"
|
||||
Assistant: I'll help you create an analytics dashboard with interactive charts and filtering. Let me first use the task_tracker tool to organize this development work.
|
||||
Adding the following tasks to the tracker:
|
||||
1. Analyze existing analytics data structure and requirements
|
||||
2. Design dashboard layout and component architecture
|
||||
3. Implement data visualization charts with interactivity
|
||||
4. Create filtering and search functionality
|
||||
5. Integrate components and perform testing
|
||||
Let me start by examining the current analytics data structure to understand what we're working with...
|
||||
[Assistant proceeds with implementation step by step, updating tasks to in_progress and done as work progresses]
|
||||
```
|
||||
</TASK_MANAGEMENT>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
{% include "system_prompt.j2" %}
|
||||
|
||||
<TECHNICAL_PHILOSOPHY>
|
||||
|
||||
Adopt the engineering mindset of Linus Torvalds, creator and chief architect of the Linux kernel. Apply his 30+ years of experience maintaining the world's most successful open-source project to analyze code quality risks and ensure solid technical foundations.
|
||||
|
||||
# My Core Philosophy
|
||||
|
||||
1. "Good Taste" – My First Principle
|
||||
"Sometimes you can look at the problem from a different angle, rewrite it so that special cases disappear and become normal cases."
|
||||
• Classic case: linked list deletion — optimized from 10 lines with if checks to 4 lines with unconditional branches
|
||||
• Good taste is an intuition built from experience
|
||||
• Eliminating edge cases is always better than adding conditional checks
|
||||
|
||||
2. "Never break userspace" – My Iron Law
|
||||
"We don't break user space!"
|
||||
• Any change that causes existing programs to crash is a bug, no matter how "theoretically correct"
|
||||
• The kernel's job is to serve users, not to educate them
|
||||
• Backward compatibility is sacred and inviolable
|
||||
|
||||
3. Pragmatism – My Belief
|
||||
"I'm a damn pragmatist."
|
||||
• Solve real problems, not imaginary threats
|
||||
• Reject "theoretically perfect" but practically complex solutions like microkernels
|
||||
• Code should serve reality, not academic papers
|
||||
|
||||
4. Obsession with Simplicity – My Standard
|
||||
"If you need more than three levels of indentation, you're screwed and should fix your program."
|
||||
• Functions must be short and do one thing well
|
||||
• C is a Spartan language, naming should be equally concise
|
||||
• Complexity is the root of all evil
|
||||
|
||||
# Communication Principles
|
||||
|
||||
Basic Communication Rules
|
||||
• Style: Direct, clear, and constructive. Focus on technical improvements rather than judgmental language.
|
||||
• Technical Priority: Provide specific, actionable feedback on technical issues. Maintain high standards while being respectful and educational.
|
||||
|
||||
# Requirement Confirmation Process
|
||||
|
||||
## 0. Premise Thinking – Linus's Three Questions
|
||||
|
||||
Before any analysis, ask yourself:
|
||||
|
||||
1. Is this a real problem or an imagined one? – Reject over-engineering
|
||||
2. Is there a simpler way? – Always seek the simplest solution
|
||||
3. What will it break? – Backward compatibility is law
|
||||
|
||||
## 1. Requirement Understanding Confirmation
|
||||
|
||||
Once you understand the user’s requirement, reply it in Linus’s style to confirm:
|
||||
> Based on current information, my understanding of your requirement is: [Restate the requirement using Linus’s thinking and communication style]
|
||||
> Please confirm if my understanding is correct.
|
||||
|
||||
## 2. Linus-Style Problem Decomposition
|
||||
|
||||
### First Layer: Data Structure Analysis
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
• What are the core data elements? How are they related?
|
||||
• Where does the data flow? Who owns it? Who modifies it?
|
||||
• Any unnecessary data copying or transformation?
|
||||
|
||||
### Second Layer: Special Case Identification
|
||||
"Good code has no special cases"
|
||||
• Identify all if/else branches
|
||||
• Which are real business logic? Which are patches for bad design?
|
||||
• Can the data structure be redesigned to remove these branches?
|
||||
|
||||
### Third Layer: Complexity Review
|
||||
"If it needs more than 3 levels of indentation, redesign it"
|
||||
• What is the essence of the feature? (One sentence)
|
||||
• How many concepts does the current solution use?
|
||||
• Can it be reduced by half? Then by half again?
|
||||
|
||||
### Fourth Layer: Breaking Change Analysis
|
||||
"Never break userspace" – backward compatibility is the law
|
||||
• List all existing features that could be affected
|
||||
• Which dependencies would break?
|
||||
• How can we improve without breaking anything?
|
||||
|
||||
### Fifth Layer: Practicality Verification
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
• Does this problem actually exist in production?
|
||||
• How many users are truly affected?
|
||||
• Does the solution's complexity match the problem's severity?
|
||||
|
||||
## 3. Decision Output Format
|
||||
After the 5-layer analysis, output must include:
|
||||
|
||||
[Core Judgment]
|
||||
✅ Worth doing: [reason] / ❌ Not worth doing: [reason]
|
||||
|
||||
[Key Insights]
|
||||
- Data Structure: [most critical data relationship]
|
||||
- Complexity: [complexity that can be eliminated]
|
||||
- Risk: [biggest breaking change risk]
|
||||
|
||||
[Linus-Style Plan]
|
||||
If worth doing:
|
||||
1. Always start by simplifying the data structure
|
||||
2. Eliminate all special cases
|
||||
3. Implement in the dumbest but clearest way
|
||||
4. Ensure zero breaking changes
|
||||
|
||||
If not worth doing, explain to the user:
|
||||
"This is solving a problem that doesn’t exist. The real problem is [XXX]."
|
||||
|
||||
## 4. Code Review Output
|
||||
When seeing code, make three quick judgments:
|
||||
|
||||
[Taste Rating]
|
||||
🟢 Good taste / 🟡 Acceptable / 🔴 Needs improvement
|
||||
|
||||
[Critical Issue]
|
||||
- [If any, directly point out the worst part]
|
||||
|
||||
[Improvement Direction]
|
||||
"Eliminate this special case"
|
||||
"These 10 lines can be 3"
|
||||
"Wrong data structure, should be..."
|
||||
|
||||
</TECHNICAL_PHILOSOPHY>
|
||||
203
openhands/agenthub/codeact_agent/tools/task_tracker.py
Normal file
203
openhands/agenthub/codeact_agent/tools/task_tracker.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
from openhands.llm.tool_names import TASK_TRACKER_TOOL_NAME
|
||||
|
||||
_DETAILED_TASK_TRACKER_DESCRIPTION = """This tool provides structured task management capabilities for development workflows.
|
||||
It enables systematic tracking of work items, progress monitoring, and efficient
|
||||
organization of complex development activities.
|
||||
|
||||
The tool maintains visibility into project status and helps communicate
|
||||
progress effectively to users.
|
||||
|
||||
## Application Guidelines
|
||||
|
||||
Utilize this tool in the following situations:
|
||||
|
||||
1. Multi-phase development work - When projects involve multiple sequential or
|
||||
parallel activities
|
||||
2. Complex implementation tasks - Work requiring systematic planning and
|
||||
coordination across multiple components
|
||||
3. Explicit user request for task organization - When users specifically ask
|
||||
for structured task management
|
||||
4. Multiple concurrent requirements - When users present several work items
|
||||
that need coordination
|
||||
5. Project initiation - Capture and organize user requirements at project start
|
||||
6. Work commencement - Update task status to in_progress before beginning
|
||||
implementation. Maintain focus by limiting active work to one task
|
||||
7. Task completion - Update status to done and identify any additional work
|
||||
that emerged during implementation
|
||||
|
||||
## Situations Where Tool Usage Is Unnecessary
|
||||
|
||||
Avoid using this tool when:
|
||||
|
||||
1. Single atomic tasks that require no decomposition
|
||||
2. Trivial operations where tracking adds no organizational value
|
||||
3. Simple activities completable in minimal steps
|
||||
4. Pure information exchange or discussion
|
||||
|
||||
Note: For single straightforward tasks, proceed with direct implementation
|
||||
rather than creating tracking overhead.
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
**Scenario A: Feature Development with Validation**
|
||||
User request: "Build a user authentication system with login/logout functionality.
|
||||
Don't forget to include input validation and error handling!"
|
||||
|
||||
Response approach: I'll implement a user authentication system with comprehensive
|
||||
validation. Let me organize this work systematically.
|
||||
|
||||
*Task breakdown includes:*
|
||||
1. Design authentication API endpoints and data models
|
||||
2. Implement user login/logout controllers with session management
|
||||
3. Add client-side form validation and error display
|
||||
4. Create backend input sanitization and security checks
|
||||
5. Implement comprehensive error handling for edge cases
|
||||
6. Perform integration testing and fix any issues
|
||||
|
||||
**Scenario B: Codebase Refactoring**
|
||||
User request: "I need to update all instances of the method 'fetchData' to
|
||||
'retrieveInformation' throughout the entire project"
|
||||
|
||||
Response approach: Let me first analyze your codebase to identify all locations
|
||||
where 'fetchData' appears.
|
||||
|
||||
*After code analysis*
|
||||
|
||||
I've located 12 occurrences of 'fetchData' across 6 files in your project.
|
||||
I'll create a systematic plan to update these references.
|
||||
|
||||
*Organized task list includes specific file-by-file updates*
|
||||
|
||||
**Scenario C: Multi-feature Development**
|
||||
User request: "Set up a blog platform with these components: admin dashboard,
|
||||
article management, comment system, and user profiles."
|
||||
|
||||
Response approach: I'll help build your blog platform. Let me break down these
|
||||
components into manageable implementation phases.
|
||||
|
||||
*Creates structured plan with each feature decomposed into specific development tasks*
|
||||
|
||||
## Counter-examples - Direct Implementation Preferred
|
||||
|
||||
**Counter-example A: Simple inquiry**
|
||||
User request: "What's the syntax for a for loop in JavaScript?"
|
||||
|
||||
Response approach: Here's how to write a for loop in JavaScript:
|
||||
|
||||
```javascript
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
console.log(array[i]);
|
||||
}
|
||||
```
|
||||
|
||||
*No task tracking needed - single informational response*
|
||||
|
||||
**Counter-example B: Minor modification**
|
||||
User request: "Please add a docstring to the processPayment function explaining
|
||||
its parameters."
|
||||
|
||||
Response approach: I'll add a docstring to explain the processPayment function.
|
||||
|
||||
*Direct edit without task tracking for simple modification*
|
||||
|
||||
## Status Management and Workflow
|
||||
|
||||
1. **Status Values**: Track work using these states:
|
||||
- todo: Not yet initiated
|
||||
- in_progress: Currently active (maintain single focus)
|
||||
- done: Successfully completed
|
||||
|
||||
2. **Workflow Practices**:
|
||||
- Update status dynamically as work progresses
|
||||
- Mark completion immediately upon task finish
|
||||
- Limit active work to ONE task at any given time
|
||||
- Complete current activities before initiating new ones
|
||||
- Remove obsolete tasks from tracking entirely
|
||||
|
||||
3. **Completion Criteria**:
|
||||
- Mark tasks as done only when fully achieved
|
||||
- Keep status as in_progress if errors, blocks, or partial completion exist
|
||||
- Create new tasks for discovered issues or dependencies
|
||||
- Never mark done when:
|
||||
- Test suites are failing
|
||||
- Implementation remains incomplete
|
||||
- Unresolved errors persist
|
||||
- Required resources are unavailable
|
||||
|
||||
4. **Task Organization**:
|
||||
- Write precise, actionable descriptions
|
||||
- Decompose complex work into manageable units
|
||||
- Use descriptive, clear naming conventions
|
||||
|
||||
When uncertain, favor using this tool. Proactive task management demonstrates
|
||||
systematic approach and ensures comprehensive requirement fulfillment.
|
||||
"""
|
||||
|
||||
_SHORT_TASK_TRACKER_DESCRIPTION = """Provides structured task management for development workflows, enabling progress
|
||||
tracking and systematic organization of complex coding activities.
|
||||
|
||||
* Apply to multi-phase projects (3+ distinct steps) or when managing multiple user requirements
|
||||
* Update status (todo/in_progress/done) dynamically throughout work
|
||||
* Maintain single active task focus at any time
|
||||
* Mark completion immediately upon task finish
|
||||
* Decompose complex work into manageable, actionable units
|
||||
"""
|
||||
|
||||
|
||||
def create_task_tracker_tool(
|
||||
use_short_description: bool = False,
|
||||
) -> ChatCompletionToolParam:
|
||||
description = (
|
||||
_SHORT_TASK_TRACKER_DESCRIPTION
|
||||
if use_short_description
|
||||
else _DETAILED_TASK_TRACKER_DESCRIPTION
|
||||
)
|
||||
return ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name=TASK_TRACKER_TOOL_NAME,
|
||||
description=description,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'command': {
|
||||
'type': 'string',
|
||||
'enum': ['view', 'plan'],
|
||||
'description': 'The command to execute. `view` shows the current task list. `plan` creates or updates the task list based on provided requirements and progress. Always `view` the current list before making changes.',
|
||||
},
|
||||
'task_list': {
|
||||
'type': 'array',
|
||||
'description': 'The full task list. Required parameter of `plan` command.',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'Unique task identifier',
|
||||
},
|
||||
'title': {
|
||||
'type': 'string',
|
||||
'description': 'Brief task description',
|
||||
},
|
||||
'status': {
|
||||
'type': 'string',
|
||||
'description': 'Current task status',
|
||||
'enum': ['todo', 'in_progress', 'done'],
|
||||
},
|
||||
'notes': {
|
||||
'type': 'string',
|
||||
'description': 'Optional additional context or details',
|
||||
},
|
||||
},
|
||||
'required': ['title', 'status', 'id'],
|
||||
'additionalProperties': False,
|
||||
},
|
||||
},
|
||||
},
|
||||
'required': ['command'],
|
||||
'additionalProperties': False,
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -22,7 +22,7 @@ from openhands.events.observation import (
|
||||
Observation,
|
||||
)
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
|
||||
"""
|
||||
FIXME: There are a few problems this surfaced
|
||||
@@ -42,8 +42,12 @@ class DummyAgent(Agent):
|
||||
without making any LLM calls.
|
||||
"""
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
super().__init__(llm, config)
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentConfig,
|
||||
llm_registry: LLMRegistry,
|
||||
):
|
||||
super().__init__(config, llm_registry)
|
||||
self.steps: list[ActionObs] = [
|
||||
{
|
||||
'action': MessageAction('Time to get started!'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import openhands.agenthub.loc_agent.function_calling as locagent_function_callin
|
||||
from openhands.agenthub.codeact_agent import CodeActAgent
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.events.action import Action
|
||||
@@ -16,8 +16,8 @@ class LocAgent(CodeActAgent):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
llm_registry: LLMRegistry,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the LocAgent class.
|
||||
|
||||
@@ -25,7 +25,8 @@ class LocAgent(CodeActAgent):
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- config (AgentConfig): The configuration for the agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
|
||||
super().__init__(config, llm_registry)
|
||||
|
||||
self.tools = locagent_function_calling.get_tools()
|
||||
logger.debug(
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm import ChatCompletionToolParam
|
||||
|
||||
@@ -15,7 +17,6 @@ from openhands.agenthub.readonly_agent import (
|
||||
)
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
|
||||
@@ -37,17 +38,16 @@ class ReadOnlyAgent(CodeActAgent):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
llm_registry: LLMRegistry,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the ReadOnlyAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- config (AgentConfig): The configuration for this agent
|
||||
"""
|
||||
# Initialize the CodeActAgent class; some of it is overridden with class methods
|
||||
super().__init__(llm, config)
|
||||
super().__init__(config, llm_registry)
|
||||
|
||||
logger.debug(
|
||||
f'TOOLS loaded for ReadOnlyAgent: {", ".join([tool.get("function").get("name") for tool in self.tools])}'
|
||||
|
||||
@@ -16,7 +16,7 @@ from openhands.events.action import (
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.observation import BrowserOutputObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
from openhands.runtime.plugins import (
|
||||
PluginRequirement,
|
||||
)
|
||||
@@ -127,17 +127,13 @@ class VisualBrowsingAgent(Agent):
|
||||
sandbox_plugins: list[PluginRequirement] = []
|
||||
response_parser = BrowsingResponseParser()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
) -> None:
|
||||
def __init__(self, config: AgentConfig, llm_registry: LLMRegistry) -> None:
|
||||
"""Initializes a new instance of the VisualBrowsingAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
super().__init__(config, llm_registry)
|
||||
# define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
|
||||
# see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
|
||||
action_subsets = [
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import toml
|
||||
import tomlkit
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.shortcuts import clear, print_container
|
||||
@@ -520,7 +520,7 @@ def load_config_file(file_path: Path) -> dict:
|
||||
if file_path.exists():
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
return toml.load(f)
|
||||
return dict(tomlkit.load(f))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -530,9 +530,36 @@ def load_config_file(file_path: Path) -> dict:
|
||||
|
||||
|
||||
def save_config_file(config_data: dict, file_path: Path) -> None:
|
||||
"""Save the config file."""
|
||||
"""Save the config file with proper MCP formatting."""
|
||||
doc = tomlkit.document()
|
||||
|
||||
for key, value in config_data.items():
|
||||
if key == 'mcp':
|
||||
# Handle MCP section specially
|
||||
mcp_section = tomlkit.table()
|
||||
|
||||
for mcp_key, mcp_value in value.items():
|
||||
# Create array with inline tables for server configurations
|
||||
server_array = tomlkit.array()
|
||||
for server_config in mcp_value:
|
||||
if isinstance(server_config, dict):
|
||||
# Create inline table for each server
|
||||
inline_table = tomlkit.inline_table()
|
||||
for server_key, server_val in server_config.items():
|
||||
inline_table[server_key] = server_val
|
||||
server_array.append(inline_table)
|
||||
else:
|
||||
# Handle non-dict values (like string URLs)
|
||||
server_array.append(server_config)
|
||||
mcp_section[mcp_key] = server_array
|
||||
|
||||
doc[key] = mcp_section
|
||||
else:
|
||||
# Handle non-MCP sections normally
|
||||
doc[key] = value
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
toml.dump(config_data, f)
|
||||
f.write(tomlkit.dumps(doc))
|
||||
|
||||
|
||||
def _ensure_mcp_config_structure(config_data: dict) -> None:
|
||||
|
||||
@@ -83,6 +83,7 @@ from openhands.microagent.microagent import BaseMicroagent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.storage.settings.file_settings_store import FileSettingsStore
|
||||
from openhands.utils.utils import create_registry_and_convo_stats
|
||||
|
||||
|
||||
async def cleanup_session(
|
||||
@@ -147,9 +148,16 @@ async def run_session(
|
||||
None, display_initialization_animation, 'Initializing...', is_loaded
|
||||
)
|
||||
|
||||
agent = create_agent(config)
|
||||
llm_registry, convo_stats, config = create_registry_and_convo_stats(
|
||||
config,
|
||||
sid,
|
||||
None,
|
||||
)
|
||||
|
||||
agent = create_agent(config, llm_registry)
|
||||
runtime = create_runtime(
|
||||
config,
|
||||
llm_registry,
|
||||
sid=sid,
|
||||
headless_mode=True,
|
||||
agent=agent,
|
||||
@@ -161,7 +169,7 @@ async def run_session(
|
||||
|
||||
runtime.subscribe_to_shell_stream(stream_to_console)
|
||||
|
||||
controller, initial_state = create_controller(agent, runtime, config)
|
||||
controller, initial_state = create_controller(agent, runtime, config, convo_stats)
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
|
||||
@@ -360,12 +368,12 @@ async def run_session(
|
||||
# Check if it's an authentication error
|
||||
if 'ERROR_LLM_AUTHENTICATION' in error_message:
|
||||
# Start with base authentication error message
|
||||
initial_message = 'Authentication error with the LLM provider. Please check your API key.'
|
||||
welcome_message = 'Authentication error with the LLM provider. Please check your API key.'
|
||||
|
||||
# Add OpenHands-specific guidance if using an OpenHands model
|
||||
llm_config = config.get_llm_config()
|
||||
if llm_config.model.startswith('openhands/'):
|
||||
initial_message += " If you're using OpenHands models, get a new API key from https://app.all-hands.dev/settings/api-keys"
|
||||
welcome_message += "\nIf you're using OpenHands models, get a new API key from https://app.all-hands.dev/settings/api-keys"
|
||||
else:
|
||||
# For other errors, use the standard message
|
||||
initial_message = (
|
||||
|
||||
26
openhands/cli/pt_style.py
Normal file
26
openhands/cli/pt_style.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from prompt_toolkit.styles import Style, merge_styles
|
||||
from prompt_toolkit.styles.defaults import default_ui_style
|
||||
|
||||
# Centralized helper for CLI styles so we can safely merge our custom colors
|
||||
# with prompt_toolkit's default UI style. This preserves completion menu and
|
||||
# fuzzy-match visibility across different terminal themes (e.g., Ubuntu).
|
||||
|
||||
COLOR_GOLD = '#FFD700'
|
||||
COLOR_GREY = '#808080'
|
||||
COLOR_AGENT_BLUE = '#4682B4' # Steel blue - readable on light/dark backgrounds
|
||||
|
||||
|
||||
def get_cli_style() -> Style:
|
||||
base = default_ui_style()
|
||||
custom = Style.from_dict(
|
||||
{
|
||||
'gold': COLOR_GOLD,
|
||||
'grey': COLOR_GREY,
|
||||
'prompt': f'{COLOR_GOLD} bold',
|
||||
# Ensure good contrast for fuzzy matches on the selected completion row
|
||||
# across terminals/themes (e.g., Ubuntu GNOME, Alacritty, Kitty).
|
||||
# See https://github.com/All-Hands-AI/OpenHands/issues/10330
|
||||
'completion-menu.completion.current fuzzymatch.outside': 'fg:#ffffff bg:#888888',
|
||||
}
|
||||
)
|
||||
return merge_styles([base, custom])
|
||||
@@ -8,8 +8,8 @@ from prompt_toolkit.shortcuts import print_container
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.cli.pt_style import COLOR_GREY, get_cli_style
|
||||
from openhands.cli.tui import (
|
||||
COLOR_GREY,
|
||||
UserCancelledError,
|
||||
cli_confirm,
|
||||
kb_cancel,
|
||||
@@ -242,7 +242,7 @@ async def modify_llm_settings_basic(
|
||||
provider_list = verified_providers + provider_list
|
||||
|
||||
provider_completer = FuzzyWordCompleter(provider_list, WORD=True)
|
||||
session = PromptSession(key_bindings=kb_cancel())
|
||||
session = PromptSession(key_bindings=kb_cancel(), style=get_cli_style())
|
||||
|
||||
current_provider, current_model, current_api_key = (
|
||||
_get_current_values_for_modification_basic(config)
|
||||
@@ -490,7 +490,7 @@ async def modify_llm_settings_basic(
|
||||
async def modify_llm_settings_advanced(
|
||||
config: OpenHandsConfig, settings_store: FileSettingsStore
|
||||
) -> None:
|
||||
session = PromptSession(key_bindings=kb_cancel())
|
||||
session = PromptSession(key_bindings=kb_cancel(), style=get_cli_style())
|
||||
llm_config = config.get_llm_config()
|
||||
|
||||
custom_model = None
|
||||
@@ -621,7 +621,7 @@ async def modify_search_api_settings(
|
||||
config: OpenHandsConfig, settings_store: FileSettingsStore
|
||||
) -> None:
|
||||
"""Modify search API settings."""
|
||||
session = PromptSession(key_bindings=kb_cancel())
|
||||
session = PromptSession(key_bindings=kb_cancel(), style=get_cli_style())
|
||||
|
||||
search_api_key = None
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
|
||||
from openhands import __version__
|
||||
from openhands.cli.pt_style import (
|
||||
COLOR_AGENT_BLUE,
|
||||
COLOR_GOLD,
|
||||
COLOR_GREY,
|
||||
get_cli_style,
|
||||
)
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events import EventSource, EventStream
|
||||
@@ -41,6 +47,7 @@ from openhands.events.action import (
|
||||
CmdRunAction,
|
||||
MCPAction,
|
||||
MessageAction,
|
||||
TaskTrackingAction,
|
||||
)
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import (
|
||||
@@ -50,6 +57,7 @@ from openhands.events.observation import (
|
||||
FileEditObservation,
|
||||
FileReadObservation,
|
||||
MCPObservation,
|
||||
TaskTrackingObservation,
|
||||
)
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.mcp.error_collector import mcp_error_collector
|
||||
@@ -64,16 +72,7 @@ recent_thoughts: list[str] = []
|
||||
MAX_RECENT_THOUGHTS = 5
|
||||
|
||||
# Color and styling constants
|
||||
COLOR_GOLD = '#FFD700'
|
||||
COLOR_GREY = '#808080'
|
||||
COLOR_AGENT_BLUE = '#4682B4' # Steel blue - less saturated, works well on both light and dark backgrounds
|
||||
DEFAULT_STYLE = Style.from_dict(
|
||||
{
|
||||
'gold': COLOR_GOLD,
|
||||
'grey': COLOR_GREY,
|
||||
'prompt': f'{COLOR_GOLD} bold',
|
||||
}
|
||||
)
|
||||
DEFAULT_STYLE = get_cli_style()
|
||||
|
||||
COMMANDS = {
|
||||
'/exit': 'Exit the application',
|
||||
@@ -273,6 +272,8 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
|
||||
initialize_streaming_output()
|
||||
elif isinstance(event, MCPAction):
|
||||
display_mcp_action(event)
|
||||
elif isinstance(event, TaskTrackingAction):
|
||||
display_task_tracking_action(event)
|
||||
elif isinstance(event, Action):
|
||||
# For other actions, display thoughts normally
|
||||
if hasattr(event, 'thought') and event.thought:
|
||||
@@ -293,6 +294,8 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
|
||||
display_file_read(event)
|
||||
elif isinstance(event, MCPObservation):
|
||||
display_mcp_observation(event)
|
||||
elif isinstance(event, TaskTrackingObservation):
|
||||
display_task_tracking_observation(event)
|
||||
elif isinstance(event, AgentStateChangedObservation):
|
||||
display_agent_state_change_message(event.agent_state)
|
||||
elif isinstance(event, ErrorObservation):
|
||||
@@ -521,6 +524,74 @@ def display_mcp_observation(event: MCPObservation) -> None:
|
||||
print_container(container)
|
||||
|
||||
|
||||
def display_task_tracking_action(event: TaskTrackingAction) -> None:
|
||||
"""Display a TaskTracking action in the CLI."""
|
||||
# Display thought first if present
|
||||
if hasattr(event, 'thought') and event.thought:
|
||||
display_message(event.thought)
|
||||
|
||||
# Format the command and task list for display
|
||||
display_text = f'Command: {event.command}'
|
||||
|
||||
if event.command == 'plan':
|
||||
if event.task_list:
|
||||
display_text += f'\n\nTask List ({len(event.task_list)} items):'
|
||||
for i, task in enumerate(event.task_list, 1):
|
||||
status = task.get('status', 'unknown')
|
||||
title = task.get('title', 'Untitled task')
|
||||
task_id = task.get('id', f'task-{i}')
|
||||
notes = task.get('notes', '')
|
||||
|
||||
# Add status indicator with color
|
||||
status_indicator = {
|
||||
'todo': '⏳',
|
||||
'in_progress': '🔄',
|
||||
'done': '✅',
|
||||
}.get(status, '❓')
|
||||
|
||||
display_text += f'\n {i}. {status_indicator} [{status.upper()}] {title} (ID: {task_id})'
|
||||
if notes:
|
||||
display_text += f'\n Notes: {notes}'
|
||||
else:
|
||||
display_text += '\n\nTask List: Empty'
|
||||
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=display_text,
|
||||
read_only=True,
|
||||
style='ansigreen',
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='Task Tracking Action',
|
||||
style='ansigreen',
|
||||
)
|
||||
print_formatted_text('')
|
||||
print_container(container)
|
||||
|
||||
|
||||
def display_task_tracking_observation(event: TaskTrackingObservation) -> None:
|
||||
"""Display a TaskTracking observation in the CLI."""
|
||||
# Format the content and task list for display
|
||||
content = (
|
||||
event.content.strip() if event.content else 'Task tracking operation completed'
|
||||
)
|
||||
|
||||
display_text = f'Result: {content}'
|
||||
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=display_text,
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='Task Tracking Result',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_formatted_text('')
|
||||
print_container(container)
|
||||
|
||||
|
||||
def initialize_streaming_output():
|
||||
"""Initialize the streaming output TextArea."""
|
||||
if not ENABLE_STREAMING:
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.events.action import Action
|
||||
@@ -17,7 +19,6 @@ from openhands.core.exceptions import (
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
|
||||
|
||||
@@ -38,10 +39,11 @@ class Agent(ABC):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
llm_registry: LLMRegistry,
|
||||
):
|
||||
self.llm = llm
|
||||
self.llm = llm_registry.get_llm_from_agent_config('agent', config)
|
||||
self.llm_registry = llm_registry
|
||||
self.config = config
|
||||
self._complete = False
|
||||
self._prompt_manager: 'PromptManager' | None = None
|
||||
|
||||
@@ -73,9 +73,9 @@ from openhands.events.observation import (
|
||||
Observation,
|
||||
)
|
||||
from openhands.events.serialization.event import truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.server.services.conversation_stats import ConversationStats
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
# note: RESUME is only available on web GUI
|
||||
@@ -109,6 +109,7 @@ class AgentController:
|
||||
self,
|
||||
agent: Agent,
|
||||
event_stream: EventStream,
|
||||
convo_stats: ConversationStats,
|
||||
iteration_delta: int,
|
||||
budget_per_task_delta: float | None = None,
|
||||
agent_to_llm_config: dict[str, LLMConfig] | None = None,
|
||||
@@ -148,6 +149,7 @@ class AgentController:
|
||||
self.agent = agent
|
||||
self.headless_mode = headless_mode
|
||||
self.is_delegate = is_delegate
|
||||
self.convo_stats = convo_stats
|
||||
|
||||
# the event stream must be set before maybe subscribing to it
|
||||
self.event_stream = event_stream
|
||||
@@ -163,6 +165,7 @@ class AgentController:
|
||||
# state from the previous session, state from a parent agent, or a fresh state
|
||||
self.set_initial_state(
|
||||
state=initial_state,
|
||||
convo_stats=convo_stats,
|
||||
max_iterations=iteration_delta,
|
||||
max_budget_per_task=budget_per_task_delta,
|
||||
confirmation_mode=confirmation_mode,
|
||||
@@ -477,11 +480,6 @@ class AgentController:
|
||||
log_level, str(observation_to_print), extra={'msg_type': 'OBSERVATION'}
|
||||
)
|
||||
|
||||
# TODO: these metrics come from the draft editor, and they get accumulated into controller's state metrics and the agent's llm metrics
|
||||
# In the future, we should have a more principled way to sharing metrics across all LLM instances for a given conversation
|
||||
if observation.llm_metrics is not None:
|
||||
self.state_tracker.merge_metrics(observation.llm_metrics)
|
||||
|
||||
# this happens for runnable actions and microagent actions
|
||||
if self._pending_action and self._pending_action.id == observation.cause:
|
||||
if self.state.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
|
||||
@@ -657,14 +655,10 @@ class AgentController:
|
||||
"""
|
||||
agent_cls: type[Agent] = Agent.get_cls(action.agent)
|
||||
agent_config = self.agent_configs.get(action.agent, self.agent.config)
|
||||
llm_config = self.agent_to_llm_config.get(action.agent, self.agent.llm.config)
|
||||
# Make sure metrics are shared between parent and child for global accumulation
|
||||
llm = LLM(
|
||||
config=llm_config,
|
||||
retry_listener=self.agent.llm.retry_listener,
|
||||
metrics=self.state.metrics,
|
||||
delegate_agent = agent_cls(
|
||||
config=agent_config, llm_registry=self.agent.llm_registry
|
||||
)
|
||||
delegate_agent = agent_cls(llm=llm, config=agent_config)
|
||||
|
||||
# Take a snapshot of the current metrics before starting the delegate
|
||||
state = State(
|
||||
@@ -683,7 +677,7 @@ class AgentController:
|
||||
)
|
||||
self.log(
|
||||
'debug',
|
||||
f'start delegate, creating agent {delegate_agent.name} using LLM {llm}',
|
||||
f'start delegate, creating agent {delegate_agent.name}',
|
||||
)
|
||||
|
||||
# Create the delegate with is_delegate=True so it does NOT subscribe directly
|
||||
@@ -693,6 +687,7 @@ class AgentController:
|
||||
user_id=self.user_id,
|
||||
agent=delegate_agent,
|
||||
event_stream=self.event_stream,
|
||||
convo_stats=self.convo_stats,
|
||||
iteration_delta=self._initial_max_iterations,
|
||||
budget_per_task_delta=self._initial_max_budget_per_task,
|
||||
agent_to_llm_config=self.agent_to_llm_config,
|
||||
@@ -795,13 +790,8 @@ class AgentController:
|
||||
extra={'msg_type': 'STEP'},
|
||||
)
|
||||
|
||||
# Ensure budget control flag is synchronized with the latest metrics.
|
||||
# In the future, we should centralized the use of one LLM object per conversation.
|
||||
# This will help us unify the cost for auto generating titles, running the condensor, etc.
|
||||
# Before many microservices will touh the same llm cost field, we should sync with the budget flag for the controller
|
||||
# and check that we haven't exceeded budget BEFORE executing an agent step.
|
||||
# Synchronize spend across all llm services with the budget flag
|
||||
self.state_tracker.sync_budget_flag_with_metrics()
|
||||
|
||||
if self._is_stuck():
|
||||
await self._react_to_exception(
|
||||
AgentStuckInLoopError('Agent got stuck in a loop')
|
||||
@@ -961,14 +951,15 @@ class AgentController:
|
||||
def set_initial_state(
|
||||
self,
|
||||
state: State | None,
|
||||
convo_stats: ConversationStats,
|
||||
max_iterations: int,
|
||||
max_budget_per_task: float | None,
|
||||
confirmation_mode: bool = False,
|
||||
):
|
||||
self.state_tracker.set_initial_state(
|
||||
self.id,
|
||||
self.agent,
|
||||
state,
|
||||
convo_stats,
|
||||
max_iterations,
|
||||
max_budget_per_task,
|
||||
confirmation_mode,
|
||||
@@ -1009,37 +1000,20 @@ class AgentController:
|
||||
action: The action to attach metrics to
|
||||
"""
|
||||
# Get metrics from agent LLM
|
||||
agent_metrics = self.state.metrics
|
||||
metrics = self.convo_stats.get_combined_metrics()
|
||||
|
||||
# Get metrics from condenser LLM if it exists
|
||||
condenser_metrics: Metrics | None = None
|
||||
if hasattr(self.agent, 'condenser') and hasattr(self.agent.condenser, 'llm'):
|
||||
condenser_metrics = self.agent.condenser.llm.metrics
|
||||
|
||||
# Create a new minimal metrics object with just what the frontend needs
|
||||
metrics = Metrics(model_name=agent_metrics.model_name)
|
||||
|
||||
# Set accumulated cost (sum of agent and condenser costs)
|
||||
metrics.accumulated_cost = agent_metrics.accumulated_cost
|
||||
if condenser_metrics:
|
||||
metrics.accumulated_cost += condenser_metrics.accumulated_cost
|
||||
# Create a clean copy with only the fields we want to keep
|
||||
clean_metrics = Metrics()
|
||||
clean_metrics.accumulated_cost = metrics.accumulated_cost
|
||||
clean_metrics._accumulated_token_usage = copy.deepcopy(
|
||||
metrics.accumulated_token_usage
|
||||
)
|
||||
|
||||
# Add max_budget_per_task to metrics
|
||||
if self.state.budget_flag:
|
||||
metrics.max_budget_per_task = self.state.budget_flag.max_value
|
||||
clean_metrics.max_budget_per_task = self.state.budget_flag.max_value
|
||||
|
||||
# Set accumulated token usage (sum of agent and condenser token usage)
|
||||
# Use a deep copy to ensure we don't modify the original object
|
||||
metrics._accumulated_token_usage = (
|
||||
agent_metrics.accumulated_token_usage.model_copy(deep=True)
|
||||
)
|
||||
if condenser_metrics:
|
||||
metrics._accumulated_token_usage = (
|
||||
metrics._accumulated_token_usage
|
||||
+ condenser_metrics.accumulated_token_usage
|
||||
)
|
||||
|
||||
action.llm_metrics = metrics
|
||||
action.llm_metrics = clean_metrics
|
||||
|
||||
# Log the metrics information for debugging
|
||||
# Get the latest usage directly from the agent's metrics
|
||||
|
||||
@@ -21,6 +21,7 @@ from openhands.events.action.agent import AgentFinishAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.memory.view import View
|
||||
from openhands.server.services.conversation_stats import ConversationStats
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.storage.locations import get_conversation_agent_state_filename
|
||||
|
||||
@@ -84,6 +85,7 @@ class State:
|
||||
limit_increase_amount=100, current_value=0, max_value=100
|
||||
)
|
||||
)
|
||||
convo_stats: ConversationStats | None = None
|
||||
budget_flag: BudgetControlFlag | None = None
|
||||
confirmation_mode: bool = False
|
||||
history: list[Event] = field(default_factory=list)
|
||||
@@ -91,8 +93,7 @@ class State:
|
||||
outputs: dict = field(default_factory=dict)
|
||||
agent_state: AgentState = AgentState.LOADING
|
||||
resume_state: AgentState | None = None
|
||||
# global metrics for the current task
|
||||
metrics: Metrics = field(default_factory=Metrics)
|
||||
|
||||
# root agent has level 0, and every delegate increases the level by one
|
||||
delegate_level: int = 0
|
||||
# start_id and end_id track the range of events in history
|
||||
@@ -116,9 +117,14 @@ class State:
|
||||
local_metrics: Metrics | None = None
|
||||
delegates: dict[tuple[int, int], tuple[str, str]] | None = None
|
||||
|
||||
metrics: Metrics = field(default_factory=Metrics)
|
||||
|
||||
def save_to_session(
|
||||
self, sid: str, file_store: FileStore, user_id: str | None
|
||||
) -> None:
|
||||
convo_stats = self.convo_stats
|
||||
self.convo_stats = None # Don't save convo stats, handles itself
|
||||
|
||||
pickled = pickle.dumps(self)
|
||||
logger.debug(f'Saving state to session {sid}:{self.agent_state}')
|
||||
encoded = base64.b64encode(pickled).decode('utf-8')
|
||||
@@ -138,6 +144,8 @@ class State:
|
||||
logger.error(f'Failed to save state to session: {e}')
|
||||
raise e
|
||||
|
||||
self.convo_stats = convo_stats # restore reference
|
||||
|
||||
@staticmethod
|
||||
def restore_from_session(
|
||||
sid: str, file_store: FileStore, user_id: str | None = None
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user