mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
15 Commits
test-user
...
openhands/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11def95da0 | ||
|
|
27512ee72c | ||
|
|
8a50164c45 | ||
|
|
1c54f333c5 | ||
|
|
e6ddf09897 | ||
|
|
d9f311a398 | ||
|
|
f3d74ab807 | ||
|
|
6dbbf76231 | ||
|
|
1231b78aea | ||
|
|
9003f40096 | ||
|
|
f70f649745 | ||
|
|
7939bd694b | ||
|
|
916bb85244 | ||
|
|
4ef1dde5f6 | ||
|
|
cf982e0134 |
58
.github/workflows/cli-build-test.yml
vendored
Normal file
58
.github/workflows/cli-build-test.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Workflow that builds and tests the CLI binary executable
|
||||
name: CLI - Build and Test Binary
|
||||
|
||||
# Run on pushes to main branch and all pull requests, but only when CLI files change
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "openhands-cli/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "openhands-cli/**"
|
||||
|
||||
# Cancel previous runs if a new commit is pushed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test-binary:
|
||||
name: Build and test binary executable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: openhands-cli
|
||||
run: |
|
||||
uv sync
|
||||
|
||||
- name: Build binary executable
|
||||
working-directory: openhands-cli
|
||||
run: |
|
||||
./build.sh --install-pyinstaller | tee output.log
|
||||
echo "Full output:"
|
||||
cat output.log
|
||||
|
||||
if grep -q "❌" output.log; then
|
||||
echo "❌ Found failure marker in output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Build & test finished without ❌ markers"
|
||||
20
.github/workflows/lint.yml
vendored
20
.github/workflows/lint.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
npm run make-i18n && tsc
|
||||
npm run check-translation-completeness
|
||||
|
||||
# Run lint on the python code
|
||||
# Run lint on the python code (excluding CLI and enterprise)
|
||||
lint-python:
|
||||
name: Lint python
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
@@ -73,6 +73,24 @@ jobs:
|
||||
working-directory: ./enterprise
|
||||
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
lint-cli-python:
|
||||
name: Lint CLI python
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: "pip"
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit==3.7.0
|
||||
- name: Run pre-commit hooks
|
||||
working-directory: ./openhands-cli
|
||||
run: pre-commit run --all-files --config ../dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
# Check version consistency across documentation
|
||||
check-version-consistency:
|
||||
name: Check version consistency
|
||||
|
||||
30
.github/workflows/py-tests.yml
vendored
30
.github/workflows/py-tests.yml
vendored
@@ -104,3 +104,33 @@ jobs:
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./enterprise
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./tests/unit
|
||||
|
||||
# Run CLI unit tests
|
||||
test-cli-python:
|
||||
name: CLI Unit Tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./openhands-cli
|
||||
run: |
|
||||
uv sync --group dev
|
||||
|
||||
- name: Run CLI unit tests
|
||||
working-directory: ./openhands-cli
|
||||
run: |
|
||||
uv run pytest -v
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
stale-issue-message: 'This issue is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
|
||||
stale-pr-message: 'This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
|
||||
days-before-stale: 40
|
||||
exempt-issue-labels: roadmap,backlog
|
||||
exempt-issue-labels: roadmap,backlog,app-team
|
||||
close-issue-message: 'This issue was automatically closed due to 50 days of inactivity. We do this to help keep the issues somewhat manageable and focus on active issues.'
|
||||
close-pr-message: 'This PR was closed because it had no activity for 50 days. If you feel this was closed in error, and you would like to continue the PR, please resubmit or let us know.'
|
||||
days-before-close: 10
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,7 +31,8 @@ requirements.txt
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
# Note: openhands-cli.spec is intentionally tracked for CLI builds
|
||||
# *.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.56-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.57-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -79,17 +79,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
|
||||
You can also run OpenHands directly with Docker:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
|
||||
|
||||
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
|
||||
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
|
||||
|
||||
@@ -489,6 +489,47 @@ type = "noop"
|
||||
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
|
||||
#privileged = false
|
||||
|
||||
#################################### MCP #####################################
|
||||
# Configuration for Model Context Protocol (MCP) servers
|
||||
# MCP allows OpenHands to communicate with external tool servers
|
||||
##############################################################################
|
||||
[mcp]
|
||||
# SSE servers - Server-Sent Events transport (legacy)
|
||||
#sse_servers = [
|
||||
# # Basic SSE server with just a URL
|
||||
# "http://localhost:8080/mcp/sse",
|
||||
#
|
||||
# # SSE server with authentication
|
||||
# {url = "https://api.example.com/mcp/sse", api_key = "your-api-key"}
|
||||
#]
|
||||
|
||||
# SHTTP servers - Streamable HTTP transport (recommended)
|
||||
#shttp_servers = [
|
||||
# # Basic SHTTP server with default 60s timeout
|
||||
# "https://api.example.com/mcp/shttp",
|
||||
#
|
||||
# # SHTTP server with custom timeout for long-running tools
|
||||
# {
|
||||
# url = "https://api.example.com/mcp/shttp",
|
||||
# api_key = "your-api-key",
|
||||
# timeout = 180 # 3 minutes for processing-heavy tools (1-3600 seconds)
|
||||
# }
|
||||
#]
|
||||
|
||||
# Stdio servers - Direct process communication (development only)
|
||||
#stdio_servers = [
|
||||
# # Basic stdio server
|
||||
# {name = "filesystem", command = "npx", args = ["@modelcontextprotocol/server-filesystem", "/"]},
|
||||
#
|
||||
# # Stdio server with environment variables
|
||||
# {
|
||||
# name = "fetch",
|
||||
# command = "uvx",
|
||||
# args = ["mcp-server-fetch"],
|
||||
# env = {DEBUG = "true"}
|
||||
# }
|
||||
#]
|
||||
|
||||
#################################### Model Routing ############################
|
||||
# Configuration for experimental model routing feature
|
||||
# Enables intelligent switching between different LLM models for specific purposes
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.56-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.57-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -3,9 +3,9 @@ repos:
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
|
||||
- id: check-yaml
|
||||
args: ["--allow-multiple-documents"]
|
||||
- id: debug-statements
|
||||
@@ -28,12 +28,12 @@ repos:
|
||||
entry: ruff check --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
args: [--fix, --unsafe-fixes]
|
||||
exclude: ^(third_party/|enterprise/)
|
||||
exclude: ^(third_party/|enterprise/|openhands-cli/)
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
entry: ruff format --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
exclude: ^(third_party/|enterprise/)
|
||||
exclude: ^(third_party/|enterprise/|openhands-cli/)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.15.0
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -113,7 +113,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -122,7 +122,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
|
||||
python -m openhands.cli.entry --override-cli-mode true
|
||||
```
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
|
||||
# Run OpenHands
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -73,7 +73,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
|
||||
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
2. Wait until the server is running (see log below):
|
||||
```
|
||||
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.56
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
|
||||
@@ -30,6 +30,20 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
|
||||
## Pricing
|
||||
|
||||
Pricing follows official API provider rates. [You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
|
||||
Pricing follows official API provider rates. Below are the current pricing details for OpenHands models:
|
||||
|
||||
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: \$0.4 per million input tokens and \$1.6 per million output tokens.
|
||||
| Model | Input Cost (per 1M tokens) | Cached Input Cost (per 1M tokens) | Output Cost (per 1M tokens) | Max Input Tokens | Max Output Tokens |
|
||||
|-------|----------------------------|-----------------------------------|------------------------------|------------------|-------------------|
|
||||
| claude-opus-4-20250514 | $15.00 | $1.50 | $75.00 | 200,000 | 32,000 |
|
||||
| claude-sonnet-4-20250514 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
|
||||
| devstral-medium-2507 | $0.40 | N/A | $2.00 | 128,000 | 128,000 |
|
||||
| devstral-small-2505 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
|
||||
| devstral-small-2507 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
|
||||
| gemini-2.5-pro | $1.25 | $0.31 | $10.00 | 1,048,576 | 65,535 |
|
||||
| gpt-5-2025-08-07 | $1.25 | $0.125 | $10.00 | 400,000 | 128,000 |
|
||||
| gpt-5-mini-2025-08-07 | $0.25 | $0.025 | $2.00 | 400,000 | 128,000 |
|
||||
| o3 | $2.00 | $0.50 | $8.00 | 200,000 | 100,000 |
|
||||
| o4-mini | $1.10 | $0.28 | $4.40 | 200,000 | 100,000 |
|
||||
| qwen3-coder-480b | $0.40 | N/A | $1.60 | N/A | N/A |
|
||||
|
||||
**Note:** Cached input tokens are charged at a reduced rate when the same content is reused across requests. Models that don't support prompt caching show "N/A" for cached input cost.
|
||||
|
||||
@@ -116,17 +116,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
|
||||
<Accordion title="Docker Command (Click to expand)">
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.56
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -67,6 +67,19 @@ sse_servers = [
|
||||
# External MCP service with authentication
|
||||
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
|
||||
]
|
||||
|
||||
# SHTTP Servers - Modern streamable HTTP transport (recommended)
|
||||
shttp_servers = [
|
||||
# Basic SHTTP server with default 60s timeout
|
||||
"https://api.example.com/mcp/shttp",
|
||||
|
||||
# Server with custom timeout for heavy operations
|
||||
{
|
||||
url = "https://files.example.com/mcp/shttp",
|
||||
api_key = "your-api-key",
|
||||
timeout = 1800 # 30 minutes for large file processing
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -118,6 +131,17 @@ SHTTP (Streamable HTTP) servers are configured using either a string URL or an o
|
||||
- Type: `str`
|
||||
- Description: API key for authentication
|
||||
|
||||
- `timeout` (optional)
|
||||
- Type: `int`
|
||||
- Default: `60`
|
||||
- Range: `1-3600` seconds (1 hour maximum)
|
||||
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
|
||||
- **Use Cases:**
|
||||
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries
|
||||
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls
|
||||
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations
|
||||
- **Note**: This timeout only applies to individual tool calls, not server connection establishment.
|
||||
|
||||
### Stdio Servers
|
||||
|
||||
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
|
||||
@@ -192,5 +216,27 @@ SHTTP is the modern HTTP-based transport protocol that provides enhanced feature
|
||||
|
||||
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
|
||||
|
||||
#### SHTTP Timeout Best Practices
|
||||
|
||||
When configuring SHTTP timeouts, consider these guidelines:
|
||||
|
||||
**Timeout Selection:**
|
||||
- **Database queries**: 30-60 seconds
|
||||
- **File operations**: 60-300 seconds (depending on file size)
|
||||
- **Web scraping**: 60-120 seconds
|
||||
- **Complex calculations**: 300-1800 seconds
|
||||
- **Batch processing**: 1800-3600 seconds (maximum)
|
||||
|
||||
**Error Handling:**
|
||||
When a tool call exceeds the configured timeout:
|
||||
- The operation is cancelled with an `asyncio.TimeoutError`
|
||||
- The agent receives a timeout error message
|
||||
- The server connection remains active for subsequent requests
|
||||
|
||||
**Monitoring:**
|
||||
- Set timeouts based on your tool's actual performance characteristics
|
||||
- Monitor timeout occurrences to optimize timeout values
|
||||
- Consider implementing server-side timeout handling for graceful degradation
|
||||
|
||||
### Standard Input/Output (stdio)
|
||||
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.
|
||||
|
||||
@@ -7,14 +7,28 @@ LABEL com.datadoghq.tags.service="deploy"
|
||||
LABEL com.datadoghq.tags.env="${DD_ENV}"
|
||||
|
||||
# Install Node.js v20+ and npm (which includes npx)
|
||||
# Apply security updates to fix CVEs
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
apt-get install -y jq gettext && \
|
||||
apt-get clean
|
||||
# Apply security updates for packages with available fixes
|
||||
apt-get upgrade -y \
|
||||
libc-bin \
|
||||
libc6 \
|
||||
libgnutls30 \
|
||||
libsqlite3-0 \
|
||||
perl-base && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace posthog "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy
|
||||
# Install Python packages with security fixes
|
||||
RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace posthog "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy && \
|
||||
# Update packages with known CVE fixes
|
||||
pip install --upgrade \
|
||||
"mcp>=1.10.0" \
|
||||
"pillow>=11.3.0"
|
||||
|
||||
WORKDIR /app
|
||||
COPY enterprise .
|
||||
|
||||
@@ -2,7 +2,6 @@ from experiments.constants import (
|
||||
ENABLE_EXPERIMENT_MANAGER,
|
||||
)
|
||||
from experiments.experiment_versions import (
|
||||
handle_claude4_vs_gpt5_experiment,
|
||||
handle_condenser_max_step_experiment,
|
||||
handle_system_prompt_experiment,
|
||||
)
|
||||
@@ -44,9 +43,6 @@ class SaaSExperimentManager(ExperimentManager):
|
||||
return conversation_settings
|
||||
|
||||
# Apply conversation-scoped experiments
|
||||
conversation_settings = handle_claude4_vs_gpt5_experiment(
|
||||
user_id, conversation_id, conversation_settings
|
||||
)
|
||||
conversation_settings = handle_condenser_max_step_experiment(
|
||||
user_id, conversation_id, conversation_settings
|
||||
)
|
||||
|
||||
152
evaluation/benchmarks/multi_swe_bench/SWE-Gym.md
Normal file
152
evaluation/benchmarks/multi_swe_bench/SWE-Gym.md
Normal file
@@ -0,0 +1,152 @@
|
||||
<h1 align="center"> Training Software Engineering Agents and Verifiers with SWE-Gym </h1>
|
||||
|
||||
A Multi-SWE-bench implementation of SWE-Gym.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.jiayipan.com/" style="text-decoration: none;">Jiayi Pan<sup>*,1</sup></a>,
|
||||
<a href="https://xwang.dev/" style="text-decoration: none;">Xingyao Wang<sup>*,2</sup></a>,
|
||||
<a href="https://www.phontron.com/" style="text-decoration: none;">Graham Neubig<sup>3</sup></a>,
|
||||
<a href="https://www.cs.toronto.edu/~ndjaitly/" style="text-decoration: none;">Navdeep Jaitly<sup>4</sup></a>,
|
||||
<a href="https://blender.cs.illinois.edu/hengji.html" style="text-decoration: none;">Heng Ji<sup>2</sup></a>,
|
||||
<a href="https://www.alanesuhr.com/" style="text-decoration: none;">Alane Suhr<sup>^,1</sup></a>,
|
||||
<a href="https://dreasysnail.github.io/" style="text-decoration: none;">Yizhe Zhang<sup>^,4</sup></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sup>1</sup>UC Berkeley, <sup>2</sup>UIUC, <sup>3</sup>CMU, <sup>4</sup>Apple </br>
|
||||
<sub><sup>*</sup>Equal contribution, <sup>^</sup>Equal supervision</sub>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://arxiv.org/abs/2412.21139">📃 Paper</a>
|
||||
•
|
||||
<a href="https://huggingface.co/SWE-Gym" >🤗 Data & Models</a>
|
||||
</p>
|
||||
|
||||
We present **SWE-Gym**, the first environment for training real-world software engineering agents.
|
||||
We use it to train strong LM agents that achieve state-of-the-art open results on SWE-Bench, with early, promising scaling characteristics as we increase training and inference-time compute.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/teaser.jpg?raw=true" width="100%" alt="teaser">
|
||||
</p>
|
||||
|
||||
---
|
||||
# Run SWE-Gym with OpenHands
|
||||
|
||||
The process of running SWE-Gym is very similar to how you'd run SWE-Bench evaluation.
|
||||
|
||||
|
||||
1. First, clone OpenHands repo `git clone https://github.com/All-Hands-AI/OpenHands.git`
|
||||
2. Then setup the repo following [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)
|
||||
3. Then you can simply serve your own model as an OpenAI compatible endpoint, put those info in config.toml. You can do this by following instruction [here](../../README.md#setup).
|
||||
4. And then simply do the following to sample for 16x parallelism:
|
||||
|
||||
```bash
|
||||
export ALLHANDS_API_KEY=ah-yourkey # You don't need to set this when running these in local docker container
|
||||
./evaluation/benchmarks/multi_swe_bench/scripts/rollout_swegym.sh llm.mymodel-temp05 'train-t05' 16
|
||||
```
|
||||
|
||||
NOTE: SWE-Gym sampling with parallelism is currently only tested with AllHands RemoteRuntime (limited beta). Fill [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply for access.
|
||||
|
||||
|
||||
5. When `rollout_swegym.sh` finishes, you will get a file called `output.with_completions.jsonl.gz`. Then you can use [`./scripts/swegym/convert_data.ipynb`](./scripts/swegym/convert_data.ipynb) to convert them into SFT data format.
|
||||
|
||||
## Running the Jupyter Notebook
|
||||
|
||||
To run the data conversion notebook, follow these steps:
|
||||
|
||||
1. Navigate to the OpenHands repository root:
|
||||
```bash
|
||||
cd openhands_repo
|
||||
```
|
||||
|
||||
2. Set the PYTHONPATH and start Jupyter notebook:
|
||||
```bash
|
||||
PYTHONPATH=$(pwd) jupyter notebook
|
||||
```
|
||||
|
||||
3. In the Jupyter interface, navigate to `evaluation/benchmarks/swe_bench/scripts/swegym/convert_data.ipynb`
|
||||
|
||||
4. Update the file paths in the notebook:
|
||||
- Set `FILE_PATHS` to point to your `output.with_completions.jsonl.gz` files
|
||||
- Set `YOUR_OUTPUT_FOLDER` to your desired output directory
|
||||
|
||||
5. Run the notebook cells sequentially to process your data and generate the SFT training format.
|
||||
|
||||
---
|
||||
# More info about SWE-Gym
|
||||
|
||||
Progress in agents for software engineering has been limited by the lack of training environments that both include rigorous verification for reinforcement learning and cover the expansive tasks encountered in real-world repository-level engineering.
|
||||
|
||||
We introduce SWE-Gym: An Open Environment for Training Software Engineering Agents & Verifiers.
|
||||
Our baselines achieve new open SOTA - 32%/26% on SWE-Bench Verified/Lite, with promising scaling trends.
|
||||
|
||||

|
||||
*SWE-Gym enables scalable improvements for software engineering agents at both training and inference time. Our current results is primarily bottlenecked by training and inference compute, rather than the size of our environment.*
|
||||
|
||||
## SWE-Gym Environment
|
||||
|
||||
We create SWE-Gym, the first environment for training SWE agents, with **2.4K real tasks from 11 Python repos** & a Lite split of 234 instances. SWE-Gym combines real-world Python tasks, repository context, executable environments, and test verification to train agents for solving software engineering problems.
|
||||
|
||||

|
||||
|
||||
|
||||
## SWE-Gym trains LMs as agents
|
||||
|
||||
When fine-tuned on less than 500 agent-environment interaction trajectories sampled from it from GPT-4o and Claude 3.5 Sonnet, we achieve **+14%** absolute gains on SWE-Bench Verified with an 32B LM-powered OpenHands agent.
|
||||
|
||||

|
||||
|
||||
|
||||
## SWE-Gym enables self-improvement
|
||||
|
||||
SWE-Gym is also effective across agent scaffolds. With rejection sampling fine-tuning and MoatlessTools scaffold, our 32B and 7B models achieve 20% and 10% respectively on SWE-Bench Lite through self-improvement.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/ml-agent.jpg?raw=true" width="80%" alt="Moatless self-improvement">
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## SWE-Gym enables inference-time scaling
|
||||
|
||||
SWE-Gym enables inference-time scaling through verifiers trained on agent trajectories.
|
||||
These verifiers identify most promising solutions via best-of-n selection, together with our learned agents, they achieve 32%/26% on SWE-Bench Verified/Lite, a new open SoTA.
|
||||
|
||||
|
||||

|
||||
*Inference Time Scaling for Moatless Agent*
|
||||
|
||||

|
||||
*Inference Time Scaling for OpenHands Agent*
|
||||
|
||||
|
||||
## Our baselines on SWE-Gym shows strong scaling trends
|
||||
|
||||
Lastly, our ablations reveal strong scaling trends - performance is now bottlenecked by train and inference compute, rather than the size of our dataset. Pushing and improving these scaling trends further is an exciting direction for future work.
|
||||
|
||||

|
||||
|
||||
## Reproducing Results
|
||||
**The Dataset**
|
||||
|
||||
To access SWE-Gym dataset, checkout our huggingface hub page [SWE-Gym](https://huggingface.co/SWE-Gym)
|
||||
|
||||
The environment constants are currently saved at [SWE-Bench-Fork](https://github.com/SWE-Gym/SWE-Bench-Fork)
|
||||
|
||||
We also have pre-built docker images for each instance under [xingyaoww/sweb.eval.x86_64](https://hub.docker.com/search?q=xingyaoww%2Fsweb.eval.x86_64.) prefix at docker hub.
|
||||
|
||||
|
||||
## 📚 Citation
|
||||
|
||||
```bibtex
|
||||
@misc{pan2024trainingsoftwareengineeringagents,
|
||||
title={Training Software Engineering Agents and Verifiers with SWE-Gym},
|
||||
author={Jiayi Pan and Xingyao Wang and Graham Neubig and Navdeep Jaitly and Heng Ji and Alane Suhr and Yizhe Zhang},
|
||||
year={2024},
|
||||
eprint={2412.21139},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.SE},
|
||||
url={https://arxiv.org/abs/2412.21139},
|
||||
}
|
||||
```
|
||||
@@ -51,8 +51,8 @@ RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'tru
|
||||
|
||||
# TODO: migrate all swe-bench docker to ghcr.io/openhands
|
||||
# TODO: 适应所有的语言
|
||||
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', '')
|
||||
LANGUAGE = os.environ.get('LANGUAGE', 'python')
|
||||
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'mswebench')
|
||||
LANGUAGE = os.environ.get('LANGUAGE', 'java')
|
||||
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
|
||||
|
||||
|
||||
@@ -305,31 +305,19 @@ def get_instance_docker_image(instance: pd.Series):
|
||||
instance_id = instance.get('instance_id', '')
|
||||
tag_suffix = instance_id.split('-')[-1] if instance_id else ''
|
||||
container_tag = f'pr-{tag_suffix}'
|
||||
# pdb.set_trace()
|
||||
return f'mswebench/{container_name}:{container_tag}'
|
||||
# return "kong/insomnia:pr-8284"
|
||||
# return "'sweb.eval.x86_64.local_insomnia"
|
||||
# return "local_insomnia_why"
|
||||
# return "local/kong-insomnia:pr-8117"
|
||||
return f'{DOCKER_IMAGE_PREFIX}/{container_name}:{container_tag}'
|
||||
|
||||
|
||||
def get_config(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
) -> OpenHandsConfig:
|
||||
SWE_BENCH_CONTAINER_IMAGE = 'ghcr.io/opendevin/eval-swe-bench:full-v1.2.1'
|
||||
if USE_INSTANCE_IMAGE:
|
||||
# We use a different instance image for the each instance of swe-bench eval
|
||||
# base_container_image = get_instance_docker_image(instance['instance_id'])
|
||||
base_container_image = get_instance_docker_image(instance)
|
||||
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.'
|
||||
)
|
||||
else:
|
||||
base_container_image = SWE_BENCH_CONTAINER_IMAGE
|
||||
logger.info(f'Using swe-bench container image: {base_container_image}')
|
||||
base_container_image = get_instance_docker_image(instance)
|
||||
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
|
||||
@@ -772,7 +760,6 @@ if __name__ == '__main__':
|
||||
parser.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
default='princeton-nlp/SWE-bench',
|
||||
help='data set to evaluate on, either full-test or lite-test',
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -787,6 +774,7 @@ if __name__ == '__main__':
|
||||
# so we don't need to manage file uploading to OpenHands's repo
|
||||
# dataset = load_dataset(args.dataset, split=args.split)
|
||||
# dataset = load_dataset(args.dataset)
|
||||
logger.info(f'Loading dataset {args.dataset} with split {args.split} ')
|
||||
dataset = load_dataset('json', data_files=args.dataset)
|
||||
dataset = dataset[args.split]
|
||||
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
@@ -839,7 +827,7 @@ if __name__ == '__main__':
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
max_retries=3,
|
||||
)
|
||||
# Check if any instances reached maximum retries
|
||||
check_maximum_retries_exceeded(metadata.eval_output_dir)
|
||||
|
||||
@@ -1,37 +1,54 @@
|
||||
import argparse
|
||||
import json
|
||||
|
||||
input_file = 'XXX.jsonl'
|
||||
output_file = 'YYY.jsonl'
|
||||
|
||||
with (
|
||||
open(input_file, 'r', encoding='utf-8') as fin,
|
||||
open(output_file, 'w', encoding='utf-8') as fout,
|
||||
):
|
||||
for line in fin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
def main(input_file, output_file):
|
||||
with (
|
||||
open(input_file, 'r', encoding='utf-8') as fin,
|
||||
open(output_file, 'w', encoding='utf-8') as fout,
|
||||
):
|
||||
for line in fin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
data = json.loads(line)
|
||||
item = data
|
||||
data = json.loads(line)
|
||||
item = data
|
||||
|
||||
# 提取原始数据
|
||||
org = item.get('org', '')
|
||||
repo = item.get('repo', '')
|
||||
number = str(item.get('number', ''))
|
||||
# Skip instances that don't have resolved_issues or have empty resolved_issues
|
||||
if not item.get('resolved_issues') or len(item['resolved_issues']) == 0:
|
||||
print(
|
||||
f'Skipping instance {item.get("org", "")}/{item.get("repo", "")}-{item.get("number", "")} - no resolved_issues'
|
||||
)
|
||||
continue
|
||||
|
||||
new_item = {}
|
||||
new_item['repo'] = f'{org}/{repo}'
|
||||
new_item['instance_id'] = f'{org}__{repo}-{number}'
|
||||
new_item['problem_statement'] = (
|
||||
item['resolved_issues'][0].get('title', '')
|
||||
+ '\n'
|
||||
+ item['resolved_issues'][0].get('body', '')
|
||||
)
|
||||
new_item['FAIL_TO_PASS'] = []
|
||||
new_item['PASS_TO_PASS'] = []
|
||||
new_item['base_commit'] = item['base'].get('sha', '')
|
||||
new_item['version'] = '0.1' # depends
|
||||
# 提取原始数据
|
||||
org = item.get('org', '')
|
||||
repo = item.get('repo', '')
|
||||
number = str(item.get('number', ''))
|
||||
|
||||
output_data = new_item
|
||||
fout.write(json.dumps(output_data, ensure_ascii=False) + '\n')
|
||||
new_item = {}
|
||||
new_item['repo'] = f'{org}/{repo}'
|
||||
new_item['instance_id'] = f'{org}__{repo}-{number}'
|
||||
|
||||
# Get the first resolved issue
|
||||
resolved_issue = item['resolved_issues'][0]
|
||||
title = resolved_issue.get('title') or ''
|
||||
body = resolved_issue.get('body') or ''
|
||||
|
||||
new_item['problem_statement'] = title + '\n' + body
|
||||
new_item['FAIL_TO_PASS'] = []
|
||||
new_item['PASS_TO_PASS'] = []
|
||||
new_item['base_commit'] = item['base'].get('sha', '')
|
||||
new_item['version'] = '0.1' # depends
|
||||
|
||||
output_data = new_item
|
||||
fout.write(json.dumps(output_data, ensure_ascii=False) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--input', required=True, help='Input .jsonl file path')
|
||||
parser.add_argument('--output', required=True, help='Output .jsonl file path')
|
||||
args = parser.parse_args()
|
||||
main(args.input, args.output)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
tqdm.pandas()
|
||||
|
||||
|
||||
# Load trajectories for resolved instances
|
||||
def load_completions(output_dir: str, instance_id: str):
|
||||
glob_path = os.path.join(output_dir, 'llm_completions', instance_id, '*.json')
|
||||
files = sorted(glob(glob_path)) # this is ascending order
|
||||
# pick the last file (last turn)
|
||||
try:
|
||||
file_path = files[-1]
|
||||
except IndexError:
|
||||
# print(f'No files found for instance {instance_id}: files={files}')
|
||||
return None
|
||||
with open(file_path, 'r') as f:
|
||||
result = json.load(f)
|
||||
# create messages
|
||||
messages = result['messages']
|
||||
messages.append(result['response']['choices'][0]['message'])
|
||||
tools = result['kwargs'].get('tools', [])
|
||||
return {
|
||||
'messages': messages,
|
||||
'tools': tools,
|
||||
}
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('jsonl_path', type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = os.path.dirname(args.jsonl_path)
|
||||
output_path = os.path.join(output_dir, 'output.with_completions.jsonl.gz')
|
||||
|
||||
# Check if output would be different from input
|
||||
needs_update = False
|
||||
with open(args.jsonl_path, 'r') as f_in:
|
||||
for line in tqdm(f_in, desc='Checking for changes'):
|
||||
data = json.loads(line)
|
||||
new_completions = load_completions(output_dir, data['instance_id'])
|
||||
current_completions = data.get('raw_completions')
|
||||
if current_completions != new_completions:
|
||||
needs_update = True
|
||||
break
|
||||
|
||||
if not needs_update:
|
||||
print('No updates required. Skipping file update.')
|
||||
exit(0)
|
||||
|
||||
if os.path.exists(output_path):
|
||||
print(f'Output file already exists at {output_path}, overwriting? (y/n)')
|
||||
if input() != 'y':
|
||||
print('Exiting...')
|
||||
exit(0)
|
||||
|
||||
# Process line by line
|
||||
with open(args.jsonl_path, 'r') as f_in, gzip.open(output_path, 'wt') as f_out:
|
||||
for line in tqdm(f_in):
|
||||
data = json.loads(line)
|
||||
data['raw_completions'] = load_completions(output_dir, data['instance_id'])
|
||||
f_out.write(json.dumps(data) + '\n')
|
||||
|
||||
print(f'Saved compressed output to {output_path}')
|
||||
@@ -1,13 +1,11 @@
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
|
||||
IN_FILE = 'output.jsonl'
|
||||
OUT_FILE = 'patch.jsonl'
|
||||
|
||||
|
||||
def main():
|
||||
with open(IN_FILE, 'r') as fin:
|
||||
with open(OUT_FILE, 'w') as fout:
|
||||
def main(input_file, output_file):
|
||||
with open(input_file, 'r') as fin:
|
||||
with open(output_file, 'w') as fout:
|
||||
for line in fin:
|
||||
data = json.loads(line)
|
||||
groups = re.match(r'(.*)__(.*)-(.*)', data['instance_id'])
|
||||
@@ -15,10 +13,14 @@ def main():
|
||||
'org': groups.group(1),
|
||||
'repo': groups.group(2),
|
||||
'number': groups.group(3),
|
||||
'fix_patch': data['test_result']['git_patch'],
|
||||
'fix_patch': data.get('test_result', {}).get('git_patch', '') or '',
|
||||
}
|
||||
fout.write(json.dumps(patch) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--input', required=True, help='Input .jsonl file path')
|
||||
parser.add_argument('--output', required=True, help='Output .jsonl file path')
|
||||
args = parser.parse_args()
|
||||
main(args.input, args.output)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def update_multi_swe_config(output_jsonl_path, config_path, dataset):
|
||||
path_to_parent = os.path.dirname(os.path.abspath(output_jsonl_path))
|
||||
converted_path = os.path.join(path_to_parent, 'output_converted.jsonl')
|
||||
|
||||
# Run the conversion script
|
||||
subprocess.run(
|
||||
[
|
||||
'python3',
|
||||
'./evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py',
|
||||
'--input',
|
||||
output_jsonl_path,
|
||||
'--output',
|
||||
converted_path,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create required directories
|
||||
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'dataset'), exist_ok=True)
|
||||
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'workdir'), exist_ok=True)
|
||||
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'repos'), exist_ok=True)
|
||||
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'logs'), exist_ok=True)
|
||||
|
||||
# Prepare config dict
|
||||
config = {
|
||||
'mode': 'evaluation',
|
||||
'workdir': os.path.join(path_to_parent, 'eval_files', 'workdir'),
|
||||
'patch_files': [converted_path],
|
||||
'dataset_files': [dataset],
|
||||
'force_build': True,
|
||||
'output_dir': os.path.join(path_to_parent, 'eval_files', 'dataset'),
|
||||
'specifics': [],
|
||||
'skips': [],
|
||||
'repo_dir': os.path.join(path_to_parent, 'eval_files', 'repos'),
|
||||
'need_clone': True,
|
||||
'global_env': [],
|
||||
'clear_env': True,
|
||||
'stop_on_error': False,
|
||||
'max_workers': 5,
|
||||
'max_workers_build_image': 5,
|
||||
'max_workers_run_instance': 5,
|
||||
'log_dir': os.path.join(path_to_parent, 'eval_files', 'logs'),
|
||||
'log_level': 'DEBUG',
|
||||
'fix_patch_run_cmd': (
|
||||
'bash -c "apt update ; apt install -y patch ; '
|
||||
"sed -i 's@git apply.*@patch --batch --fuzz=5 -p1 -i /home/test.patch;"
|
||||
'patch --batch --fuzz=5 -p1 -i /home/fix.patch@g\' /home/fix-run.sh ; chmod +x /home/*.sh ; /home/fix-run.sh"'
|
||||
),
|
||||
}
|
||||
|
||||
# Save to multibench.config
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--input', required=True, help='Path to input file')
|
||||
parser.add_argument('--output', required=True, help='Path to create config')
|
||||
parser.add_argument('--dataset', required=True, help='Path to dataset')
|
||||
args = parser.parse_args()
|
||||
|
||||
update_multi_swe_config(args.input, args.output, args.dataset)
|
||||
@@ -0,0 +1,176 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('input_file', type=str)
|
||||
parser.add_argument(
|
||||
'--force',
|
||||
action='store_true',
|
||||
help='Force update all reports even if no changes are detected',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--overwrite-backup',
|
||||
action='store_true',
|
||||
help='Automatically overwrite existing backup files without prompting',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
dirname = os.path.dirname(args.input_file)
|
||||
|
||||
# Initialize counters and data structures
|
||||
instance_id_to_status = defaultdict(
|
||||
lambda: {
|
||||
'empty_generation': False,
|
||||
'resolved': False,
|
||||
'failed_apply_patch': False,
|
||||
'error_eval': False,
|
||||
'test_timeout': False,
|
||||
}
|
||||
)
|
||||
|
||||
# Process official report if it exists
|
||||
swebench_official_report_json = os.path.join(
|
||||
dirname, 'eval_files/dataset/final_report.json'
|
||||
)
|
||||
openhands_remote_report_jsonl = args.input_file.replace(
|
||||
'.jsonl', '.swebench_eval.jsonl'
|
||||
)
|
||||
|
||||
if os.path.exists(swebench_official_report_json):
|
||||
output_md_filepath = os.path.join(dirname, 'README.md')
|
||||
with open(swebench_official_report_json, 'r') as f:
|
||||
report = json.load(f)
|
||||
|
||||
# Convert instance IDs from "repo/name:pr-123" format to "repo__name-123" format
|
||||
def convert_instance_id(instance_id):
|
||||
"""Convert instance ID from slash/colon-pr format to double underscore/dash format."""
|
||||
if '/' in instance_id and ':pr-' in instance_id:
|
||||
# Split on '/' and ':pr-'
|
||||
parts = instance_id.split('/')
|
||||
if len(parts) == 2:
|
||||
repo_part = parts[0]
|
||||
name_and_pr = parts[1]
|
||||
if ':pr-' in name_and_pr:
|
||||
name, pr_number = name_and_pr.split(':pr-')
|
||||
return f'{repo_part}__{name}-{pr_number}'
|
||||
return instance_id
|
||||
|
||||
# Convert all instance ID lists in the report
|
||||
for key in [
|
||||
'resolved_ids',
|
||||
'unresolved_ids',
|
||||
'error_ids',
|
||||
'empty_patch_ids',
|
||||
'incomplete_ids',
|
||||
]:
|
||||
if key in report:
|
||||
report[key] = [
|
||||
convert_instance_id(instance_id) for instance_id in report[key]
|
||||
]
|
||||
|
||||
output_md = (
|
||||
'# Multi-SWE-bench Report\n'
|
||||
'This folder contains the evaluation results of the SWE-bench using the [official evaluation docker containerization](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level).\n\n'
|
||||
'## Summary\n'
|
||||
f'- total instances: {report["total_instances"]}\n'
|
||||
f'- submitted instances: {report["submitted_instances"]}\n'
|
||||
f'- completed instances: {report["completed_instances"]}\n'
|
||||
f'- empty patch instances: {report["empty_patch_instances"]}\n'
|
||||
f'- resolved instances: {report["resolved_instances"]}\n'
|
||||
f'- unresolved instances: {report["unresolved_instances"]}\n'
|
||||
f'- error instances: {report["error_instances"]}\n'
|
||||
)
|
||||
|
||||
output_md += '\n## Resolved Instances\n'
|
||||
# instance_id to status
|
||||
for instance_id in report['resolved_ids']:
|
||||
instance_id_to_status[instance_id]['resolved'] = True
|
||||
output_md += (
|
||||
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
|
||||
)
|
||||
|
||||
output_md += '\n## Unresolved Instances\n'
|
||||
for instance_id in report['unresolved_ids']:
|
||||
output_md += (
|
||||
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
|
||||
)
|
||||
|
||||
output_md += '\n## Error Instances\n'
|
||||
for instance_id in report['error_ids']:
|
||||
instance_id_to_status[instance_id]['error_eval'] = True
|
||||
output_md += (
|
||||
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
|
||||
)
|
||||
|
||||
output_md += '\n## Empty Patch Instances\n'
|
||||
for instance_id in report['empty_patch_ids']:
|
||||
instance_id_to_status[instance_id]['empty_generation'] = True
|
||||
output_md += (
|
||||
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
|
||||
)
|
||||
|
||||
output_md += '\n## Incomplete Instances\n'
|
||||
for instance_id in report['incomplete_ids']:
|
||||
output_md += (
|
||||
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
|
||||
)
|
||||
|
||||
with open(output_md_filepath, 'w') as f:
|
||||
f.write(output_md)
|
||||
|
||||
else:
|
||||
print(
|
||||
f'No report file found: Both {swebench_official_report_json} and {openhands_remote_report_jsonl} do not exist.'
|
||||
)
|
||||
exit()
|
||||
|
||||
# Before backup and update, check if any changes would be made (unless --force is used)
|
||||
if not args.force:
|
||||
needs_update = False
|
||||
with open(args.input_file, 'r') as infile:
|
||||
for line in tqdm(infile, desc='Checking for changes'):
|
||||
data = json.loads(line)
|
||||
instance_id = data['instance_id']
|
||||
current_report = data.get('report', {})
|
||||
new_report = instance_id_to_status[
|
||||
instance_id
|
||||
] # if no report, it's not resolved
|
||||
if current_report != new_report:
|
||||
needs_update = True
|
||||
break
|
||||
|
||||
if not needs_update:
|
||||
print('No updates detected. Skipping file update.')
|
||||
exit()
|
||||
else:
|
||||
print('Force flag enabled. Updating all reports regardless of changes.')
|
||||
|
||||
# Backup and update the original file row by row
|
||||
if os.path.exists(args.input_file + '.bak'):
|
||||
if args.overwrite_backup:
|
||||
print(
|
||||
'Existing backup file found. Overwriting automatically due to --overwrite-backup flag.'
|
||||
)
|
||||
os.remove(args.input_file + '.bak')
|
||||
else:
|
||||
conf = input('Existing backup file found. Do you want to overwrite it? (y/n)')
|
||||
if conf != 'y':
|
||||
exit()
|
||||
os.remove(args.input_file + '.bak')
|
||||
|
||||
os.rename(args.input_file, args.input_file + '.bak')
|
||||
|
||||
# Process and write file row by row
|
||||
with (
|
||||
open(args.input_file + '.bak', 'r') as infile,
|
||||
open(args.input_file, 'w') as outfile,
|
||||
):
|
||||
for line in tqdm(infile, desc='Updating output file'):
|
||||
data = json.loads(line)
|
||||
instance_id = data['instance_id']
|
||||
data['report'] = instance_id_to_status[instance_id]
|
||||
outfile.write(json.dumps(data) + '\n')
|
||||
146
evaluation/benchmarks/multi_swe_bench/scripts/rollout_multi_swegym.sh
Executable file
146
evaluation/benchmarks/multi_swe_bench/scripts/rollout_multi_swegym.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NOTE: this script is for rolling out the Multi-SWE-Gym dataset for **TRAINING**
|
||||
# For more information, please refer to
|
||||
# 1. the Github Repo: https://github.com/SWE-Gym/SWE-Gym
|
||||
# 2. the paper: https://arxiv.org/abs/2412.21139
|
||||
|
||||
MODEL=$1 # eg your llm config name in config.toml (eg: "llm.claude-3-5-sonnet-20241022-t05")
|
||||
EXP_NAME=$2 # "train-t05"
|
||||
EVAL_DATASET=$3 # path to original dataset (jsonl file)
|
||||
N_WORKERS=${4:-64}
|
||||
N_RUNS=${5:-1}
|
||||
|
||||
export EXP_NAME=$EXP_NAME
|
||||
# use 2x resources for rollout since some codebases are pretty resource-intensive
|
||||
export DEFAULT_RUNTIME_RESOURCE_FACTOR=2
|
||||
echo "MODEL: $MODEL"
|
||||
echo "EXP_NAME: $EXP_NAME"
|
||||
echo "EVAL_DATASET: $EVAL_DATASET"
|
||||
# Generate DATASET path by adding _with_runtime_ before .jsonl extension
|
||||
DATASET="${EVAL_DATASET%.jsonl}_with_runtime_.jsonl" # path to converted dataset
|
||||
|
||||
# Create the converted dataset file
|
||||
echo "Creating converted dataset at: $DATASET"
|
||||
poetry run python ./evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py --input "$EVAL_DATASET" --output "$DATASET"
|
||||
|
||||
SPLIT="train"
|
||||
export LANGUAGE=java
|
||||
|
||||
if [ -z "$ALLHANDS_API_KEY" ] || [ "$RUNTIME" != "remote" ]; then
|
||||
echo "ALLHANDS_API_KEY is not set or RUNTIME is not set to remote. Will rollout and evaluate locally using Docker. WARNING: A large value of N_WORKERS will result in a large number of Docker containers being spun up and may crash your machine."
|
||||
export RUNTIME=docker
|
||||
else
|
||||
echo "ALLHANDS_API_KEY is set and RUNTIME is set to remote. Continuing rollout and evaluation with remote runtime..."
|
||||
export SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev"
|
||||
fi
|
||||
|
||||
#EVAL_LIMIT=3000
|
||||
MAX_ITER=100
|
||||
|
||||
|
||||
# ===== Run inference =====
|
||||
source "evaluation/utils/version_control.sh"
|
||||
get_openhands_version
|
||||
|
||||
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "DATASET: $DATASET"
|
||||
echo "EVAL_DOCKER_IMAGE_PREFIX: $EVAL_DOCKER_IMAGE_PREFIX"
|
||||
|
||||
# Default to NOT use Hint
|
||||
export USE_INSTANCE_IMAGE=true
|
||||
export USE_HINT_TEXT=false
|
||||
export RUN_WITH_BROWSING=false
|
||||
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
|
||||
EVAL_NOTE="$OPENHANDS_VERSION-no-hint-$EXP_NAME"
|
||||
|
||||
function run_eval() {
|
||||
local eval_note=$1
|
||||
export LANGUAGE=java
|
||||
echo "About to run command"
|
||||
COMMAND="EVAL_DOCKER_IMAGE_PREFIX=$EVAL_DOCKER_IMAGE_PREFIX; LANGUAGE=java;
|
||||
poetry run python evaluation/benchmarks/multi_swe_bench/run_infer.py \
|
||||
--agent-cls CodeActAgent \
|
||||
--llm-config $MODEL \
|
||||
--max-iterations $MAX_ITER \
|
||||
--eval-num-workers $N_WORKERS \
|
||||
--eval-note $eval_note \
|
||||
--dataset $DATASET \
|
||||
--split $SPLIT"
|
||||
|
||||
echo "Running command: $COMMAND"
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
}
|
||||
|
||||
for run_idx in $(seq 1 $N_RUNS); do
|
||||
|
||||
while true; do
|
||||
echo "### Running inference... ###"
|
||||
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
|
||||
current_eval_note="$EVAL_NOTE-run_$run_idx"
|
||||
echo "EVAL_NOTE: $current_eval_note"
|
||||
echo "DATASET command: $DATASET"
|
||||
#INFER_OUTPUT=$(run_eval $current_eval_note)
|
||||
INFER_OUTPUT=$(run_eval $current_eval_note | tee /dev/stderr)
|
||||
INFER_STATUS=$? # Capture the exit status of run_infer.sh
|
||||
echo "INFER_STATUS: $INFER_STATUS"
|
||||
|
||||
echo "### Cleaning up remote runtime... ###"
|
||||
./evaluation/utils/scripts/cleanup_remote_runtime.sh
|
||||
|
||||
if [ $INFER_STATUS -eq 0 ]; then
|
||||
echo "### Inference completed successfully. ###"
|
||||
break
|
||||
else
|
||||
echo "### Inference failed with exit code $INFER_STATUS. Retrying... ###"
|
||||
fi
|
||||
done
|
||||
|
||||
# Extract the output directory using the special delimiters
|
||||
OUTPUT_FILE=$(echo "$INFER_OUTPUT" | grep -o '### OUTPUT FILE:.* ###' | sed 's/### OUTPUT FILE: \(.*\) ###/\1/')
|
||||
echo "Got OUTPUT_FILE: $OUTPUT_FILE"
|
||||
|
||||
while true; do
|
||||
echo "### Evaluating on $OUTPUT_FILE ... ###"
|
||||
OUTPUT_CONFIG_FILE="${OUTPUT_FILE%.jsonl}_config.json"
|
||||
export EVAL_SKIP_BUILD_ERRORS=true
|
||||
pip install multi-swe-bench --quiet --disable-pip-version-check > /dev/null 2>&1
|
||||
COMMAND="poetry run python ./evaluation/benchmarks/multi_swe_bench/scripts/eval/update_multi_swe_bench_config.py --input $OUTPUT_FILE --output $OUTPUT_CONFIG_FILE --dataset $EVAL_DATASET;
|
||||
python -m multi_swe_bench.harness.run_evaluation --config $OUTPUT_CONFIG_FILE
|
||||
"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
echo "Running command: $COMMAND"
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
EVAL_STATUS=$?
|
||||
if [ $EVAL_STATUS -eq 0 ]; then
|
||||
echo "### Evaluation completed successfully. ###"
|
||||
break
|
||||
else
|
||||
echo "### Evaluation failed with exit code $EVAL_STATUS. Retrying... ###"
|
||||
fi
|
||||
|
||||
./evaluation/utils/scripts/cleanup_remote_runtime.sh
|
||||
done
|
||||
|
||||
# update the output with evaluation results
|
||||
echo "### Updating the output with evaluation results... ###"
|
||||
poetry run python evaluation/benchmarks/multi_swe_bench/scripts/eval/update_output_with_eval.py $OUTPUT_FILE
|
||||
|
||||
echo "### Combining the final completions... ###"
|
||||
poetry run python evaluation/benchmarks/multi_swe_bench/scripts/eval/combine_final_completions.py $OUTPUT_FILE
|
||||
|
||||
echo "### DONE for run $run_idx! ###"
|
||||
echo "You can find the final output at $(dirname $OUTPUT_FILE)/$FINAL_OUTPUT_FILE"
|
||||
done
|
||||
@@ -47,8 +47,8 @@ if [ -z "$DATASET" ]; then
|
||||
fi
|
||||
|
||||
if [ -z "$LANGUAGE" ]; then
|
||||
echo "LANUGUAGE not specified, use default python"
|
||||
LANGUAGE="python"
|
||||
echo "LANGUAGE not specified, use default python"
|
||||
LANGUAGE="java"
|
||||
fi
|
||||
|
||||
if [ -z "$SPLIT" ]; then
|
||||
@@ -69,10 +69,10 @@ fi
|
||||
|
||||
if [ -z "$EVAL_DOCKER_IMAGE_PREFIX" ]; then
|
||||
if [ "$LANGUAGE" = "python" ]; then
|
||||
echo "EVAL_DOCKER_IMAGE_PREFIX is docker.io/xingyaoww/ as default as LANUGUAGE is python"
|
||||
echo "EVAL_DOCKER_IMAGE_PREFIX is docker.io/xingyaoww/ as default as LANGUAGE is python"
|
||||
EVAL_DOCKER_IMAGE_PREFIX="docker.io/xingyaoww/"
|
||||
elif [ "$LANGUAGE" = "java" ]; then
|
||||
echo "EVAL_DOCKER_IMAGE_PREFIX is java_verified as LANUGUAGE is java"
|
||||
echo "EVAL_DOCKER_IMAGE_PREFIX is empty as LANGUAGE is java"
|
||||
EVAL_DOCKER_IMAGE_PREFIX=""
|
||||
fi
|
||||
fi
|
||||
|
||||
344
evaluation/benchmarks/multi_swe_bench/scripts/swegym/convert_data.ipynb
vendored
Normal file
344
evaluation/benchmarks/multi_swe_bench/scripts/swegym/convert_data.ipynb
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"\n",
|
||||
"import pandas as pd\n",
|
||||
"from tqdm import tqdm\n",
|
||||
"\n",
|
||||
"tqdm.pandas()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 1. Load raw data and convert to training data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import gzip\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"from tqdm import tqdm\n",
|
||||
"\n",
|
||||
"FILE_PATHS = [\n",
|
||||
" 'YOURPATH-no-hint-train-t05-run_1/output.with_completions.jsonl.gz',\n",
|
||||
" 'YOURPATH-no-hint-train-t05-run_2/output.with_completions.jsonl.gz',\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# More memory efficient for large files\n",
|
||||
"# Initialize lists to store the data\n",
|
||||
"data = []\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Read file line by line\n",
|
||||
"for FILE_PATH in FILE_PATHS:\n",
|
||||
" with gzip.open(FILE_PATH, 'rb') as f: # Use 'rb' for gzipped files\n",
|
||||
" for i, line in tqdm(\n",
|
||||
" enumerate(f), desc=f'Processing {FILE_PATH.split(\"/\")[-1]}'\n",
|
||||
" ):\n",
|
||||
" # Parse only the fields we need\n",
|
||||
" raw_data = json.loads(line)\n",
|
||||
" data.append(\n",
|
||||
" {\n",
|
||||
" 'resolved': raw_data['report']['resolved'],\n",
|
||||
" 'messages': raw_data['raw_completions']['messages']\n",
|
||||
" if raw_data['raw_completions'] is not None\n",
|
||||
" else None,\n",
|
||||
" 'git_patch': raw_data['test_result'].get('git_patch', ''),\n",
|
||||
" 'tools': raw_data['raw_completions']['tools']\n",
|
||||
" if raw_data['raw_completions'] is not None\n",
|
||||
" and 'tools' in raw_data['raw_completions']\n",
|
||||
" else None,\n",
|
||||
" }\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"# Convert to DataFrame after collecting all data\n",
|
||||
"df = pd.DataFrame(data)\n",
|
||||
"print(f'#total amount of data={len(df)}')\n",
|
||||
"df = df[~df['messages'].isna()]\n",
|
||||
"print(f'#total amount of data after removing nan={len(df)}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def _contains_multiple_tool_calls(messages: list[dict]) -> bool:\n",
|
||||
" return any(\n",
|
||||
" message.get('tool_calls') and len(message['tool_calls']) > 1\n",
|
||||
" for message in messages\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"df['contains_multiple_tool_calls'] = df['messages'].apply(_contains_multiple_tool_calls)\n",
|
||||
"display(df.groupby(['contains_multiple_tool_calls'])['resolved'].sum())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import copy\n",
|
||||
"\n",
|
||||
"# Convert function calling messages to non-function calling messages\n",
|
||||
"from openhands.llm.fn_call_converter import (\n",
|
||||
" FunctionCallConversionError,\n",
|
||||
" convert_fncall_messages_to_non_fncall_messages,\n",
|
||||
" convert_from_multiple_tool_calls_to_single_tool_call_messages,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"total_failed = 0\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _convert_messages(messages: list[dict], tools: list[dict]) -> list[dict]:\n",
|
||||
" global total_failed\n",
|
||||
" message_copy = copy.deepcopy(messages)\n",
|
||||
" for message in message_copy:\n",
|
||||
" if message['content'] is None:\n",
|
||||
" message['content'] = ''\n",
|
||||
" try:\n",
|
||||
" return convert_fncall_messages_to_non_fncall_messages(\n",
|
||||
" message_copy, tools, add_in_context_learning_example=False\n",
|
||||
" )\n",
|
||||
" except FunctionCallConversionError:\n",
|
||||
" total_failed += 1\n",
|
||||
" # print(f'Failed to convert messages: {messages}\\nTools: {tools}')\n",
|
||||
" # traceback.print_exc()\n",
|
||||
" return None\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"df['converted_messages'] = df.apply(\n",
|
||||
" lambda row: convert_from_multiple_tool_calls_to_single_tool_call_messages(\n",
|
||||
" row['messages'], ignore_final_tool_result=True\n",
|
||||
" ),\n",
|
||||
" axis=1,\n",
|
||||
")\n",
|
||||
"df['nonfncall_messages'] = df.apply(\n",
|
||||
" lambda row: _convert_messages(row['converted_messages'], row['tools']), axis=1\n",
|
||||
")\n",
|
||||
"print('total nan', df['nonfncall_messages'].isna().sum())\n",
|
||||
"df = df[~df['nonfncall_messages'].isna()]\n",
|
||||
"print(df['nonfncall_messages'].iloc[0])\n",
|
||||
"\n",
|
||||
"print(f'Total failed: {total_failed}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Tokenization"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pandarallel import pandarallel\n",
|
||||
"from transformers import AutoTokenizer\n",
|
||||
"\n",
|
||||
"os.environ['TOKENIZERS_PARALLELISM'] = 'false'\n",
|
||||
"pandarallel.initialize(progress_bar=True, verbose=1, nb_workers=16)\n",
|
||||
"tokenizer = AutoTokenizer.from_pretrained('Qwen/Qwen2.5-7B-Instruct')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def clean_messages(messages):\n",
|
||||
" clean = []\n",
|
||||
" for msg in messages:\n",
|
||||
" if not isinstance(msg, dict):\n",
|
||||
" continue\n",
|
||||
" role = msg.get('role')\n",
|
||||
" content = msg.get('content')\n",
|
||||
" if isinstance(content, str):\n",
|
||||
" text = content\n",
|
||||
" elif isinstance(content, dict):\n",
|
||||
" text = content.get('text')\n",
|
||||
" elif (\n",
|
||||
" isinstance(content, list)\n",
|
||||
" and len(content) == 1\n",
|
||||
" and isinstance(content[0], dict)\n",
|
||||
" ):\n",
|
||||
" text = content[0].get('text')\n",
|
||||
" else:\n",
|
||||
" print(f'Format not accepted {content}')\n",
|
||||
" clean.append({'role': role, 'content': text})\n",
|
||||
" return clean\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Step 1: Clean the messages\n",
|
||||
"df['nonfncall_messages'] = df['nonfncall_messages'].apply(clean_messages)\n",
|
||||
"\n",
|
||||
"# Step 2: Compute token count\n",
|
||||
"df['n_tokens'] = df['nonfncall_messages'].parallel_apply(\n",
|
||||
" lambda x: len(tokenizer.apply_chat_template(x))\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# print(df['nonfncall_messages'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(f'BEFORE: #total={len(df)}')\n",
|
||||
"df_selected = df[df['n_tokens'] < 131072]\n",
|
||||
"print(f'AFTER(truncated to 128k): #total={len(df_selected)}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df_selected['n_tokens'].describe()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# ecdf of n_tokens\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import seaborn as sns\n",
|
||||
"\n",
|
||||
"display(df.groupby(['resolved'])['n_tokens'].describe())\n",
|
||||
"sns.ecdfplot(x='n_tokens', data=df, hue='resolved')\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"print(f'#total={len(df)}')\n",
|
||||
"df_selected = df[df['n_tokens'] < 131072]\n",
|
||||
"print(f'#selected={len(df_selected)}')\n",
|
||||
"display(df_selected.groupby(['resolved'])['n_tokens'].describe())\n",
|
||||
"sns.ecdfplot(x='n_tokens', data=df_selected, hue='resolved')\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df_selected[~df_selected['resolved']]['n_tokens'].describe()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df_selected['resolved'].value_counts()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df_selected.groupby(['resolved'])['n_tokens'].describe()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Save Resolved Messages for SFT"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Flatten messages and change format to {\"content\": \"\", \"role\": \"\"}\n",
|
||||
"df_selected[df_selected['resolved']][['nonfncall_messages']].rename(\n",
|
||||
" columns={'nonfncall_messages': 'messages'}\n",
|
||||
").to_json(\n",
|
||||
" os.path.join(\n",
|
||||
" 'PATH_TO_FILE',\n",
|
||||
" f'policy_traj_128k_swegym_{df_selected[\"resolved\"].value_counts()[True]}i.jsonl',\n",
|
||||
" ),\n",
|
||||
" lines=True,\n",
|
||||
" orient='records',\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
81
evaluation/benchmarks/swe_perf/README.md
Normal file
81
evaluation/benchmarks/swe_perf/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# SWE-Perf Evaluation
|
||||
|
||||
This folder contains the OpenHands inference generation of the [SWE-Perf benchmark](https://swe-perf.github.io/) ([paper](https://arxiv.org/pdf/2507.12415v1)).
|
||||
|
||||
The evaluation consists of three steps:
|
||||
|
||||
1. Environment setup: [install python environment](../../README.md#development-environment) and [configure LLM config](../../README.md#configure-openhands-and-your-llm).
|
||||
2. [Run inference](#running-inference-locally-with-docker): Generate a edit patch for each Github issue
|
||||
3. [Evaluate patches](#evaluate-generated-patches)
|
||||
|
||||
## Setup Environment and LLM Configuration
|
||||
|
||||
Please follow instruction [here](../../README.md#setup) to setup your local development environment and LLM.
|
||||
|
||||
## Running inference Locally with Docker
|
||||
|
||||
Make sure your Docker daemon is running, and you have ample disk space (at least 200-500GB, depends on the SWE-PErf set you are running on) for the instance-level docker image.
|
||||
|
||||
When the `run_infer.sh` script is started, it will automatically pull the relevant SWE-Perf images.
|
||||
For example, for instance ID `scikit-learn_scikit-learn-11674`, it will try to pull our pre-build docker image `betty1202/sweb.eval.x86_64.scikit-learn_s_scikit-learn-11674` from DockerHub.
|
||||
This image will be used create an OpenHands runtime image where the agent will operate on.
|
||||
|
||||
```bash
|
||||
./evaluation/benchmarks/swe_perf/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split] [n_runs] [mode]
|
||||
|
||||
# Example
|
||||
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 500 100 1 SWE-Perf/SWE-Perf test
|
||||
```
|
||||
|
||||
where `model_config` is mandatory, and the rest are optional.
|
||||
|
||||
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
|
||||
LLM settings, as defined in your `config.toml`.
|
||||
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version you would
|
||||
like to evaluate. It could also be a release tag like `0.6.2`.
|
||||
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
|
||||
to `CodeActAgent`.
|
||||
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances. By
|
||||
default, the script evaluates the entire SWE-Perf test set (140 issues). Note:
|
||||
in order to use `eval_limit`, you must also set `agent`.
|
||||
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
|
||||
default, it is set to 100.
|
||||
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
|
||||
default, it is set to 1.
|
||||
- `dataset`, a huggingface dataset name. e.g. `SWE-Perf/SWE-Perf`, specifies which dataset to evaluate on.
|
||||
- `dataset_split`, split for the huggingface dataset. e.g., `test`, `dev`. Default to `test`.
|
||||
|
||||
- `n_runs`, e.g. `3`, is the number of times to run the evaluation. Default is 1.
|
||||
- `mode`, e.g. `swt`, `swt-ci`, or `swe`, specifies the evaluation mode. Default is `swe`.
|
||||
|
||||
> [!CAUTION]
|
||||
> Setting `num_workers` larger than 1 is not officially tested, YMMV.
|
||||
|
||||
|
||||
Let's say you'd like to run 10 instances using `llm.eval_gpt4_1106_preview` and CodeActAgent,
|
||||
|
||||
then your command would be:
|
||||
|
||||
```bash
|
||||
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 10
|
||||
```
|
||||
|
||||
## Evaluate Generated Patches
|
||||
|
||||
|
||||
To evaluate the generated patch, follow these steps:
|
||||
|
||||
### 1. Convert output to the evaluation standard format
|
||||
Run the following command:
|
||||
```bash
|
||||
python -m evaluation.benchmarks.swe_perf.format_conversion \
|
||||
--input_path [input_path] \
|
||||
--output_path [output_path]
|
||||
```
|
||||
|
||||
* `input_path`: Path to the raw generated patch file.
|
||||
* `output_path`: Path where the converted file will be saved.
|
||||
|
||||
### 2. Run the SWE-Perf benchmark official evaluation
|
||||
|
||||
Once the output is converted, use the [official SWE-Perf benchmark evaluation](https://github.com/SWE-Perf/SWE-Perf/tree/main/evaluation) to evaluate it.
|
||||
0
evaluation/benchmarks/swe_perf/__init__.py
Normal file
0
evaluation/benchmarks/swe_perf/__init__.py
Normal file
52
evaluation/benchmarks/swe_perf/binary_patch_utils.py
Normal file
52
evaluation/benchmarks/swe_perf/binary_patch_utils.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Utilities for handling binary files and patch generation in SWE-Perf 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()
|
||||
45
evaluation/benchmarks/swe_perf/format_conversion.py
Normal file
45
evaluation/benchmarks/swe_perf/format_conversion.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--input_path', type=str, help='Name of input path to JSON file.')
|
||||
parser.add_argument('--output_path', type=str, help='Name of output path to JSON file.')
|
||||
args = parser.parse_args()
|
||||
|
||||
input_path = args.input_path
|
||||
output_path = args.output_path
|
||||
os.makedirs(output_path, exist_ok=True)
|
||||
|
||||
|
||||
def load_jsonl(file_path):
|
||||
"""Load JSONL file into a list of dictionaries."""
|
||||
data = []
|
||||
with open(file_path, 'r') as f:
|
||||
for line in f:
|
||||
data.append(json.loads(line))
|
||||
return data
|
||||
|
||||
|
||||
dataset = load_jsonl(input_path)
|
||||
ooutput_dataset = []
|
||||
for data in dataset:
|
||||
instance_id = data['instance_id']
|
||||
model_name_or_path = 'openhands'
|
||||
model_patch = (
|
||||
data['test_result']['git_patch']
|
||||
if 'test_result' in data and 'git_patch' in data['test_result']
|
||||
else None
|
||||
)
|
||||
ooutput_dataset.append(
|
||||
{
|
||||
'instance_id': instance_id,
|
||||
'model_name_or_path': model_name_or_path,
|
||||
'model_patch': model_patch,
|
||||
}
|
||||
)
|
||||
|
||||
with open(os.path.join(output_path, 'output.jsonl'), 'w') as f:
|
||||
for item in ooutput_dataset:
|
||||
json_line = json.dumps(item, ensure_ascii=False)
|
||||
f.write(json_line + '\n')
|
||||
39
evaluation/benchmarks/swe_perf/resource/mapping.py
Normal file
39
evaluation/benchmarks/swe_perf/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))
|
||||
842
evaluation/benchmarks/swe_perf/resource/swt_bench_constants.py
Normal file
842
evaluation/benchmarks/swe_perf/resource/swt_bench_constants.py
Normal file
@@ -0,0 +1,842 @@
|
||||
# Based on https://github.com/logic-star-ai/swt-bench/blob/master/src/constants.py
|
||||
|
||||
# Constants - Installation Specifications
|
||||
MAP_VERSION_TO_INSTALL_SKLEARN = {
|
||||
k: {
|
||||
'python': '3.6',
|
||||
'packages': 'numpy scipy cython pytest pandas matplotlib',
|
||||
'install': 'python -m pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'pip_packages': [
|
||||
'cython',
|
||||
'numpy==1.19.2',
|
||||
'setuptools',
|
||||
'scipy==1.5.2',
|
||||
],
|
||||
}
|
||||
for k in ['0.20', '0.21', '0.22']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_SKLEARN.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': "'numpy==1.19.2' 'scipy==1.5.2' 'cython==3.0.10' pytest 'pandas<2.0.0' 'matplotlib<3.9.0' setuptools pytest joblib threadpoolctl",
|
||||
'install': 'python -m pip install -v --no-use-pep517 --no-build-isolation -e .',
|
||||
'pip_packages': ['cython', 'setuptools', 'numpy', 'scipy'],
|
||||
}
|
||||
for k in ['1.3', '1.4']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_FLASK = {
|
||||
'2.0': {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': [
|
||||
'setuptools==70.0.0',
|
||||
'Werkzeug==2.3.7',
|
||||
'Jinja2==3.0.1',
|
||||
'itsdangerous==2.1.2',
|
||||
'click==8.0.1',
|
||||
'MarkupSafe==2.1.3',
|
||||
],
|
||||
},
|
||||
'2.1': {
|
||||
'python': '3.10',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': [
|
||||
'click==8.1.3',
|
||||
'itsdangerous==2.1.2',
|
||||
'Jinja2==3.1.2',
|
||||
'MarkupSafe==2.1.1',
|
||||
'Werkzeug==2.3.7',
|
||||
],
|
||||
},
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_FLASK.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.11',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': [
|
||||
'click==8.1.3',
|
||||
'itsdangerous==2.1.2',
|
||||
'Jinja2==3.1.2',
|
||||
'MarkupSafe==2.1.1',
|
||||
'Werkzeug==2.3.7',
|
||||
],
|
||||
}
|
||||
for k in ['2.2', '2.3']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_DJANGO = {
|
||||
k: {
|
||||
'python': '3.5',
|
||||
'packages': 'requirements.txt',
|
||||
'pre_install': [
|
||||
'apt-get update && apt-get install -y locales',
|
||||
"echo 'en_US UTF-8' > /etc/locale.gen",
|
||||
'locale-gen en_US.UTF-8',
|
||||
],
|
||||
'install': 'python setup.py install',
|
||||
'pip_packages': ['setuptools'],
|
||||
'eval_commands': [
|
||||
'export LANG=en_US.UTF-8',
|
||||
'export LC_ALL=en_US.UTF-8',
|
||||
'export PYTHONIOENCODING=utf8',
|
||||
'export LANGUAGE=en_US:en',
|
||||
],
|
||||
}
|
||||
for k in ['1.7', '1.8', '1.9', '1.10', '1.11', '2.0', '2.1', '2.2']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_DJANGO.update(
|
||||
{
|
||||
k: {'python': '3.5', 'install': 'python setup.py install'}
|
||||
for k in ['1.4', '1.5', '1.6']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_DJANGO.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.6',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'eval_commands': [
|
||||
"sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen",
|
||||
'export LANG=en_US.UTF-8',
|
||||
'export LANGUAGE=en_US:en',
|
||||
'export LC_ALL=en_US.UTF-8',
|
||||
],
|
||||
}
|
||||
for k in ['3.0', '3.1', '3.2']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_DJANGO.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.8',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in ['4.0']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_DJANGO.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in ['4.1', '4.2']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_DJANGO.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.11',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in ['5.0']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_REQUESTS = {
|
||||
k: {'python': '3.9', 'packages': 'pytest', 'install': 'python -m pip install .'}
|
||||
for k in ['0.7', '0.8', '0.9', '0.11', '0.13', '0.14', '1.1', '1.2', '2.0', '2.2']
|
||||
+ ['2.3', '2.4', '2.5', '2.7', '2.8', '2.9', '2.10', '2.11', '2.12', '2.17']
|
||||
+ ['2.18', '2.19', '2.22', '2.26', '2.25', '2.27', '3.0']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_SEABORN = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': [
|
||||
'contourpy==1.1.0',
|
||||
'cycler==0.11.0',
|
||||
'fonttools==4.42.1',
|
||||
'importlib-resources==6.0.1',
|
||||
'kiwisolver==1.4.5',
|
||||
'matplotlib==3.7.2',
|
||||
'numpy==1.25.2',
|
||||
'packaging==23.1',
|
||||
'pandas==1.3.5', # 2.0.3
|
||||
'pillow==10.0.0',
|
||||
'pyparsing==3.0.9',
|
||||
'pytest',
|
||||
'python-dateutil==2.8.2',
|
||||
'pytz==2023.3.post1',
|
||||
'scipy==1.11.2',
|
||||
'six==1.16.0',
|
||||
'tzdata==2023.1',
|
||||
'zipp==3.16.2',
|
||||
],
|
||||
}
|
||||
for k in ['0.11']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_SEABORN.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': 'python -m pip install -e .[dev]',
|
||||
'pip_packages': [
|
||||
'contourpy==1.1.0',
|
||||
'cycler==0.11.0',
|
||||
'fonttools==4.42.1',
|
||||
'importlib-resources==6.0.1',
|
||||
'kiwisolver==1.4.5',
|
||||
'matplotlib==3.7.2',
|
||||
'numpy==1.25.2',
|
||||
'packaging==23.1',
|
||||
'pandas==2.0.0',
|
||||
'pillow==10.0.0',
|
||||
'pyparsing==3.0.9',
|
||||
'pytest',
|
||||
'python-dateutil==2.8.2',
|
||||
'pytz==2023.3.post1',
|
||||
'scipy==1.11.2',
|
||||
'six==1.16.0',
|
||||
'tzdata==2023.1',
|
||||
'zipp==3.16.2',
|
||||
],
|
||||
}
|
||||
for k in ['0.12', '0.13']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_PYTEST = {
|
||||
k: {'python': '3.9', 'install': 'python -m pip install -e .'}
|
||||
for k in [
|
||||
'4.4',
|
||||
'4.5',
|
||||
'4.6',
|
||||
'5.0',
|
||||
'5.1',
|
||||
'5.2',
|
||||
'5.3',
|
||||
'5.4',
|
||||
'6.0',
|
||||
'6.2',
|
||||
'6.3',
|
||||
'7.0',
|
||||
'7.1',
|
||||
'7.2',
|
||||
'7.4',
|
||||
'8.0',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['4.4']['pip_packages'] = [
|
||||
'atomicwrites==1.4.1',
|
||||
'attrs==23.1.0',
|
||||
'more-itertools==10.1.0',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'setuptools==68.0.0',
|
||||
'six==1.16.0',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['4.5']['pip_packages'] = [
|
||||
'atomicwrites==1.4.1',
|
||||
'attrs==23.1.0',
|
||||
'more-itertools==10.1.0',
|
||||
'pluggy==0.11.0',
|
||||
'py==1.11.0',
|
||||
'setuptools==68.0.0',
|
||||
'six==1.16.0',
|
||||
'wcwidth==0.2.6',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['4.6']['pip_packages'] = [
|
||||
'atomicwrites==1.4.1',
|
||||
'attrs==23.1.0',
|
||||
'more-itertools==10.1.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'six==1.16.0',
|
||||
'wcwidth==0.2.6',
|
||||
]
|
||||
for k in ['5.0', '5.1', '5.2']:
|
||||
MAP_VERSION_TO_INSTALL_PYTEST[k]['pip_packages'] = [
|
||||
'atomicwrites==1.4.1',
|
||||
'attrs==23.1.0',
|
||||
'more-itertools==10.1.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'wcwidth==0.2.6',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['5.3']['pip_packages'] = [
|
||||
'attrs==23.1.0',
|
||||
'more-itertools==10.1.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'wcwidth==0.2.6',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['5.4']['pip_packages'] = [
|
||||
'py==1.11.0',
|
||||
'packaging==23.1',
|
||||
'attrs==23.1.0',
|
||||
'more-itertools==10.1.0',
|
||||
'pluggy==0.13.1',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['6.0']['pip_packages'] = [
|
||||
'attrs==23.1.0',
|
||||
'iniconfig==2.0.0',
|
||||
'more-itertools==10.1.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'toml==0.10.2',
|
||||
]
|
||||
for k in ['6.2', '6.3']:
|
||||
MAP_VERSION_TO_INSTALL_PYTEST[k]['pip_packages'] = [
|
||||
'attrs==23.1.0',
|
||||
'iniconfig==2.0.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'toml==0.10.2',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['7.0']['pip_packages'] = [
|
||||
'attrs==23.1.0',
|
||||
'iniconfig==2.0.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
]
|
||||
for k in ['7.1', '7.2']:
|
||||
MAP_VERSION_TO_INSTALL_PYTEST[k]['pip_packages'] = [
|
||||
'attrs==23.1.0',
|
||||
'iniconfig==2.0.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==0.13.1',
|
||||
'py==1.11.0',
|
||||
'tomli==2.0.1',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['7.4']['pip_packages'] = [
|
||||
'iniconfig==2.0.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==1.3.0',
|
||||
'exceptiongroup==1.1.3',
|
||||
'tomli==2.0.1',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYTEST['8.0']['pip_packages'] = [
|
||||
'iniconfig==2.0.0',
|
||||
'packaging==23.1',
|
||||
'pluggy==1.3.0',
|
||||
'exceptiongroup==1.1.3',
|
||||
'tomli==2.0.1',
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_MATPLOTLIB = {
|
||||
k: {
|
||||
'python': '3.11',
|
||||
'packages': 'environment.yml',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pre_install': [
|
||||
'apt-get -y update && apt-get -y upgrade && apt-get install -y imagemagick ffmpeg texlive texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-luatex cm-super dvipng'
|
||||
],
|
||||
'pip_packages': [
|
||||
'contourpy==1.1.0',
|
||||
'cycler==0.11.0',
|
||||
'fonttools==4.42.1',
|
||||
'ghostscript',
|
||||
'kiwisolver==1.4.5',
|
||||
'numpy==1.25.2',
|
||||
'packaging==23.1',
|
||||
'pillow==10.0.0',
|
||||
'pikepdf',
|
||||
'pyparsing==3.0.9',
|
||||
'python-dateutil==2.8.2',
|
||||
'six==1.16.0',
|
||||
'setuptools==68.1.2',
|
||||
'setuptools-scm==7.1.0',
|
||||
'typing-extensions==4.7.1',
|
||||
],
|
||||
}
|
||||
for k in ['3.5', '3.6', '3.7']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_MATPLOTLIB.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.8',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pre_install': [
|
||||
'apt-get -y update && apt-get -y upgrade && apt-get install -y imagemagick ffmpeg libfreetype6-dev pkg-config texlive texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-luatex cm-super'
|
||||
],
|
||||
'pip_packages': ['pytest', 'ipython'],
|
||||
}
|
||||
for k in ['3.1', '3.2', '3.3', '3.4']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_MATPLOTLIB.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.7',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pre_install': [
|
||||
'apt-get -y update && apt-get -y upgrade && apt-get install -y imagemagick ffmpeg libfreetype6-dev pkg-config'
|
||||
],
|
||||
'pip_packages': ['pytest'],
|
||||
}
|
||||
for k in ['3.0']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_MATPLOTLIB.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.5',
|
||||
'install': 'python setup.py build; python setup.py install',
|
||||
'pre_install': [
|
||||
'apt-get -y update && apt-get -y upgrade && && apt-get install -y imagemagick ffmpeg'
|
||||
],
|
||||
'pip_packages': ['pytest'],
|
||||
'execute_test_as_nonroot': True,
|
||||
}
|
||||
for k in ['2.0', '2.1', '2.2', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_SPHINX = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'pip_packages': ['tox==4.16.0', 'tox-current-env==0.0.11'],
|
||||
'install': 'python -m pip install -e .[test]',
|
||||
'pre_install': ["sed -i 's/pytest/pytest -rA/' tox.ini"],
|
||||
}
|
||||
for k in ['1.5', '1.6', '1.7', '1.8', '2.0', '2.1', '2.2', '2.3', '2.4', '3.0']
|
||||
+ ['3.1', '3.2', '3.3', '3.4', '3.5', '4.0', '4.1', '4.2', '4.3', '4.4']
|
||||
+ ['4.5', '5.0', '5.1', '5.2', '5.3', '6.0', '6.2', '7.0', '7.1', '7.2']
|
||||
}
|
||||
for k in ['3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '4.0', '4.1', '4.2', '4.3', '4.4']:
|
||||
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
|
||||
[
|
||||
"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",
|
||||
]
|
||||
)
|
||||
if k in ['4.2', '4.3', '4.4']:
|
||||
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
|
||||
[
|
||||
"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",
|
||||
]
|
||||
)
|
||||
elif k == '4.1':
|
||||
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
|
||||
[
|
||||
(
|
||||
"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"
|
||||
),
|
||||
]
|
||||
)
|
||||
else:
|
||||
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
|
||||
[
|
||||
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py",
|
||||
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py",
|
||||
]
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_SPHINX['7.2']['pre_install'] += [
|
||||
'apt-get update && apt-get install -y graphviz'
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_ASTROPY = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': 'python -m pip install -e .[test] --verbose',
|
||||
'pip_packages': [
|
||||
'attrs==23.1.0',
|
||||
'exceptiongroup==1.1.3',
|
||||
'execnet==2.0.2',
|
||||
'hypothesis==6.82.6',
|
||||
'iniconfig==2.0.0',
|
||||
'numpy==1.25.2',
|
||||
'packaging==23.1',
|
||||
'pluggy==1.3.0',
|
||||
'psutil==5.9.5',
|
||||
'pyerfa==2.0.0.3',
|
||||
'pytest-arraydiff==0.5.0',
|
||||
'pytest-astropy-header==0.2.2',
|
||||
'pytest-astropy==0.10.0',
|
||||
'pytest-cov==4.1.0',
|
||||
'pytest-doctestplus==1.0.0',
|
||||
'pytest-filter-subpackage==0.1.2',
|
||||
'pytest-mock==3.11.1',
|
||||
'pytest-openfiles==0.5.0',
|
||||
'pytest-remotedata==0.4.0',
|
||||
'pytest-xdist==3.3.1',
|
||||
'pytest==7.4.0',
|
||||
'PyYAML==6.0.1',
|
||||
'setuptools==68.0.0',
|
||||
'sortedcontainers==2.4.0',
|
||||
'tomli==2.0.1',
|
||||
],
|
||||
}
|
||||
for k in ['0.1', '0.2', '0.3', '0.4', '1.1', '1.2', '1.3', '3.0', '3.1', '3.2']
|
||||
+ ['4.1', '4.2', '4.3', '5.0', '5.1', '5.2']
|
||||
}
|
||||
for k in ['4.1', '4.2', '4.3', '5.0', '5.1', '5.2']:
|
||||
MAP_VERSION_TO_INSTALL_ASTROPY[k]['pre_install'] = [
|
||||
'sed -i \'s/requires = \\["setuptools",/requires = \\["setuptools==68.0.0",/\' pyproject.toml'
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_SYMPY = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'mpmath flake8',
|
||||
'pip_packages': ['mpmath==1.3.0', 'flake8-comprehensions'],
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in ['0.7', '1.0', '1.1', '1.10', '1.11', '1.12', '1.2', '1.4', '1.5', '1.6']
|
||||
+ ['1.7', '1.8', '1.9']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_SYMPY.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': ['mpmath==1.3.0'],
|
||||
}
|
||||
for k in ['1.13']
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_PYLINT = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in [
|
||||
'2.10',
|
||||
'2.11',
|
||||
'2.13',
|
||||
'2.14',
|
||||
'2.15',
|
||||
'2.16',
|
||||
'2.17',
|
||||
'2.8',
|
||||
'2.9',
|
||||
'3.0',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PYLINT['2.8']['pip_packages'] = ['pyenchant==3.2']
|
||||
MAP_VERSION_TO_INSTALL_PYLINT['2.8']['pre_install'] = [
|
||||
'apt-get update && apt-get install -y libenchant-2-dev hunspell-en-us'
|
||||
]
|
||||
MAP_VERSION_TO_INSTALL_PYLINT.update(
|
||||
{
|
||||
k: {
|
||||
**MAP_VERSION_TO_INSTALL_PYLINT[k],
|
||||
'pip_packages': ['astroid==3.0.0a6', 'setuptools'],
|
||||
}
|
||||
for k in ['3.0']
|
||||
}
|
||||
)
|
||||
|
||||
MAP_VERSION_TO_INSTALL_XARRAY = {
|
||||
k: {
|
||||
'python': '3.10',
|
||||
'packages': 'environment.yml',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': [
|
||||
'numpy==1.23.0',
|
||||
'packaging==23.1',
|
||||
'pandas==1.5.3',
|
||||
'pytest==7.4.0',
|
||||
'python-dateutil==2.8.2',
|
||||
'pytz==2023.3',
|
||||
'six==1.16.0',
|
||||
'scipy==1.11.1',
|
||||
'setuptools==68.0.0',
|
||||
],
|
||||
'no_use_env': True,
|
||||
}
|
||||
for k in ['0.12', '0.18', '0.19', '0.20', '2022.03', '2022.06', '2022.09']
|
||||
}
|
||||
|
||||
MAP_VERSION_TO_INSTALL_SQLFLUFF = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in [
|
||||
'0.10',
|
||||
'0.11',
|
||||
'0.12',
|
||||
'0.13',
|
||||
'0.4',
|
||||
'0.5',
|
||||
'0.6',
|
||||
'0.8',
|
||||
'0.9',
|
||||
'1.0',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3',
|
||||
'1.4',
|
||||
'2.0',
|
||||
'2.1',
|
||||
'2.2',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_DBT_CORE = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
}
|
||||
for k in [
|
||||
'0.13',
|
||||
'0.14',
|
||||
'0.15',
|
||||
'0.16',
|
||||
'0.17',
|
||||
'0.18',
|
||||
'0.19',
|
||||
'0.20',
|
||||
'0.21',
|
||||
'1.0',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3',
|
||||
'1.4',
|
||||
'1.5',
|
||||
'1.6',
|
||||
'1.7',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PYVISTA = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': ['pytest'],
|
||||
}
|
||||
for k in ['0.20', '0.21', '0.22', '0.23']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PYVISTA.update(
|
||||
{
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'packages': 'requirements.txt',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': ['pytest'],
|
||||
}
|
||||
for k in [
|
||||
'0.24',
|
||||
'0.25',
|
||||
'0.26',
|
||||
'0.27',
|
||||
'0.28',
|
||||
'0.29',
|
||||
'0.30',
|
||||
'0.31',
|
||||
'0.32',
|
||||
'0.33',
|
||||
'0.34',
|
||||
'0.35',
|
||||
'0.36',
|
||||
'0.37',
|
||||
'0.38',
|
||||
'0.39',
|
||||
'0.40',
|
||||
'0.41',
|
||||
'0.42',
|
||||
'0.43',
|
||||
]
|
||||
}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_ASTROID = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': 'python -m pip install -e .',
|
||||
'pip_packages': ['pytest'],
|
||||
}
|
||||
for k in [
|
||||
'2.10',
|
||||
'2.12',
|
||||
'2.13',
|
||||
'2.14',
|
||||
'2.15',
|
||||
'2.16',
|
||||
'2.5',
|
||||
'2.6',
|
||||
'2.7',
|
||||
'2.8',
|
||||
'2.9',
|
||||
'3.0',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_MARSHMALLOW = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': "python -m pip install -e '.[dev]'",
|
||||
}
|
||||
for k in [
|
||||
'2.18',
|
||||
'2.19',
|
||||
'2.20',
|
||||
'3.0',
|
||||
'3.1',
|
||||
'3.10',
|
||||
'3.11',
|
||||
'3.12',
|
||||
'3.13',
|
||||
'3.15',
|
||||
'3.16',
|
||||
'3.19',
|
||||
'3.2',
|
||||
'3.4',
|
||||
'3.8',
|
||||
'3.9',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PVLIB = {
|
||||
k: {
|
||||
'python': '3.9',
|
||||
'install': 'python -m pip install -e .[all]',
|
||||
'packages': 'pandas scipy',
|
||||
'pip_packages': ['jupyter', 'ipython', 'matplotlib', 'pytest', 'flake8'],
|
||||
}
|
||||
for k in ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9']
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PYDICOM = {
|
||||
k: {'python': '3.6', 'install': 'python -m pip install -e .', 'packages': 'numpy'}
|
||||
for k in [
|
||||
'1.0',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3',
|
||||
'1.4',
|
||||
'2.0',
|
||||
'2.1',
|
||||
'2.2',
|
||||
'2.3',
|
||||
'2.4',
|
||||
'3.0',
|
||||
]
|
||||
}
|
||||
MAP_VERSION_TO_INSTALL_PYDICOM.update(
|
||||
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.8'} for k in ['1.4', '2.0']}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_PYDICOM.update(
|
||||
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.9'} for k in ['2.1', '2.2']}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_PYDICOM.update(
|
||||
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.10'} for k in ['2.3']}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_PYDICOM.update(
|
||||
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.11'} for k in ['2.4', '3.0']}
|
||||
)
|
||||
MAP_VERSION_TO_INSTALL_HUMANEVAL = {k: {'python': '3.9'} for k in ['1.0']}
|
||||
MAP_VERSION_TO_INSTALL_HUMANEVAL_FIX = {
|
||||
k: {'python': '3.10', 'packages': 'pytest'} for k in ['0.0.1']
|
||||
}
|
||||
|
||||
# Constants - Task Instance Instllation Environment
|
||||
MAP_VERSION_TO_INSTALL = {
|
||||
'astropy/astropy': MAP_VERSION_TO_INSTALL_ASTROPY,
|
||||
'dbt-labs/dbt-core': MAP_VERSION_TO_INSTALL_DBT_CORE,
|
||||
'django/django': MAP_VERSION_TO_INSTALL_DJANGO,
|
||||
'matplotlib/matplotlib': MAP_VERSION_TO_INSTALL_MATPLOTLIB,
|
||||
'marshmallow-code/marshmallow': MAP_VERSION_TO_INSTALL_MARSHMALLOW,
|
||||
'mwaskom/seaborn': MAP_VERSION_TO_INSTALL_SEABORN,
|
||||
'pallets/flask': MAP_VERSION_TO_INSTALL_FLASK,
|
||||
'psf/requests': MAP_VERSION_TO_INSTALL_REQUESTS,
|
||||
'pvlib/pvlib-python': MAP_VERSION_TO_INSTALL_PVLIB,
|
||||
'pydata/xarray': MAP_VERSION_TO_INSTALL_XARRAY,
|
||||
'pydicom/pydicom': MAP_VERSION_TO_INSTALL_PYDICOM,
|
||||
'pylint-dev/astroid': MAP_VERSION_TO_INSTALL_ASTROID,
|
||||
'pylint-dev/pylint': MAP_VERSION_TO_INSTALL_PYLINT,
|
||||
'pytest-dev/pytest': MAP_VERSION_TO_INSTALL_PYTEST,
|
||||
'pyvista/pyvista': MAP_VERSION_TO_INSTALL_PYVISTA,
|
||||
'scikit-learn/scikit-learn': MAP_VERSION_TO_INSTALL_SKLEARN,
|
||||
'sphinx-doc/sphinx': MAP_VERSION_TO_INSTALL_SPHINX,
|
||||
'sqlfluff/sqlfluff': MAP_VERSION_TO_INSTALL_SQLFLUFF,
|
||||
'swe-bench/humaneval': MAP_VERSION_TO_INSTALL_HUMANEVAL,
|
||||
'nielstron/humaneval_fix': MAP_VERSION_TO_INSTALL_HUMANEVAL_FIX,
|
||||
'sympy/sympy': MAP_VERSION_TO_INSTALL_SYMPY,
|
||||
}
|
||||
|
||||
# Constants - Repository Specific Installation Instructions
|
||||
MAP_REPO_TO_INSTALL = {}
|
||||
|
||||
# Constants - Task Instance Test Frameworks
|
||||
TEST_PYTEST_VERBOSE = 'pytest -rA --tb=long -p no:cacheprovider'
|
||||
MAP_REPO_TO_TEST_FRAMEWORK_VERBOSE = {
|
||||
'astropy/astropy': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_ASTROPY.keys()
|
||||
},
|
||||
'django/django': {
|
||||
k: './tests/runtests.py --verbosity 2 --settings=test_sqlite --parallel 1'
|
||||
for k in MAP_VERSION_TO_INSTALL_DJANGO.keys()
|
||||
},
|
||||
'marshmallow-code/marshmallow': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_MARSHMALLOW.keys()
|
||||
},
|
||||
'matplotlib/matplotlib': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_MATPLOTLIB.keys()
|
||||
},
|
||||
'mwaskom/seaborn': {
|
||||
k: 'pytest -rA --tb=long' for k in MAP_VERSION_TO_INSTALL_SEABORN.keys()
|
||||
},
|
||||
'pallets/flask': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_FLASK.keys()
|
||||
},
|
||||
'psf/requests': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_REQUESTS.keys()
|
||||
},
|
||||
'pvlib/pvlib-python': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PVLIB.keys()
|
||||
},
|
||||
'pydata/xarray': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_XARRAY.keys()
|
||||
},
|
||||
'pydicom/pydicom': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PYDICOM.keys()
|
||||
},
|
||||
'pylint-dev/astroid': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_ASTROID.keys()
|
||||
},
|
||||
'pylint-dev/pylint': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PYLINT.keys()
|
||||
},
|
||||
'pytest-dev/pytest': {
|
||||
k: 'pytest -rA --tb=long' for k in MAP_VERSION_TO_INSTALL_PYTEST.keys()
|
||||
},
|
||||
'pyvista/pyvista': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PYVISTA.keys()
|
||||
},
|
||||
'scikit-learn/scikit-learn': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_SKLEARN.keys()
|
||||
},
|
||||
'sphinx-doc/sphinx': {
|
||||
k: 'tox -epy39 -v --' for k in MAP_VERSION_TO_INSTALL_SPHINX.keys()
|
||||
},
|
||||
'sqlfluff/sqlfluff': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_SQLFLUFF.keys()
|
||||
},
|
||||
'swe-bench/humaneval': {
|
||||
k: 'python' for k in MAP_VERSION_TO_INSTALL_HUMANEVAL.keys()
|
||||
},
|
||||
'nielstron/humaneval_fix': {
|
||||
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_HUMANEVAL.keys()
|
||||
},
|
||||
'sympy/sympy': {
|
||||
k: 'bin/test -C --verbose' for k in MAP_VERSION_TO_INSTALL_SYMPY.keys()
|
||||
},
|
||||
}
|
||||
MAP_REPO_TO_TEST_FRAMEWORK_VERBOSE['django/django']['1.9'] = (
|
||||
'./tests/runtests.py --verbosity 2'
|
||||
)
|
||||
978
evaluation/benchmarks/swe_perf/run_infer.py
Normal file
978
evaluation/benchmarks/swe_perf/run_infer.py
Normal file
@@ -0,0 +1,978 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any, Literal
|
||||
|
||||
import pandas as pd
|
||||
import toml
|
||||
from datasets import load_dataset
|
||||
|
||||
import openhands.agenthub
|
||||
from evaluation.benchmarks.swe_perf.binary_patch_utils import (
|
||||
remove_binary_diffs,
|
||||
remove_binary_files_from_git,
|
||||
)
|
||||
from evaluation.benchmarks.swe_perf.resource.mapping import (
|
||||
get_instance_resource_factor,
|
||||
)
|
||||
from evaluation.benchmarks.swe_perf.resource.swt_bench_constants import (
|
||||
MAP_REPO_TO_INSTALL,
|
||||
MAP_VERSION_TO_INSTALL,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
EvalException,
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
assert_and_raise,
|
||||
check_maximum_retries_exceeded,
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
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 = 'SWE-Perf'
|
||||
|
||||
|
||||
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
|
||||
'CodeActAgent': codeact_user_response,
|
||||
}
|
||||
|
||||
|
||||
def _get_sweperf_workspace_dir_name(instance: pd.Series) -> str:
|
||||
return f'{instance.repo}__{instance.version}'.replace('/', '__')
|
||||
|
||||
|
||||
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
|
||||
workspace_dir_name = _get_sweperf_workspace_dir_name(instance)
|
||||
|
||||
# The instruction
|
||||
instruction = f"""
|
||||
<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:
|
||||
|
||||
|
||||
<issue_description>
|
||||
{instance.problem_statement_realistic}
|
||||
</issue_description>
|
||||
|
||||
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
|
||||
I've already taken care of all changes to any of the test files described in the <issue_description>. 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 ensure the <issue_description> is satisfied.
|
||||
|
||||
Follow these phases to resolve the issue:
|
||||
|
||||
## ⚙️ Phase 1: Understand the Problem & Test Reuse
|
||||
|
||||
**1.1. Install the package locally:**
|
||||
|
||||
```bash
|
||||
python -m pip install pyinstrument
|
||||
python -m pip install -e .
|
||||
```
|
||||
|
||||
> Only proceed to README-based install if the above fails.
|
||||
|
||||
**1.2. Identify relevant modules and logic:**
|
||||
|
||||
* Use test cases mentioned in `<issue_description>` to locate the functions and files involved.
|
||||
* Focus on potential performance bottlenecks: loops, I/O, locks, cache access, data structures, etc.
|
||||
|
||||
**1.3. Run initial benchmark:**
|
||||
|
||||
```bash
|
||||
pytest -rA --durations=0 --disable-warnings -p no:warnings --tb=no <test_case>
|
||||
```
|
||||
|
||||
## 📊 Phase 2: Localization (Hierarchical Bottleneck Detection)
|
||||
|
||||
**2.1. Global profiling using `pyinstrument`:**
|
||||
|
||||
```bash
|
||||
pyinstrument -m pytest -rA --durations=0 --disable-warnings --tb=no --continue-on-collection-errors -p no:warnings <test_case>
|
||||
```
|
||||
|
||||
**2.2. Analyze performance stack if necessary:**
|
||||
|
||||
* 🔍 **Module level**: Identify hot files and methods.
|
||||
* 🔬 **Function level**: Focus on top-consuming classes/functions.
|
||||
* 🧬 **Line level**: Add fine-grained sampling/logging if needed.
|
||||
|
||||
**2.3. Output a layered summary** showing where time is spent and why.
|
||||
|
||||
|
||||
## 🧠 Phase 3: Repair (Design Candidate Fixes)
|
||||
|
||||
**3.1. Propose multiple optimization ideas:**
|
||||
|
||||
* Algorithm refinement
|
||||
* Data structure improvement
|
||||
* Parallelism / async
|
||||
* Caching / batching
|
||||
|
||||
**3.2. For each candidate:**
|
||||
|
||||
* Describe the idea using pseudocode or `diff`
|
||||
* Evaluate expected gain vs implementation complexity
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Phase 4: Patch Validation (Quantitative Benchmarking)
|
||||
|
||||
**4.1. Apply each patch separately**
|
||||
|
||||
**4.2. Re-run benchmark using the same test case**
|
||||
|
||||
**4.3. Record improvements in pytest `call` phase**
|
||||
|
||||
**4.4. Build a comparison table:**
|
||||
|
||||
| Patch | Call Time (s) | % Improvement | Notes |
|
||||
| ----- | ------------- | ------------- | ----- |
|
||||
|
||||
**4.5. Select the best-performing, low-risk patch**
|
||||
|
||||
---
|
||||
|
||||
## 📌 Phase 5: Fix Analysis (Explain the Fix)
|
||||
|
||||
**5.1.** Clearly describe the **problem**
|
||||
|
||||
**5.2.** Identify the **exact location** in the code
|
||||
|
||||
**5.3.** Describe **how the test reveals** the problem
|
||||
|
||||
**5.4.** State **best practices** relevant to the fix
|
||||
|
||||
**5.5.** Explain **how the fix resolves** the issue
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Phase 6: Fix Implementation
|
||||
|
||||
**6.1.** Implement the selected fix in the non-test source files
|
||||
**6.2.** Ensure the changes are minimal and performance-focused
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 7: Final Verification & Review
|
||||
|
||||
**7.1.** Run **full regression tests** to ensure correctness
|
||||
|
||||
**7.2.** Re-benchmark performance and compare with base commit `{instance['base_commit']}`
|
||||
|
||||
**7.3.** Ensure `call` time is significantly improved in `pytest` output
|
||||
|
||||
**7.4.** If gain is limited, consider deeper optimization
|
||||
|
||||
**7.5.** Write a concise final report including:
|
||||
|
||||
* Bottleneck path
|
||||
* Fix alternatives considered
|
||||
* Final solution and metrics
|
||||
* Code diff summary
|
||||
|
||||
## Important ##
|
||||
1. Please do not optimize specifically for certain test cases, as this may be considered cheating.
|
||||
2. Delete all files unrelated to optimization, including temporary files, auxiliary test code, and so on.
|
||||
3. Avoid excessive optimization and unnecessary divergence; if the improvement is not significant, stop promptly to maintain efficiency and focus.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def get_instance_docker_image(
|
||||
instance_id: str,
|
||||
) -> str:
|
||||
docker_image_prefix = 'docker.io/betty1202/'
|
||||
image_name = 'sweb.eval.x86_64.' + instance_id
|
||||
image_name = image_name.replace(
|
||||
'__', '_s_'
|
||||
) # to comply with docker image naming convention
|
||||
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
|
||||
|
||||
|
||||
def get_config(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
) -> OpenHandsConfig:
|
||||
base_container_image = get_instance_docker_image(
|
||||
instance['instance_id'],
|
||||
)
|
||||
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,
|
||||
enable_browser=RUN_WITH_BROWSING,
|
||||
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 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_sweperf_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-perf-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):
|
||||
json.dump([instance.to_dict()], f)
|
||||
else:
|
||||
json.dump([instance], 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_swe_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 metadata.details['mode'] == 'swt-ci':
|
||||
# set up repo
|
||||
setup_commands = []
|
||||
if instance['repo'] in MAP_REPO_TO_INSTALL:
|
||||
setup_commands.append(MAP_REPO_TO_INSTALL[instance['repo']])
|
||||
|
||||
# Run pre-install set up if provided
|
||||
install = MAP_VERSION_TO_INSTALL.get(instance['repo'], {}).get(
|
||||
instance['version'], []
|
||||
)
|
||||
if 'pre_install' in install:
|
||||
for pre_install in install['pre_install']:
|
||||
setup_commands.append(pre_install)
|
||||
|
||||
if 'install' in install:
|
||||
setup_commands.append(install['install'])
|
||||
|
||||
for command in setup_commands:
|
||||
action = CmdRunAction(command=command)
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
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 and 'testbed' in obs.content,
|
||||
f'Expected to find python interpreter from testbed, 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_sweperf_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)
|
||||
|
||||
# Get git patch
|
||||
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:
|
||||
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='SWE-Perf/SWE-Perf',
|
||||
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, split=args.split)
|
||||
|
||||
swe_perf_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
logger.info(
|
||||
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_perf_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_perf_tests, output_file, args.eval_n_limit)
|
||||
|
||||
run_evaluation(
|
||||
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_perf_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
|
||||
)
|
||||
|
||||
# Run evaluation - but save them to cur_output_file
|
||||
logger.info(
|
||||
f'Evaluating {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
run_evaluation(
|
||||
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}'
|
||||
)
|
||||
# Check if any instances reached maximum retries
|
||||
check_maximum_retries_exceeded(metadata.eval_output_dir)
|
||||
146
evaluation/benchmarks/swe_perf/scripts/run_infer.sh
Executable file
146
evaluation/benchmarks/swe_perf/scripts/run_infer.sh
Executable 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 SWE-Perf/SWE-Perf"
|
||||
DATASET="SWE-Perf/SWE-Perf"
|
||||
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/swe_perf/run_infer.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
|
||||
54
evaluation/benchmarks/swe_perf/scripts/setup/compare_patch_filename.py
Executable file
54
evaluation/benchmarks/swe_perf/scripts/setup/compare_patch_filename.py
Executable file
@@ -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)
|
||||
43
evaluation/benchmarks/swe_perf/scripts/setup/instance_swe_entry.sh
Executable file
43
evaluation/benchmarks/swe_perf/scripts/setup/instance_swe_entry.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source ~/.bashrc
|
||||
SWEUTIL_DIR=/swe_util
|
||||
|
||||
# FIXME: Cannot read SWE_INSTANCE_ID from the environment variable
|
||||
# SWE_INSTANCE_ID=django__django-11099
|
||||
if [ -z "$SWE_INSTANCE_ID" ]; then
|
||||
echo "Error: SWE_INSTANCE_ID is not set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read the swe-bench-test-lite.json file and extract the required item based on instance_id
|
||||
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
|
||||
|
||||
|
||||
WORKSPACE_NAME=$(echo "$item" | jq -r '(.repo | tostring) + "__" + (.version | tostring) | gsub("/"; "__")')
|
||||
|
||||
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
|
||||
cp -r /testbed /workspace/$WORKSPACE_NAME
|
||||
|
||||
# Activate instance-specific environment
|
||||
if [ -d /opt/miniconda3 ]; then
|
||||
. /opt/miniconda3/etc/profile.d/conda.sh
|
||||
conda activate testbed
|
||||
fi
|
||||
@@ -13,7 +13,8 @@ vi.mock("react-router", async () => {
|
||||
|
||||
vi.mock("#/context/conversation-context", () => ({
|
||||
useConversation: () => ({ conversationId: "test-conversation-id" }),
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) =>
|
||||
children,
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
@@ -29,21 +30,18 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock redux
|
||||
const mockDispatch = vi.fn();
|
||||
// Mock Zustand browser store
|
||||
let mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useDispatch: () => mockDispatch,
|
||||
useSelector: () => mockBrowserState,
|
||||
};
|
||||
});
|
||||
vi.mock("#/stores/browser-store", () => ({
|
||||
useBrowserStore: () => mockBrowserState,
|
||||
}));
|
||||
|
||||
// Import the component after all mocks are set up
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
@@ -55,6 +53,9 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -63,6 +64,9 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
@@ -75,7 +79,11 @@ describe("Browser", () => {
|
||||
// Set the mock state for this test
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
|
||||
@@ -357,69 +357,6 @@ describe("ConversationCard", () => {
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show display cost button only when showOptions is true", async () => {
|
||||
const onContextMenuToggle = vi.fn();
|
||||
const { rerender } = renderWithProviders(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
contextMenuOpen
|
||||
onContextMenuToggle={onContextMenuToggle}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for context menu to appear
|
||||
const menu = await screen.findByTestId("context-menu");
|
||||
expect(
|
||||
within(menu).queryByTestId("display-cost-button"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
showOptions
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
contextMenuOpen
|
||||
onContextMenuToggle={onContextMenuToggle}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for context menu to appear and check for display cost button
|
||||
const newMenu = await screen.findByTestId("context-menu");
|
||||
within(newMenu).getByTestId("display-cost-button");
|
||||
});
|
||||
|
||||
it("should show metrics modal when clicking the display cost button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onContextMenuToggle = vi.fn();
|
||||
renderWithProviders(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
showOptions
|
||||
contextMenuOpen
|
||||
onContextMenuToggle={onContextMenuToggle}
|
||||
/>,
|
||||
);
|
||||
|
||||
const menu = screen.getByTestId("context-menu");
|
||||
const displayCostButton = within(menu).getByTestId("display-cost-button");
|
||||
|
||||
await user.click(displayCostButton);
|
||||
|
||||
// Verify if metrics modal is displayed by checking for the modal content
|
||||
expect(screen.getByTestId("metrics-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display the edit or delete options if the handler is not provided", async () => {
|
||||
const onContextMenuToggle = vi.fn();
|
||||
const { rerender } = renderWithProviders(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientConfig } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { renderWithQueryAndI18n } from "test-utils";
|
||||
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
@@ -18,16 +17,7 @@ describe("ConversationPanel", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const renderConversationPanel = (config?: QueryClientConfig) =>
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
const renderConversationPanel = () => renderWithQueryAndI18n(<RouterStub />);
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
@@ -297,15 +287,7 @@ describe("ConversationPanel", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
renderWithProviders(<MyRouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithQueryAndI18n(<MyRouterStub />);
|
||||
|
||||
const toggleButton = screen.getByText("Toggle");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
// Mock hooks
|
||||
const mockUseUserProviders = vi.fn();
|
||||
@@ -55,25 +56,47 @@ describe("MicroagentManagement", () => {
|
||||
]);
|
||||
|
||||
const renderMicroagentManagement = (config?: QueryClientConfig) =>
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem: null,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
renderWithProviders(<RouterStub />);
|
||||
|
||||
// Common test data
|
||||
const testRepository = {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github" as const,
|
||||
is_public: true,
|
||||
owner_type: "user" as const,
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
};
|
||||
|
||||
// Helper function to render with custom Zustand store state
|
||||
const renderWithCustomStore = (storeOverrides: Partial<any>) => {
|
||||
useMicroagentManagementStore.setState(storeOverrides);
|
||||
return renderWithProviders(<RouterStub />);
|
||||
};
|
||||
|
||||
// Helper function to render with update modal visible
|
||||
const renderWithUpdateModal = (additionalState: Partial<any> = {}) => {
|
||||
return renderWithCustomStore({
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: testRepository,
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to render with selected microagent
|
||||
const renderWithSelectedMicroagent = (
|
||||
microagent: any,
|
||||
additionalState: Partial<any> = {},
|
||||
) => {
|
||||
return renderWithCustomStore({
|
||||
selectedRepository: testRepository,
|
||||
selectedMicroagentItem: {
|
||||
microagent,
|
||||
conversation: null,
|
||||
},
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
@@ -186,6 +209,23 @@ describe("MicroagentManagement", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Reset Zustand store to default state
|
||||
useMicroagentManagementStore.setState({
|
||||
// Modal visibility states
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
|
||||
// Repository states
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
|
||||
// Microagent states
|
||||
selectedMicroagentItem: null,
|
||||
});
|
||||
|
||||
// Setup default hook mocks
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: ["github"],
|
||||
@@ -1347,33 +1387,10 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render modal when Redux state is set to visible", async () => {
|
||||
// Render with modal already visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: true, // Start with modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
it("should render modal when Zustand state is set to visible", async () => {
|
||||
// Render with modal already visible in Zustand state
|
||||
renderWithCustomStore({
|
||||
addMicroagentModalVisible: true,
|
||||
});
|
||||
|
||||
// Check that modal is rendered
|
||||
@@ -1643,34 +1660,16 @@ describe("MicroagentManagement", () => {
|
||||
pr_number: null,
|
||||
};
|
||||
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) =>
|
||||
renderWithProviders(<MicroagentManagementMain />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) => {
|
||||
// Set the store with the selected microagent item and a repository
|
||||
useMicroagentManagementStore.setState({
|
||||
selectedMicroagentItem,
|
||||
selectedRepository: testRepository,
|
||||
});
|
||||
|
||||
return renderWithProviders(<MicroagentManagementMain />);
|
||||
};
|
||||
|
||||
it("should render MicroagentManagementDefault when no microagent or conversation is selected", async () => {
|
||||
renderMicroagentManagementMain(null);
|
||||
|
||||
@@ -1995,36 +1994,8 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
it("should render update microagent modal when updateMicroagentModalVisible is true", async () => {
|
||||
// Render with update modal visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true, // Start with update modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
// Render with update modal visible in Zustand state
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that update modal is rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2035,35 +2006,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display update microagent title when isUpdate is true", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that the update title is displayed
|
||||
expect(
|
||||
@@ -2073,33 +2016,10 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should populate form fields with existing microagent data when updating", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
renderWithUpdateModal({
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2116,35 +2036,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2172,35 +2064,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2223,35 +2087,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2277,32 +2113,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle update modal with empty microagent data", async () => {
|
||||
// Render with update modal visible but no microagent data
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that update modal is still rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2323,35 +2134,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for the content to be loaded and check that the form field is empty
|
||||
await waitFor(() => {
|
||||
@@ -2372,35 +2155,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that the modal is rendered correctly
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2559,35 +2314,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should render learn something new button in microagent view", async () => {
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Check that the learn something new button is displayed
|
||||
expect(
|
||||
@@ -2599,35 +2326,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2656,35 +2355,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2716,35 +2387,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2774,35 +2417,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
max_budget_per_task: null,
|
||||
usage: null,
|
||||
},
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
|
||||
@@ -342,13 +342,7 @@ describe("InteractiveChatBox", () => {
|
||||
// Simulate parent component updating the value prop
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
isWaitingForUserInput={true}
|
||||
hasSubstantiveAgentActions={true}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
<InteractiveChatBox onSubmit={onSubmit} onStop={onStop} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { act, screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { vi, describe, afterEach, it, expect } from "vitest";
|
||||
import { Command, appendInput, appendOutput } from "#/state/command-slice";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
|
||||
const renderTerminal = (commands: Command[] = []) =>
|
||||
renderWithProviders(<Terminal />, {
|
||||
preloadedState: {
|
||||
cmd: {
|
||||
commands,
|
||||
},
|
||||
},
|
||||
});
|
||||
const renderTerminal = (commands: Command[] = []) => {
|
||||
// Set initial commands in Zustand store
|
||||
useCommandStore.setState({ commands });
|
||||
return renderWithProviders(<Terminal />);
|
||||
};
|
||||
|
||||
describe.skip("Terminal", () => {
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
@@ -58,25 +55,25 @@ describe.skip("Terminal", () => {
|
||||
});
|
||||
|
||||
it("should write commands to the terminal", () => {
|
||||
const { store } = renderTerminal();
|
||||
renderTerminal();
|
||||
|
||||
act(() => {
|
||||
store.dispatch(appendInput("echo Hello"));
|
||||
store.dispatch(appendOutput("Hello"));
|
||||
useCommandStore.getState().appendInput("echo Hello");
|
||||
useCommandStore.getState().appendOutput("Hello");
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo Hello");
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "Hello");
|
||||
|
||||
act(() => {
|
||||
store.dispatch(appendInput("echo World"));
|
||||
useCommandStore.getState().appendInput("echo World");
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(3, "echo World");
|
||||
});
|
||||
|
||||
it("should load and write commands to the terminal", () => {
|
||||
const { store } = renderTerminal([
|
||||
renderTerminal([
|
||||
{ type: "input", content: "echo Hello" },
|
||||
{ type: "output", content: "Hello" },
|
||||
]);
|
||||
@@ -85,17 +82,17 @@ describe.skip("Terminal", () => {
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "Hello");
|
||||
|
||||
act(() => {
|
||||
store.dispatch(appendInput("echo Hello"));
|
||||
useCommandStore.getState().appendInput("echo Hello");
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(3, "echo Hello");
|
||||
});
|
||||
|
||||
it("should end the line with a dollar sign after writing a command", () => {
|
||||
const { store } = renderTerminal();
|
||||
renderTerminal();
|
||||
|
||||
act(() => {
|
||||
store.dispatch(appendInput("echo Hello"));
|
||||
useCommandStore.getState().appendInput("echo Hello");
|
||||
});
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenCalledWith("echo Hello");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach } from "node:test";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
import { Command } from "#/state/command-slice";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
@@ -19,10 +19,10 @@ interface TestTerminalComponentProps {
|
||||
commands: Command[];
|
||||
}
|
||||
|
||||
function TestTerminalComponent({
|
||||
commands,
|
||||
}: TestTerminalComponentProps) {
|
||||
const ref = useTerminal({ commands });
|
||||
function TestTerminalComponent({ commands }: TestTerminalComponentProps) {
|
||||
// Set commands in Zustand store
|
||||
useCommandStore.setState({ commands });
|
||||
const ref = useTerminal();
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ describe("useTerminal", () => {
|
||||
renderWithProviders(<TestTerminalComponent commands={[]} />, {
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
cmd: { commands: [] },
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -74,7 +73,6 @@ describe("useTerminal", () => {
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />, {
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
cmd: { commands },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,17 +92,11 @@ describe("useTerminal", () => {
|
||||
{ content: secret, type: "output" },
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<TestTerminalComponent
|
||||
commands={commands}
|
||||
/>,
|
||||
{
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
cmd: { commands },
|
||||
},
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />, {
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// This test is no longer relevant as secrets filtering has been removed
|
||||
});
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import store from "../src/store";
|
||||
import {
|
||||
setInitialPrompt,
|
||||
clearInitialPrompt,
|
||||
} from "../src/state/initial-query-slice";
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useInitialQueryStore } from "../src/stores/initial-query-store";
|
||||
|
||||
describe("Initial Query Behavior", () => {
|
||||
it("should clear initial query when clearInitialPrompt is dispatched", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the store before each test
|
||||
useInitialQueryStore.getState().reset();
|
||||
});
|
||||
|
||||
it("should clear initial query when clearInitialPrompt is called", () => {
|
||||
const { setInitialPrompt, clearInitialPrompt, initialPrompt } =
|
||||
useInitialQueryStore.getState();
|
||||
|
||||
// Set up initial query in the store
|
||||
store.dispatch(setInitialPrompt("test query"));
|
||||
expect(store.getState().initialQuery.initialPrompt).toBe("test query");
|
||||
setInitialPrompt("test query");
|
||||
expect(useInitialQueryStore.getState().initialPrompt).toBe("test query");
|
||||
|
||||
// Clear the initial query
|
||||
store.dispatch(clearInitialPrompt());
|
||||
clearInitialPrompt();
|
||||
|
||||
// Verify initial query is cleared
|
||||
expect(store.getState().initialQuery.initialPrompt).toBeNull();
|
||||
expect(useInitialQueryStore.getState().initialPrompt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,14 +13,26 @@ vi.mock("#/store", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/command-slice", () => ({
|
||||
appendInput: mockAppendInput,
|
||||
vi.mock("#/state/command-store", () => ({
|
||||
useCommandStore: {
|
||||
getState: () => ({
|
||||
appendInput: mockAppendInput,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/jupyter-slice", () => ({
|
||||
appendJupyterInput: mockAppendJupyterInput,
|
||||
}));
|
||||
|
||||
vi.mock("#/state/metrics-slice", () => ({
|
||||
setMetrics: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("#/state/security-analyzer-slice", () => ({
|
||||
appendSecurityAnalyzerInput: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("handleActionMessage", () => {
|
||||
beforeEach(() => {
|
||||
// Clear all mocks before each test
|
||||
@@ -45,7 +57,8 @@ describe("handleActionMessage", () => {
|
||||
handleActionMessage(runAction);
|
||||
|
||||
// Check that appendInput was called with the command
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockAppendInput("ls -la"));
|
||||
expect(mockAppendInput).toHaveBeenCalledWith("ls -la");
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockAppendJupyterInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -59,7 +72,8 @@ describe("handleActionMessage", () => {
|
||||
args: {
|
||||
code: "print('Hello from Jupyter!')",
|
||||
},
|
||||
message: "Running Python code interactively: print('Hello from Jupyter!')",
|
||||
message:
|
||||
"Running Python code interactively: print('Hello from Jupyter!')",
|
||||
timestamp: "2023-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@@ -67,7 +81,9 @@ describe("handleActionMessage", () => {
|
||||
handleActionMessage(ipythonAction);
|
||||
|
||||
// Check that appendJupyterInput was called with the code
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockAppendJupyterInput("print('Hello from Jupyter!')"));
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
mockAppendJupyterInput("print('Hello from Jupyter!')"),
|
||||
);
|
||||
expect(mockAppendInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -89,7 +105,9 @@ describe("handleActionMessage", () => {
|
||||
// Handle the action
|
||||
handleActionMessage(hiddenAction);
|
||||
|
||||
// Check that nothing was dispatched
|
||||
// Check that nothing was dispatched or called
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockAppendInput).not.toHaveBeenCalled();
|
||||
expect(mockAppendJupyterInput).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,13 +60,7 @@ describe("Check for hardcoded English strings", () => {
|
||||
test("InteractiveChatBox should not have hardcoded English strings", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox
|
||||
onSubmit={() => {}}
|
||||
onStop={() => {}}
|
||||
isWaitingForUserInput={false}
|
||||
hasSubstantiveAgentActions={false}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
<InteractiveChatBox onSubmit={() => {}} onStop={() => {}} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
1764
frontend/package-lock.json
generated
1764
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,44 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.56.0",
|
||||
"version": "0.57.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.8.2",
|
||||
"@react-router/serve": "^7.8.2",
|
||||
"@react-router/node": "^7.9.1",
|
||||
"@react-router/serve": "^7.9.1",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/react-stripe-js": "^4.0.0",
|
||||
"@stripe/react-stripe-js": "^4.0.2",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.87.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "^9.0.10",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"framer-motion": "^12.23.19",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.30",
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"posthog-js": "^1.268.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -47,8 +47,7 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^3.0.5",
|
||||
"react-router": "^7.8.2",
|
||||
"react-router": "^7.9.1",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
@@ -56,9 +55,10 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"vite": "^7.1.4",
|
||||
"vite": "^7.1.7",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2"
|
||||
"ws": "^8.18.2",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
|
||||
@@ -97,16 +97,16 @@
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@react-router/dev": "^7.8.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.86.0",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@react-router/dev": "^7.9.1",
|
||||
"@tailwindcss/typography": "^0.5.18",
|
||||
"@tanstack/eslint-plugin-query": "^5.90.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -128,8 +128,8 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.6",
|
||||
"jsdom": "^27.0.0",
|
||||
"lint-staged": "^16.2.0",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.5.0",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.10.5'
|
||||
const PACKAGE_VERSION = '2.11.1'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { BrowserSnapshot } from "./browser-snapshot";
|
||||
import { EmptyBrowserMessage } from "./empty-browser-message";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setUrl,
|
||||
setScreenshotSrc,
|
||||
} from "#/state/browser-slice";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
|
||||
export function BrowserPanel() {
|
||||
const { url, screenshotSrc } = useSelector(
|
||||
(state: RootState) => state.browser,
|
||||
);
|
||||
const { url, screenshotSrc, reset } = useBrowserStore();
|
||||
const { conversationId } = useConversationId();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setUrl(browserInitialState.url));
|
||||
dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
|
||||
}, [conversationId]);
|
||||
reset();
|
||||
}, [conversationId, reset]);
|
||||
|
||||
const imgSrc =
|
||||
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
@@ -18,6 +18,7 @@ import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { Messages } from "./messages";
|
||||
import { ChatSuggestions } from "./chat-suggestions";
|
||||
import { ScrollProvider } from "#/context/scroll-context";
|
||||
import { useInitialQueryStore } from "#/stores/initial-query-store";
|
||||
|
||||
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
@@ -32,7 +33,8 @@ import {
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
|
||||
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
@@ -44,7 +46,7 @@ function getEntryPoint(
|
||||
}
|
||||
|
||||
export function ChatInterface() {
|
||||
const dispatch = useDispatch();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const { getErrorMessage } = useWSErrorMessage();
|
||||
const { send, isLoadingMessages, parsedEvents } = useWsClient();
|
||||
const { setOptimisticUserMessage, getOptimisticUserMessage } =
|
||||
@@ -67,9 +69,7 @@ export function ChatInterface() {
|
||||
"positive" | "negative"
|
||||
>("positive");
|
||||
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
|
||||
const { selectedRepository, replayJson } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
const { selectedRepository, replayJson } = useInitialQueryStore();
|
||||
const params = useParams();
|
||||
const { mutateAsync: uploadFiles } = useUploadFiles();
|
||||
|
||||
@@ -141,7 +141,7 @@ export function ChatInterface() {
|
||||
|
||||
send(createChatMessage(prompt, imageUrls, uploadedFiles, timestamp));
|
||||
setOptimisticUserMessage(content);
|
||||
dispatch(setMessageToSend(null));
|
||||
setMessageToSend("");
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
@@ -156,10 +156,6 @@ export function ChatInterface() {
|
||||
setFeedbackPolarity(polarity);
|
||||
};
|
||||
|
||||
const isWaitingForUserInput =
|
||||
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
||||
curAgentState === AgentState.FINISHED;
|
||||
|
||||
// Create a ScrollProvider with the scroll hook values
|
||||
const scrollProviderValue = {
|
||||
scrollRef,
|
||||
@@ -180,9 +176,7 @@ export function ChatInterface() {
|
||||
!optimisticUserMessage &&
|
||||
!userEventsExist && (
|
||||
<ChatSuggestions
|
||||
onSuggestionsClick={(message) =>
|
||||
dispatch(setMessageToSend(message))
|
||||
}
|
||||
onSuggestionsClick={(message) => setMessageToSend(message)}
|
||||
/>
|
||||
)}
|
||||
{/* Note: We only hide chat suggestions when there's a user message */}
|
||||
@@ -210,17 +204,20 @@ export function ChatInterface() {
|
||||
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="flex justify-between relative">
|
||||
{events.length > 0 && (
|
||||
<TrajectoryActions
|
||||
onPositiveFeedback={() =>
|
||||
onClickShareFeedbackActionButton("positive")
|
||||
}
|
||||
onNegativeFeedback={() =>
|
||||
onClickShareFeedbackActionButton("negative")
|
||||
}
|
||||
isSaasMode={config?.APP_MODE === "saas"}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<ConfirmationModeEnabled />
|
||||
{events.length > 0 && (
|
||||
<TrajectoryActions
|
||||
onPositiveFeedback={() =>
|
||||
onClickShareFeedbackActionButton("positive")
|
||||
}
|
||||
onNegativeFeedback={() =>
|
||||
onClickShareFeedbackActionButton("negative")
|
||||
}
|
||||
isSaasMode={config?.APP_MODE === "saas"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0">
|
||||
{curAgentState === AgentState.RUNNING && <TypingIndicator />}
|
||||
@@ -234,9 +231,6 @@ export function ChatInterface() {
|
||||
<InteractiveChatBox
|
||||
onSubmit={handleSendMessage}
|
||||
onStop={handleStop}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={!!optimisticUserMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function ChatMessage({
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className={cn(
|
||||
"rounded-xl relative w-fit max-w-full",
|
||||
"rounded-xl relative w-fit max-w-full last:mb-4",
|
||||
"flex flex-col gap-2",
|
||||
type === "user" && " p-4 bg-tertiary self-end",
|
||||
type === "agent" && "mt-6 max-w-full bg-transparent",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Suggestions } from "#/components/features/suggestions/suggestions";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { RootState } from "#/store";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
interface ChatSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -13,9 +12,7 @@ interface ChatSuggestionsProps {
|
||||
|
||||
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldHideSuggestions = useSelector(
|
||||
(state: RootState) => state.conversation.shouldHideSuggestions,
|
||||
);
|
||||
const { shouldHideSuggestions } = useConversationStore();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { ServerStatus } from "#/components/features/controls/server-status";
|
||||
import { AgentStatus } from "#/components/features/controls/agent-status";
|
||||
import { Tools } from "../../controls/tools";
|
||||
|
||||
interface ChatInputActionsProps {
|
||||
conversationStatus: ConversationStatus | null;
|
||||
disabled: boolean;
|
||||
handleStop: (onStop?: () => void) => void;
|
||||
handleResumeAgent: () => void;
|
||||
onStop?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputActions({
|
||||
conversationStatus,
|
||||
disabled,
|
||||
handleStop,
|
||||
handleResumeAgent,
|
||||
onStop,
|
||||
}: ChatInputActionsProps) {
|
||||
return (
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tools />
|
||||
<ServerStatus conversationStatus={conversationStatus} />
|
||||
</div>
|
||||
<AgentStatus
|
||||
handleStop={() => handleStop(onStop)}
|
||||
handleResumeAgent={handleResumeAgent}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { DragOver } from "../drag-over";
|
||||
import { UploadedFiles } from "../uploaded-files";
|
||||
import { ChatInputRow } from "./chat-input-row";
|
||||
import { ChatInputActions } from "./chat-input-actions";
|
||||
|
||||
interface ChatInputContainerProps {
|
||||
chatContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||
isDragOver: boolean;
|
||||
disabled: boolean;
|
||||
showButton: boolean;
|
||||
buttonClassName: string;
|
||||
conversationStatus: ConversationStatus | null;
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleFileIconClick: (isDisabled: boolean) => void;
|
||||
handleSubmit: () => void;
|
||||
handleStop: (onStop?: () => void) => void;
|
||||
handleResumeAgent: () => void;
|
||||
onDragOver: (e: React.DragEvent, isDisabled: boolean) => void;
|
||||
onDragLeave: (e: React.DragEvent, isDisabled: boolean) => void;
|
||||
onDrop: (e: React.DragEvent, isDisabled: boolean) => void;
|
||||
onInput: () => void;
|
||||
onPaste: (e: React.ClipboardEvent) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onStop?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputContainer({
|
||||
chatContainerRef,
|
||||
isDragOver,
|
||||
disabled,
|
||||
showButton,
|
||||
buttonClassName,
|
||||
conversationStatus,
|
||||
chatInputRef,
|
||||
handleFileIconClick,
|
||||
handleSubmit,
|
||||
handleStop,
|
||||
handleResumeAgent,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onInput,
|
||||
onPaste,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onStop,
|
||||
}: ChatInputContainerProps) {
|
||||
return (
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className="bg-[#25272D] box-border content-stretch flex flex-col items-start justify-center p-4 pt-3 relative rounded-[15px] w-full"
|
||||
onDragOver={(e) => onDragOver(e, disabled)}
|
||||
onDragLeave={(e) => onDragLeave(e, disabled)}
|
||||
onDrop={(e) => onDrop(e, disabled)}
|
||||
>
|
||||
{/* Drag Over UI */}
|
||||
{isDragOver && <DragOver />}
|
||||
|
||||
<UploadedFiles />
|
||||
|
||||
<ChatInputRow
|
||||
chatInputRef={chatInputRef}
|
||||
disabled={disabled}
|
||||
showButton={showButton}
|
||||
buttonClassName={buttonClassName}
|
||||
handleFileIconClick={handleFileIconClick}
|
||||
handleSubmit={handleSubmit}
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
|
||||
<ChatInputActions
|
||||
conversationStatus={conversationStatus}
|
||||
disabled={disabled}
|
||||
handleStop={handleStop}
|
||||
handleResumeAgent={handleResumeAgent}
|
||||
onStop={onStop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ChatInputFieldProps {
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>;
|
||||
onInput: () => void;
|
||||
onPaste: (e: React.ClipboardEvent) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputField({
|
||||
chatInputRef,
|
||||
onInput,
|
||||
onPaste,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
}: ChatInputFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="box-border content-stretch flex flex-row items-center justify-start min-h-6 p-0 relative shrink-0 flex-1"
|
||||
data-name="Text & caret"
|
||||
>
|
||||
<div className="basis-0 flex flex-col font-normal grow justify-center leading-[0] min-h-px min-w-px overflow-ellipsis overflow-hidden relative shrink-0 text-[#d0d9fa] text-[16px] text-left">
|
||||
<div
|
||||
ref={chatInputRef}
|
||||
className="chat-input bg-transparent text-white text-[16px] font-normal leading-[20px] outline-none resize-none custom-scrollbar min-h-[20px] max-h-[400px] [text-overflow:inherit] [text-wrap-mode:inherit] [white-space-collapse:inherit] block whitespace-pre-wrap"
|
||||
contentEditable
|
||||
data-placeholder={t("SUGGESTIONS$WHAT_TO_BUILD")}
|
||||
data-testid="chat-input"
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ChatInputGripProps {
|
||||
gripRef: React.RefObject<HTMLDivElement | null>;
|
||||
isGripVisible: boolean;
|
||||
handleTopEdgeClick: (e: React.MouseEvent) => void;
|
||||
handleGripMouseDown: (e: React.MouseEvent) => void;
|
||||
handleGripTouchStart: (e: React.TouchEvent) => void;
|
||||
}
|
||||
|
||||
export function ChatInputGrip({
|
||||
gripRef,
|
||||
isGripVisible,
|
||||
handleTopEdgeClick,
|
||||
handleGripMouseDown,
|
||||
handleGripTouchStart,
|
||||
}: ChatInputGripProps) {
|
||||
return (
|
||||
<div
|
||||
className="absolute -top-[12px] left-0 w-full h-6 lg:h-3 z-20 group"
|
||||
id="resize-grip"
|
||||
onClick={handleTopEdgeClick}
|
||||
>
|
||||
{/* Resize Grip - appears on hover of top edge area, when dragging, or when clicked */}
|
||||
<div
|
||||
ref={gripRef}
|
||||
className={cn(
|
||||
"absolute top-[4px] left-0 w-full h-[3px] bg-white cursor-ns-resize z-10 transition-opacity duration-200",
|
||||
isGripVisible ? "opacity-100" : "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
onMouseDown={handleGripMouseDown}
|
||||
onTouchStart={handleGripTouchStart}
|
||||
style={{ userSelect: "none" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ChatAddFileButton } from "../chat-add-file-button";
|
||||
import { ChatSendButton } from "../chat-send-button";
|
||||
import { ChatInputField } from "./chat-input-field";
|
||||
|
||||
interface ChatInputRowProps {
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>;
|
||||
disabled: boolean;
|
||||
showButton: boolean;
|
||||
buttonClassName: string;
|
||||
handleFileIconClick: (isDisabled: boolean) => void;
|
||||
handleSubmit: () => void;
|
||||
onInput: () => void;
|
||||
onPaste: (e: React.ClipboardEvent) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputRow({
|
||||
chatInputRef,
|
||||
disabled,
|
||||
showButton,
|
||||
buttonClassName,
|
||||
handleFileIconClick,
|
||||
handleSubmit,
|
||||
onInput,
|
||||
onPaste,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
}: ChatInputRowProps) {
|
||||
return (
|
||||
<div className="box-border content-stretch flex flex-row items-end justify-between p-0 relative shrink-0 w-full pb-[18px] gap-2">
|
||||
<div className="basis-0 box-border content-stretch flex flex-row gap-4 grow items-end justify-start min-h-px min-w-px p-0 relative shrink-0">
|
||||
<ChatAddFileButton
|
||||
disabled={disabled}
|
||||
handleFileIconClick={() => handleFileIconClick(disabled)}
|
||||
/>
|
||||
|
||||
<ChatInputField
|
||||
chatInputRef={chatInputRef}
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Send Button */}
|
||||
{showButton && (
|
||||
<ChatSendButton
|
||||
buttonClassName={cn(buttonClassName, "translate-y-[3px]")}
|
||||
handleSubmit={handleSubmit}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
interface HiddenFileInputProps {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export function HiddenFileInput({
|
||||
fileInputRef,
|
||||
onChange,
|
||||
}: HiddenFileInputProps) {
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
accept="*/*"
|
||||
style={{ display: "none" }}
|
||||
onChange={onChange}
|
||||
data-testid="upload-image-input"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import LockIcon from "#/icons/lock.svg?react";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
|
||||
function ConfirmationModeEnabled() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
if (!settings?.CONFIRMATION_MODE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={t(I18nKey.COMMON$CONFIRMATION_MODE_ENABLED)}
|
||||
closeDelay={100}
|
||||
className="bg-white text-black hover:bg-transparent"
|
||||
>
|
||||
<div className="flex items-center justify-center w-[26px] h-[26px] rounded-lg bg-[#25272D]">
|
||||
<LockIcon width={15} height={15} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmationModeEnabled;
|
||||
@@ -1,25 +1,14 @@
|
||||
import React, { useRef, useCallback, useState, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useEffect } from "react";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { ServerStatus } from "#/components/features/controls/server-status";
|
||||
import { AgentStatus } from "#/components/features/controls/agent-status";
|
||||
import { ChatSendButton } from "./chat-send-button";
|
||||
import { ChatAddFileButton } from "./chat-add-file-button";
|
||||
import { cn, isMobileDevice } from "#/utils/utils";
|
||||
import { useAutoResize } from "#/hooks/use-auto-resize";
|
||||
import { DragOver } from "./drag-over";
|
||||
import { UploadedFiles } from "./uploaded-files";
|
||||
import { Tools } from "../controls/tools";
|
||||
import {
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
setMessageToSend,
|
||||
setIsRightPanelShown,
|
||||
} from "#/state/conversation-slice";
|
||||
import { CHAT_INPUT } from "#/utils/constants";
|
||||
import { RootState } from "#/store";
|
||||
import { useChatInputLogic } from "#/hooks/chat/use-chat-input-logic";
|
||||
import { useFileHandling } from "#/hooks/chat/use-file-handling";
|
||||
import { useGripResize } from "#/hooks/chat/use-grip-resize";
|
||||
import { useChatInputEvents } from "#/hooks/chat/use-chat-input-events";
|
||||
import { useChatSubmission } from "#/hooks/chat/use-chat-submission";
|
||||
import { ChatInputGrip } from "./components/chat-input-grip";
|
||||
import { ChatInputContainer } from "./components/chat-input-container";
|
||||
import { HiddenFileInput } from "./components/hidden-file-input";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export interface CustomChatInputProps {
|
||||
disabled?: boolean;
|
||||
@@ -46,14 +35,12 @@ export function CustomChatInput({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
}: CustomChatInputProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isGripVisible, setIsGripVisible] = useState(false);
|
||||
|
||||
const { messageToSend, submittedMessage, hasRightPanelToggled } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
submittedMessage,
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} = useConversationStore();
|
||||
|
||||
// Disable input when conversation is stopped
|
||||
const isConversationStopped = conversationStatus === "STOPPED";
|
||||
@@ -65,377 +52,108 @@ export function CustomChatInput({
|
||||
return;
|
||||
}
|
||||
onSubmit(submittedMessage);
|
||||
dispatch(setSubmittedMessage(null));
|
||||
}, [submittedMessage, disabled, onSubmit, dispatch]);
|
||||
setSubmittedMessage(null);
|
||||
}, [submittedMessage, disabled, onSubmit, setSubmittedMessage]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const chatInputRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const gripRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Save current input value when drawer state changes
|
||||
useEffect(() => {
|
||||
if (chatInputRef.current) {
|
||||
const currentText = chatInputRef.current?.innerText || "";
|
||||
// Dispatch to save current input value when drawer state changes
|
||||
dispatch(setMessageToSend(currentText));
|
||||
dispatch(setIsRightPanelShown(hasRightPanelToggled));
|
||||
}
|
||||
}, [hasRightPanelToggled, dispatch]);
|
||||
|
||||
// Helper function to check if contentEditable is truly empty
|
||||
const isContentEmpty = useCallback((): boolean => {
|
||||
if (!chatInputRef.current) return true;
|
||||
const text =
|
||||
chatInputRef.current.innerText || chatInputRef.current.textContent || "";
|
||||
return text.trim() === "";
|
||||
}, []);
|
||||
|
||||
// Helper function to properly clear contentEditable for placeholder display
|
||||
const clearEmptyContent = useCallback((): void => {
|
||||
if (chatInputRef.current && isContentEmpty()) {
|
||||
chatInputRef.current.innerHTML = "";
|
||||
chatInputRef.current.textContent = "";
|
||||
}
|
||||
}, [isContentEmpty]);
|
||||
|
||||
// Drag state management callbacks
|
||||
const handleDragStart = useCallback(() => {
|
||||
// Keep grip visible during drag by adding a CSS class
|
||||
if (gripRef.current) {
|
||||
gripRef.current.classList.add("opacity-100");
|
||||
gripRef.current.classList.remove("opacity-0");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
// Restore hover-based visibility
|
||||
if (gripRef.current) {
|
||||
gripRef.current.classList.remove("opacity-100");
|
||||
gripRef.current.classList.add("opacity-0");
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle click on top edge area to toggle grip visibility
|
||||
const handleTopEdgeClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsGripVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
// Callback to handle height changes and manage suggestions visibility
|
||||
const handleHeightChange = useCallback(
|
||||
(height: number) => {
|
||||
// Hide suggestions when input height exceeds the threshold
|
||||
const shouldHideChatSuggestions = height > CHAT_INPUT.HEIGHT_THRESHOLD;
|
||||
dispatch(setShouldHideSuggestions(shouldHideChatSuggestions));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Use the auto-resize hook with height change callback
|
||||
// Custom hooks
|
||||
const {
|
||||
chatInputRef,
|
||||
messageToSend,
|
||||
checkIsContentEmpty,
|
||||
clearEmptyContentHandler,
|
||||
} = useChatInputLogic();
|
||||
|
||||
const {
|
||||
fileInputRef,
|
||||
chatContainerRef,
|
||||
isDragOver,
|
||||
handleFileIconClick,
|
||||
handleFileInputChange,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
} = useFileHandling(onFilesPaste);
|
||||
|
||||
const {
|
||||
gripRef,
|
||||
isGripVisible,
|
||||
handleTopEdgeClick,
|
||||
smartResize,
|
||||
handleGripMouseDown,
|
||||
handleGripTouchStart,
|
||||
increaseHeightForEmptyContent,
|
||||
} = useAutoResize(chatInputRef, {
|
||||
minHeight: 20,
|
||||
maxHeight: 400,
|
||||
onHeightChange: handleHeightChange,
|
||||
onGripDragStart: handleDragStart,
|
||||
onGripDragEnd: handleDragEnd,
|
||||
value: messageToSend ?? undefined,
|
||||
enableManualResize: true,
|
||||
});
|
||||
} = useGripResize(
|
||||
chatInputRef as React.RefObject<HTMLDivElement | null>,
|
||||
messageToSend,
|
||||
);
|
||||
|
||||
const { handleSubmit, handleResumeAgent, handleStop } = useChatSubmission(
|
||||
chatInputRef as React.RefObject<HTMLDivElement | null>,
|
||||
fileInputRef as React.RefObject<HTMLInputElement | null>,
|
||||
smartResize,
|
||||
onSubmit,
|
||||
);
|
||||
|
||||
const { handleInput, handlePaste, handleKeyDown, handleBlur, handleFocus } =
|
||||
useChatInputEvents(
|
||||
chatInputRef as React.RefObject<HTMLDivElement | null>,
|
||||
smartResize,
|
||||
increaseHeightForEmptyContent,
|
||||
checkIsContentEmpty,
|
||||
clearEmptyContentHandler,
|
||||
onFocus,
|
||||
onBlur,
|
||||
);
|
||||
|
||||
// Cleanup: reset suggestions visibility when component unmounts
|
||||
useEffect(
|
||||
() => () => {
|
||||
dispatch(setShouldHideSuggestions(false));
|
||||
dispatch(clearAllFiles());
|
||||
setShouldHideSuggestions(false);
|
||||
clearAllFiles();
|
||||
},
|
||||
[dispatch],
|
||||
[setShouldHideSuggestions, clearAllFiles],
|
||||
);
|
||||
|
||||
// Function to add files and notify parent
|
||||
const addFiles = useCallback(
|
||||
(files: File[]) => {
|
||||
// Call onFilesPaste if provided with the new files
|
||||
if (onFilesPaste && files.length > 0) {
|
||||
onFilesPaste(files);
|
||||
}
|
||||
},
|
||||
[onFilesPaste],
|
||||
);
|
||||
|
||||
// File icon click handler
|
||||
const handleFileIconClick = () => {
|
||||
if (!isDisabled && fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
// File input change handler
|
||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
addFiles(files);
|
||||
};
|
||||
|
||||
// Drag and drop event handlers
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
if (isDisabled) return;
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
if (isDisabled) return;
|
||||
e.preventDefault();
|
||||
// Only remove drag-over class if we're leaving the container entirely
|
||||
if (!chatContainerRef.current?.contains(e.relatedTarget as Node)) {
|
||||
setIsDragOver(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
if (isDisabled) return;
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
addFiles(files);
|
||||
};
|
||||
|
||||
// Send button click handler
|
||||
const handleSubmit = () => {
|
||||
const message = chatInputRef.current?.innerText || "";
|
||||
|
||||
if (message.trim()) {
|
||||
onSubmit(message);
|
||||
|
||||
// Clear the input
|
||||
if (chatInputRef.current) {
|
||||
chatInputRef.current.textContent = "";
|
||||
}
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
|
||||
// Reset height and show suggestions again
|
||||
smartResize();
|
||||
}
|
||||
};
|
||||
|
||||
// Resume agent button click handler
|
||||
const handleResumeAgent = () => {
|
||||
const message = chatInputRef.current?.innerText || "continue";
|
||||
|
||||
onSubmit(message.trim());
|
||||
|
||||
// Clear the input
|
||||
if (chatInputRef.current) {
|
||||
chatInputRef.current.textContent = "";
|
||||
}
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
|
||||
// Reset height and show suggestions again
|
||||
smartResize();
|
||||
};
|
||||
|
||||
// Handle stop button click
|
||||
const handleStop = () => {
|
||||
if (onStop) {
|
||||
onStop();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle input events
|
||||
const handleInput = () => {
|
||||
smartResize();
|
||||
|
||||
// Clear empty content to ensure placeholder shows
|
||||
if (chatInputRef.current) {
|
||||
clearEmptyContent();
|
||||
}
|
||||
|
||||
// Ensure cursor stays visible when content is scrollable
|
||||
if (!chatInputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
if (
|
||||
!range.getBoundingClientRect ||
|
||||
!chatInputRef.current.getBoundingClientRect
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = range.getBoundingClientRect();
|
||||
const inputRect = chatInputRef.current.getBoundingClientRect();
|
||||
|
||||
// If cursor is below the visible area, scroll to show it
|
||||
if (rect.bottom > inputRect.bottom) {
|
||||
chatInputRef.current.scrollTop =
|
||||
chatInputRef.current.scrollHeight - chatInputRef.current.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle paste events to clean up formatting
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Get plain text from clipboard
|
||||
const text = e.clipboardData.getData("text/plain");
|
||||
|
||||
// Insert plain text
|
||||
document.execCommand("insertText", false, text);
|
||||
|
||||
// Trigger resize
|
||||
setTimeout(smartResize, 0);
|
||||
};
|
||||
|
||||
// Handle key events
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContentEmpty()) {
|
||||
e.preventDefault();
|
||||
increaseHeightForEmptyContent();
|
||||
return;
|
||||
}
|
||||
|
||||
// Original submit logic - only for desktop without shift key
|
||||
if (!isMobileDevice() && !e.shiftKey && !disabled) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle blur events to ensure placeholder shows when empty
|
||||
const handleBlur = () => {
|
||||
// Clear empty content to ensure placeholder shows
|
||||
if (chatInputRef.current) {
|
||||
clearEmptyContent();
|
||||
}
|
||||
|
||||
// Call the original onBlur callback if provided
|
||||
if (onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`}>
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
accept="*/*"
|
||||
style={{ display: "none" }}
|
||||
<HiddenFileInput
|
||||
fileInputRef={fileInputRef}
|
||||
onChange={handleFileInputChange}
|
||||
data-testid="upload-image-input"
|
||||
/>
|
||||
|
||||
{/* Container with grip */}
|
||||
<div className="relative w-full">
|
||||
{/* Top edge hover area - invisible area that triggers grip visibility */}
|
||||
<div
|
||||
className="absolute -top-[12px] left-0 w-full h-6 lg:h-3 z-20 group"
|
||||
id="resize-grip"
|
||||
onClick={handleTopEdgeClick}
|
||||
>
|
||||
{/* Resize Grip - appears on hover of top edge area, when dragging, or when clicked */}
|
||||
<div
|
||||
ref={gripRef}
|
||||
className={cn(
|
||||
"absolute top-[4px] left-0 w-full h-[3px] bg-white cursor-ns-resize z-10 transition-opacity duration-200",
|
||||
isGripVisible
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100",
|
||||
)}
|
||||
onMouseDown={handleGripMouseDown}
|
||||
onTouchStart={handleGripTouchStart}
|
||||
style={{ userSelect: "none" }}
|
||||
/>
|
||||
</div>
|
||||
<ChatInputGrip
|
||||
gripRef={gripRef}
|
||||
isGripVisible={isGripVisible}
|
||||
handleTopEdgeClick={handleTopEdgeClick}
|
||||
handleGripMouseDown={handleGripMouseDown}
|
||||
handleGripTouchStart={handleGripTouchStart}
|
||||
/>
|
||||
|
||||
{/* Chat Input Component */}
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className="bg-[#25272D] box-border content-stretch flex flex-col items-start justify-center p-4 pt-3 relative rounded-[15px] w-full"
|
||||
<ChatInputContainer
|
||||
chatContainerRef={chatContainerRef}
|
||||
isDragOver={isDragOver}
|
||||
disabled={isDisabled}
|
||||
showButton={showButton}
|
||||
buttonClassName={buttonClassName}
|
||||
conversationStatus={conversationStatus}
|
||||
chatInputRef={chatInputRef}
|
||||
handleFileIconClick={handleFileIconClick}
|
||||
handleSubmit={handleSubmit}
|
||||
handleStop={handleStop}
|
||||
handleResumeAgent={handleResumeAgent}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Drag Over UI */}
|
||||
{isDragOver && <DragOver />}
|
||||
|
||||
<UploadedFiles />
|
||||
{/* Main Input Row */}
|
||||
<div className="box-border content-stretch flex flex-row items-end justify-between p-0 relative shrink-0 w-full pb-[18px] gap-2">
|
||||
<div className="basis-0 box-border content-stretch flex flex-row gap-4 grow items-end justify-start min-h-px min-w-px p-0 relative shrink-0">
|
||||
<ChatAddFileButton
|
||||
disabled={disabled}
|
||||
handleFileIconClick={handleFileIconClick}
|
||||
/>
|
||||
|
||||
{/* Chat Input Area */}
|
||||
<div
|
||||
className="box-border content-stretch flex flex-row items-center justify-start min-h-6 p-0 relative shrink-0 flex-1"
|
||||
data-name="Text & caret"
|
||||
>
|
||||
<div className="basis-0 flex flex-col font-normal grow justify-center leading-[0] min-h-px min-w-px overflow-ellipsis overflow-hidden relative shrink-0 text-[#d0d9fa] text-[16px] text-left">
|
||||
<div
|
||||
ref={chatInputRef}
|
||||
className="chat-input bg-transparent text-white text-[16px] font-normal leading-[20px] outline-none resize-none custom-scrollbar min-h-[20px] max-h-[400px] [text-overflow:inherit] [text-wrap-mode:inherit] [white-space-collapse:inherit] block whitespace-pre-wrap"
|
||||
contentEditable
|
||||
data-placeholder={t("SUGGESTIONS$WHAT_TO_BUILD")}
|
||||
data-testid="chat-input"
|
||||
onInput={handleInput}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Send Button */}
|
||||
{showButton && (
|
||||
<ChatSendButton
|
||||
buttonClassName={cn(buttonClassName, "translate-y-[3px]")}
|
||||
handleSubmit={handleSubmit}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tools />
|
||||
<ServerStatus conversationStatus={conversationStatus} />
|
||||
</div>
|
||||
<AgentStatus
|
||||
handleStop={handleStop}
|
||||
handleResumeAgent={handleResumeAgent}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
onInput={handleInput}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={(e) => handleKeyDown(e, isDisabled, handleSubmit)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onStop={onStop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import {
|
||||
FileWriteAction,
|
||||
CommandAction,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { ErrorMessage } from "../error-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface ErrorEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ErrorEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: ErrorEventMessageProps) {
|
||||
if (!isErrorObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isFinishAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface FinishEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function FinishEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: FinishEventMessageProps) {
|
||||
if (!isFinishAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface GenericEventMessageWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function GenericEventMessageWrapper({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: GenericEventMessageWrapperProps) {
|
||||
return (
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { ErrorEventMessage } from "./error-event-message";
|
||||
export { UserAssistantEventMessage } from "./user-assistant-event-message";
|
||||
export { FinishEventMessage } from "./finish-event-message";
|
||||
export { RejectEventMessage } from "./reject-event-message";
|
||||
export { McpEventMessage } from "./mcp-event-message";
|
||||
export { TaskTrackingEventMessage } from "./task-tracking-event-message";
|
||||
export { ObservationPairEventMessage } from "./observation-pair-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
export { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { LikertScale } from "../../feedback/likert-scale";
|
||||
|
||||
interface LikertScaleWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function LikertScaleWrapper({
|
||||
event,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: LikertScaleWrapperProps) {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isMcpObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { MCPObservationContent } from "../mcp-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface McpEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function McpEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: McpEventMessageProps) {
|
||||
if (!isMcpObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "../microagent/microagent-status-indicator";
|
||||
|
||||
interface MicroagentStatusWrapperProps {
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function MicroagentStatusWrapper({
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: MicroagentStatusWrapperProps) {
|
||||
if (!microagentStatus || !actions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface ObservationPairEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function ObservationPairEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: ObservationPairEventMessageProps) {
|
||||
if (!isOpenHandsAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isRejectObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
|
||||
interface RejectEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
}
|
||||
|
||||
export function RejectEventMessage({ event }: RejectEventMessageProps) {
|
||||
if (!isRejectObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isTaskTrackingObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { TaskTrackingObservationContent } from "../task-tracking-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface TaskTrackingEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function TaskTrackingEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: TaskTrackingEventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isTaskTrackingObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isUserMessage, isAssistantMessage } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { ImageCarousel } from "../../images/image-carousel";
|
||||
import { FileList } from "../../files/file-list";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { parseMessageFromEvent } from "../event-content-helpers/parse-message-from-event";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface UserAssistantEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAssistantEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: UserAssistantEventMessageProps) {
|
||||
if (!isUserMessage(event) && !isAssistantMessage(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
{isAssistantMessage(event) && event.action === "message" && (
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +1,29 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import {
|
||||
isUserMessage,
|
||||
isErrorObservation,
|
||||
isAssistantMessage,
|
||||
isOpenHandsAction,
|
||||
isOpenHandsObservation,
|
||||
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";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "./microagent/microagent-status-indicator";
|
||||
import { FileList } from "../files/file-list";
|
||||
import { parseMessageFromEvent } from "./event-content-helpers/parse-message-from-event";
|
||||
import { LikertScale } from "../feedback/likert-scale";
|
||||
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
import {
|
||||
ErrorEventMessage,
|
||||
UserAssistantEventMessage,
|
||||
FinishEventMessage,
|
||||
RejectEventMessage,
|
||||
McpEventMessage,
|
||||
TaskTrackingEventMessage,
|
||||
ObservationPairEventMessage,
|
||||
GenericEventMessageWrapper,
|
||||
} from "./event-message-components";
|
||||
|
||||
interface EventMessageProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
@@ -51,6 +41,7 @@ interface EventMessageProps {
|
||||
isInLast10Actions: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
export function EventMessage({
|
||||
event,
|
||||
hasObservationPair,
|
||||
@@ -62,7 +53,6 @@ export function EventMessage({
|
||||
actions,
|
||||
isInLast10Actions,
|
||||
}: EventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowConfirmationButtons =
|
||||
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
|
||||
|
||||
@@ -73,194 +63,83 @@ export function EventMessage({
|
||||
isLoading: isCheckingFeedback,
|
||||
} = useFeedbackExists(event.id);
|
||||
|
||||
const renderLikertScale = () => {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
// Common props for components that need them
|
||||
const commonProps = {
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
};
|
||||
|
||||
// Error observations
|
||||
if (isErrorObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</div>
|
||||
);
|
||||
return <ErrorEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// Observation pairs with OpenHands actions
|
||||
if (hasObservationPair && isOpenHandsAction(event)) {
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return microagentStatus && actions ? (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
return (
|
||||
<ObservationPairEventMessage
|
||||
event={event}
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
// Finish actions
|
||||
if (isFinishAction(event)) {
|
||||
return (
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
return <FinishEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// User and assistant messages
|
||||
if (isUserMessage(event) || isAssistantMessage(event)) {
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{isAssistantMessage(event) &&
|
||||
event.action === "message" &&
|
||||
renderLikertScale()}
|
||||
</>
|
||||
<UserAssistantEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Reject observations
|
||||
if (isRejectObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
return <RejectEventMessage event={event} />;
|
||||
}
|
||||
|
||||
// MCP observations
|
||||
if (isMcpObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
<McpEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
// Task tracking observations
|
||||
if (isTaskTrackingObservation(event)) {
|
||||
return (
|
||||
<TaskTrackingEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return (
|
||||
<GenericEventMessageWrapper
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,12 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPrButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPrButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPrButtonProps) {
|
||||
@@ -24,7 +22,7 @@ export function GitControlBarPrButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
|
||||
const handlePrClick = () => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
|
||||
@@ -8,12 +8,10 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface GitControlBarPullButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBarPullButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
}: GitControlBarPullButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -22,7 +20,7 @@ export function GitControlBarPullButton({
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const hasRepository = conversation?.selected_repository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
|
||||
const handlePullClick = () => {
|
||||
posthog.capture("pull_button_clicked");
|
||||
|
||||
@@ -8,14 +8,12 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPushButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPushButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPushButtonProps) {
|
||||
@@ -24,7 +22,7 @@ export function GitControlBarPushButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
|
||||
const handlePushClick = () => {
|
||||
posthog.capture("push_button_clicked");
|
||||
|
||||
@@ -11,17 +11,9 @@ import { GitControlBarTooltipWrapper } from "./git-control-bar-tooltip-wrapper";
|
||||
|
||||
interface GitControlBarProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBar({
|
||||
onSuggestionsClick,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: GitControlBarProps) {
|
||||
export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: conversation } = useActiveConversation();
|
||||
@@ -30,12 +22,6 @@ export function GitControlBar({
|
||||
const gitProvider = conversation?.git_provider as Provider;
|
||||
const selectedBranch = conversation?.selected_branch;
|
||||
|
||||
// Button is enabled when the agent is waiting for user input, has substantive actions, and no optimistic message
|
||||
const isButtonEnabled =
|
||||
isWaitingForUserInput &&
|
||||
hasSubstantiveAgentActions &&
|
||||
!optimisticUserMessage;
|
||||
|
||||
const hasRepository = !!selectedRepository;
|
||||
|
||||
return (
|
||||
@@ -73,7 +59,6 @@ export function GitControlBar({
|
||||
>
|
||||
<GitControlBarPullButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
/>
|
||||
</GitControlBarTooltipWrapper>
|
||||
|
||||
@@ -84,7 +69,6 @@ export function GitControlBar({
|
||||
>
|
||||
<GitControlBarPushButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
@@ -97,7 +81,6 @@ export function GitControlBar({
|
||||
>
|
||||
<GitControlBarPrButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { isFileImage } from "#/utils/is-file-image";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { CustomChatInput } from "./custom-chat-input";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { GitControlBar } from "./git-control-bar";
|
||||
import {
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { processFiles, processImages } from "#/utils/file-processing";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface InteractiveChatBoxProps {
|
||||
onSubmit: (message: string, images: File[], files: File[]) => void;
|
||||
onStop: () => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function InteractiveChatBox({
|
||||
onSubmit,
|
||||
onStop,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: InteractiveChatBoxProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} = useConversationStore();
|
||||
const curAgentState = useSelector(
|
||||
(state: RootState) => state.agent.curAgentState,
|
||||
);
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
// Helper function to validate and filter files
|
||||
@@ -58,26 +52,24 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to show loading indicators for files
|
||||
const showLoadingIndicators = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => dispatch(addFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(addImageLoading(image.name)));
|
||||
validFiles.forEach((file) => addFileLoading(file.name));
|
||||
validImages.forEach((image) => addImageLoading(image.name));
|
||||
};
|
||||
|
||||
// Helper function to handle successful file processing results
|
||||
const handleSuccessfulFiles = (fileResults: { successful: File[] }) => {
|
||||
if (fileResults.successful.length > 0) {
|
||||
dispatch(addFiles(fileResults.successful));
|
||||
fileResults.successful.forEach((file) =>
|
||||
dispatch(removeFileLoading(file.name)),
|
||||
);
|
||||
addFiles(fileResults.successful);
|
||||
fileResults.successful.forEach((file) => removeFileLoading(file.name));
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle successful image processing results
|
||||
const handleSuccessfulImages = (imageResults: { successful: File[] }) => {
|
||||
if (imageResults.successful.length > 0) {
|
||||
dispatch(addImages(imageResults.successful));
|
||||
addImages(imageResults.successful);
|
||||
imageResults.successful.forEach((image) =>
|
||||
dispatch(removeImageLoading(image.name)),
|
||||
removeImageLoading(image.name),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -88,14 +80,14 @@ export function InteractiveChatBox({
|
||||
imageResults: { failed: { file: File; error: Error }[] },
|
||||
) => {
|
||||
fileResults.failed.forEach(({ file, error }) => {
|
||||
dispatch(removeFileLoading(file.name));
|
||||
removeFileLoading(file.name);
|
||||
displayErrorToast(
|
||||
`Failed to process file ${file.name}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
imageResults.failed.forEach(({ file, error }) => {
|
||||
dispatch(removeImageLoading(file.name));
|
||||
removeImageLoading(file.name);
|
||||
displayErrorToast(
|
||||
`Failed to process image ${file.name}: ${error.message}`,
|
||||
);
|
||||
@@ -104,8 +96,8 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to clear loading states on error
|
||||
const clearLoadingStates = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => dispatch(removeFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(removeImageLoading(image.name)));
|
||||
validFiles.forEach((file) => removeFileLoading(file.name));
|
||||
validImages.forEach((image) => removeImageLoading(image.name));
|
||||
};
|
||||
|
||||
const handleUpload = async (selectedFiles: File[]) => {
|
||||
@@ -140,7 +132,7 @@ export function InteractiveChatBox({
|
||||
|
||||
const handleSubmit = (message: string) => {
|
||||
onSubmit(message, images, files);
|
||||
dispatch(clearAllFiles());
|
||||
clearAllFiles();
|
||||
};
|
||||
|
||||
const handleSuggestionsClick = (suggestion: string) => {
|
||||
@@ -161,12 +153,7 @@ export function InteractiveChatBox({
|
||||
conversationStatus={conversation?.status || null}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<GitControlBar
|
||||
onSuggestionsClick={handleSuggestionsClick}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={optimisticUserMessage}
|
||||
/>
|
||||
<GitControlBar onSuggestionsClick={handleSuggestionsClick} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useHandleRuntimeActive } from "#/hooks/use-handle-runtime-active";
|
||||
import { LoadingMicroagentBody } from "./loading-microagent-body";
|
||||
import { LoadingMicroagentTextarea } from "./loading-microagent-textarea";
|
||||
import { useGetMicroagents } from "#/hooks/query/use-get-microagents";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface LaunchMicroagentModalProps {
|
||||
onClose: () => void;
|
||||
@@ -76,9 +77,9 @@ export function LaunchMicroagentModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
<Typography.Text className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
{t("MICROAGENT$DEFINITION")}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
|
||||
<form
|
||||
data-testid="launch-microagent-modal"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
export function LoadingMicroagentBody() {
|
||||
const { t } = useTranslation();
|
||||
@@ -10,7 +11,7 @@ export function LoadingMicroagentBody() {
|
||||
{t("MICROAGENT$ADD_TO_MICROAGENT")}
|
||||
</h2>
|
||||
<Spinner size="lg" />
|
||||
<p>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</p>
|
||||
<Typography.Text>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</Typography.Text>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { SuccessIndicator } from "../success-indicator";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface MicroagentStatusIndicatorProps {
|
||||
status: MicroagentStatus;
|
||||
@@ -81,7 +82,9 @@ export function MicroagentStatusIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="underline">{statusText}</span>;
|
||||
return (
|
||||
<Typography.Text className="underline">{statusText}</Typography.Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TaskTrackingObservation } from "#/types/core/observations";
|
||||
import { TaskListSection } from "./task-tracking/task-list-section";
|
||||
import { ResultSection } from "./task-tracking/result-section";
|
||||
|
||||
interface TaskTrackingObservationContentProps {
|
||||
event: TaskTrackingObservation;
|
||||
@@ -9,101 +9,17 @@ interface TaskTrackingObservationContentProps {
|
||||
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>
|
||||
)}
|
||||
{shouldShowTaskList && <TaskListSection taskList={taskList} />}
|
||||
|
||||
{/* 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>
|
||||
<ResultSection content={event.content} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface ResultSectionProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function ResultSection({ content }: ResultSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.H3>{t("TASK_TRACKING_OBSERVATION$RESULT")}</Typography.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">{content.trim()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { getStatusClassName } from "#/utils/utils";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function StatusBadge({ status }: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded uppercase font-semibold ${getStatusClassName(
|
||||
status,
|
||||
)}`}
|
||||
>
|
||||
{status.replace("_", " ")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getStatusIcon } from "#/utils/utils";
|
||||
|
||||
interface StatusIconProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function StatusIcon({ status }: StatusIconProps) {
|
||||
return <span className="text-lg">{getStatusIcon(status)}</span>;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { StatusIcon } from "./status-icon";
|
||||
import { StatusBadge } from "./status-badge";
|
||||
|
||||
interface TaskItemProps {
|
||||
task: {
|
||||
id: string;
|
||||
title: string;
|
||||
status: "todo" | "in_progress" | "done";
|
||||
notes?: string;
|
||||
};
|
||||
index: number;
|
||||
}
|
||||
|
||||
export function TaskItem({ task, index }: TaskItemProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="border-l-2 border-gray-600 pl-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<StatusIcon status={task.status} />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Typography.Text className="text-sm text-gray-400">
|
||||
{index + 1}.
|
||||
</Typography.Text>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
<h4 className="font-medium text-white mb-1">{task.title}</h4>
|
||||
<Typography.Text className="text-xs text-gray-400 mb-1">
|
||||
{t("TASK_TRACKING_OBSERVATION$TASK_ID")}: {task.id}
|
||||
</Typography.Text>
|
||||
{task.notes && (
|
||||
<Typography.Text className="text-sm text-gray-300 italic">
|
||||
{t("TASK_TRACKING_OBSERVATION$TASK_NOTES")}: {task.notes}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TaskItem } from "./task-item";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface TaskListSectionProps {
|
||||
taskList: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
status: "todo" | "in_progress" | "done";
|
||||
notes?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function TaskListSection({ taskList }: TaskListSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.H3>
|
||||
{t("TASK_TRACKING_OBSERVATION$TASK_LIST")} ({taskList.length}{" "}
|
||||
{taskList.length === 1 ? "item" : "items"})
|
||||
</Typography.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) => (
|
||||
<TaskItem key={task.id} task={task} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { UploadedFile } from "./uploaded-file";
|
||||
import { UploadedImage } from "./uploaded-image";
|
||||
import { removeFile, removeImage } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export function UploadedFiles() {
|
||||
const dispatch = useDispatch();
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const loadingFiles = useSelector(
|
||||
(state: RootState) => state.conversation.loadingFiles,
|
||||
);
|
||||
const loadingImages = useSelector(
|
||||
(state: RootState) => state.conversation.loadingImages,
|
||||
);
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
loadingFiles,
|
||||
loadingImages,
|
||||
removeFile,
|
||||
removeImage,
|
||||
} = useConversationStore();
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
dispatch(removeFile(index));
|
||||
removeFile(index);
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
dispatch(removeImage(index));
|
||||
removeImage(index);
|
||||
};
|
||||
|
||||
// Don't render anything if there are no files, images, or loading items
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Utility functions for chat input component
|
||||
*/
|
||||
/* eslint-disable no-param-reassign */
|
||||
/**
|
||||
* Check if contentEditable element is truly empty
|
||||
*/
|
||||
export const isContentEmpty = (element: HTMLDivElement | null): boolean => {
|
||||
if (!element) {
|
||||
return true;
|
||||
}
|
||||
const text = element.innerText || element.textContent || "";
|
||||
return text.trim() === "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear empty content from contentEditable element for placeholder display
|
||||
*/
|
||||
export const clearEmptyContent = (element: HTMLDivElement | null): void => {
|
||||
if (element && isContentEmpty(element)) {
|
||||
element.innerHTML = "";
|
||||
element.textContent = "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get text content from contentEditable element
|
||||
*/
|
||||
export const getTextContent = (element: HTMLDivElement | null): string =>
|
||||
element?.innerText || "";
|
||||
|
||||
/**
|
||||
* Clear text content from contentEditable element
|
||||
*/
|
||||
export const clearTextContent = (element: HTMLDivElement | null): void => {
|
||||
if (element) {
|
||||
element.textContent = "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear file input value
|
||||
*/
|
||||
export const clearFileInput = (element: HTMLInputElement | null): void => {
|
||||
if (element) {
|
||||
element.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure cursor stays visible when content is scrollable
|
||||
*/
|
||||
export const ensureCursorVisible = (element: HTMLDivElement | null): void => {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
if (!range.getBoundingClientRect || !element.getBoundingClientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = range.getBoundingClientRect();
|
||||
const inputRect = element.getBoundingClientRect();
|
||||
|
||||
// If cursor is below the visible area, scroll to show it
|
||||
if (rect.bottom > inputRect.bottom) {
|
||||
element.scrollTop = element.scrollHeight - element.clientHeight;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { RootState } from "#/store";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { getStatusCode } from "#/utils/status";
|
||||
@@ -11,7 +12,7 @@ import ClockIcon from "#/icons/u-clock-three.svg?react";
|
||||
import { ChatResumeAgentButton } from "../chat/chat-play-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { AgentLoading } from "./agent-loading";
|
||||
import { setShouldShownAgentLoading } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
|
||||
export interface AgentStatusProps {
|
||||
@@ -28,9 +29,9 @@ export function AgentStatus({
|
||||
disabled = false,
|
||||
}: AgentStatusProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { setShouldShownAgentLoading } = useConversationStore();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curStatusMessage } = useSelector((state: RootState) => state.status);
|
||||
const { curStatusMessage } = useStatusStore();
|
||||
const { webSocketStatus } = useWsClient();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
@@ -57,8 +58,8 @@ export function AgentStatus({
|
||||
|
||||
// Update global state when agent loading condition changes
|
||||
useEffect(() => {
|
||||
dispatch(setShouldShownAgentLoading(shouldShownAgentLoading));
|
||||
}, [shouldShownAgentLoading, dispatch]);
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
getCreatePRPrompt,
|
||||
getCreateNewBranchPrompt,
|
||||
} from "#/utils/utils";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
@@ -28,28 +27,28 @@ interface GitToolsSubmenuProps {
|
||||
|
||||
export function GitToolsSubmenu({ onClose }: GitToolsSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
const currentGitProvider = conversation?.git_provider as Provider;
|
||||
|
||||
const onGitPull = () => {
|
||||
dispatch(setMessageToSend(getGitPullPrompt()));
|
||||
setMessageToSend(getGitPullPrompt());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onGitPush = () => {
|
||||
dispatch(setMessageToSend(getGitPushPrompt(currentGitProvider)));
|
||||
setMessageToSend(getGitPushPrompt(currentGitProvider));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreatePR = () => {
|
||||
dispatch(setMessageToSend(getCreatePRPrompt(currentGitProvider)));
|
||||
setMessageToSend(getCreatePRPrompt(currentGitProvider));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreateNewBranch = () => {
|
||||
dispatch(setMessageToSend(getCreateNewBranchPrompt()));
|
||||
setMessageToSend(getCreateNewBranchPrompt());
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -9,7 +8,7 @@ import PrStatusIcon from "#/icons/pr-status.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import WaterIcon from "#/icons/u-water.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { REPO_SUGGESTIONS } from "#/utils/suggestions/repo-suggestions";
|
||||
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||
|
||||
@@ -22,22 +21,22 @@ interface MacrosSubmenuProps {
|
||||
|
||||
export function MacrosSubmenu({ onClose }: MacrosSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
|
||||
const onIncreaseTestCoverage = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE));
|
||||
setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE);
|
||||
onClose();
|
||||
};
|
||||
const onFixReadme = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.FIX_README));
|
||||
setMessageToSend(REPO_SUGGESTIONS.FIX_README);
|
||||
onClose();
|
||||
};
|
||||
const onAutoMergePRs = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS));
|
||||
setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS);
|
||||
onClose();
|
||||
};
|
||||
const onCleanDependencies = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES));
|
||||
setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
|
||||
import EllipsisIcon from "#/icons/ellipsis.svg?react";
|
||||
|
||||
interface ConversationCardActionsProps {
|
||||
contextMenuOpen: boolean;
|
||||
onContextMenuToggle: (isOpen: boolean) => void;
|
||||
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onStop?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
conversationStatus?: ConversationStatus;
|
||||
conversationId?: string;
|
||||
showOptions?: boolean;
|
||||
}
|
||||
|
||||
export function ConversationCardActions({
|
||||
contextMenuOpen,
|
||||
onContextMenuToggle,
|
||||
onDelete,
|
||||
onStop,
|
||||
onEdit,
|
||||
onDownloadViaVSCode,
|
||||
conversationStatus,
|
||||
conversationId,
|
||||
showOptions,
|
||||
}: ConversationCardActionsProps) {
|
||||
return (
|
||||
<div className="group">
|
||||
<button
|
||||
data-testid="ellipsis-button"
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onContextMenuToggle(!contextMenuOpen);
|
||||
}}
|
||||
className="cursor-pointer w-6 h-6 flex flex-row items-center justify-end"
|
||||
>
|
||||
<EllipsisIcon />
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
// Show on hover (desktop) or when explicitly opened (click/touch)
|
||||
"relative opacity-0 invisible group-hover:opacity-100 group-hover:visible",
|
||||
// Override hover styles when explicitly opened via click
|
||||
contextMenuOpen && "opacity-100 visible",
|
||||
)}
|
||||
>
|
||||
<ConversationCardContextMenu
|
||||
onClose={() => onContextMenuToggle(false)}
|
||||
onDelete={onDelete}
|
||||
onStop={conversationStatus !== "STOPPED" ? onStop : undefined}
|
||||
onEdit={onEdit}
|
||||
onDownloadViaVSCode={
|
||||
conversationId && showOptions ? onDownloadViaVSCode : undefined
|
||||
}
|
||||
position="bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RepositorySelection } from "#/api/open-hands.types";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import { NoRepository } from "./no-repository";
|
||||
|
||||
interface ConversationCardFooterProps {
|
||||
selectedRepository: RepositorySelection | null;
|
||||
lastUpdatedAt: string; // ISO 8601
|
||||
createdAt?: string; // ISO 8601
|
||||
}
|
||||
|
||||
export function ConversationCardFooter({
|
||||
selectedRepository,
|
||||
lastUpdatedAt,
|
||||
createdAt,
|
||||
}: ConversationCardFooterProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-row justify-between items-center mt-1")}>
|
||||
{selectedRepository?.selected_repository ? (
|
||||
<ConversationRepoLink selectedRepository={selectedRepository} />
|
||||
) : (
|
||||
<NoRepository />
|
||||
)}
|
||||
{(createdAt ?? lastUpdatedAt) && (
|
||||
<p className="text-xs text-[#A3A3A3] flex-1 text-right">
|
||||
<time>
|
||||
{`${formatTimeDelta(new Date(lastUpdatedAt ?? createdAt))} ${t(I18nKey.CONVERSATION$AGO)}`}
|
||||
</time>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user