Compare commits

..

10 Commits

Author SHA1 Message Date
Robert Brennan bb33535275 fix timesouts 2024-11-03 12:39:20 -05:00
Robert Brennan 00739e3270 revert some changes 2024-11-03 12:32:30 -05:00
Robert Brennan 4a41c979cb fix log stack 2024-11-03 12:23:49 -05:00
Robert Brennan 032a577a3a remove max_retires 2024-11-03 12:21:39 -05:00
Robert Brennan ea511af181 revert log_gen 2024-11-03 12:20:27 -05:00
Robert Brennan eb639fad62 fix try 2024-11-03 12:16:28 -05:00
openhands 6aff0459eb fix: improve runtime shutdown handling
- Add proper timeouts to runtime startup and shutdown
- Improve container cleanup with graceful shutdown
- Add better error handling in log buffer
- Add proper thread cleanup
- Add timeouts to all blocking operations

Key improvements:
1. Shorter timeouts for runtime startup (30s vs 120s)
2. Better container cleanup with graceful stop then force kill
3. Proper thread cleanup in log buffer
4. Better error handling during shutdown
5. Proper cleanup order (requests -> logs -> container)
6. Detailed error logging during cleanup
2024-11-03 17:12:25 +00:00
Robert Brennan 7738de78d3 fix indent 2024-11-03 12:03:55 -05:00
openhands 6280ee7a03 fix: improve agent session cleanup and startup
- Add timeouts to all critical operations in session cleanup
- Add proper task cancellation and cleanup order
- Improve error handling and logging during cleanup
- Add timeouts and error handling to session startup
- Add proper thread and event loop handling
- Add cleanup of background tasks

Key improvements:
1. Cancel agent tasks before cleanup
2. Add timeouts to all async operations
3. Proper cleanup order (tasks before loop)
4. Better error handling and recovery
5. Proper thread and loop management
6. Detailed error logging
2024-11-03 17:02:38 +00:00
openhands f93701a76a fix: improve websocket handling robustness
- Add heartbeat mechanism to detect disconnected clients
- Add timeouts to all critical websocket operations
- Improve error handling and recovery
- Add proper cleanup of resources and background tasks
- Prevent deadlocks in event processing
- Add better logging for websocket errors

These changes make the websocket handling more robust and prevent
server lockups when connections close unexpectedly. Key improvements:

1. Heartbeat every 30s to detect disconnected clients
2. Timeouts on all critical operations
3. Better error handling and recovery
4. Proper cleanup of resources
5. Prevention of deadlocks
6. Detailed error logging
2024-11-03 16:55:16 +00:00
234 changed files with 9636 additions and 12422 deletions
-2
View File
@@ -31,8 +31,6 @@ body:
options:
- Docker command in README
- Development workflow
- app.all-hands.dev
- Other
default: 0
- type: input
+2 -2
View File
@@ -424,9 +424,9 @@ jobs:
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
--name openhands-app-$SHORT_SHA \
docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA"
ghcr.io/all-hands-ai/runtime:$SHORT_SHA"
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body)
+1 -1
View File
@@ -11,5 +11,5 @@ jobs:
uses: All-Hands-AI/openhands-resolver/.github/workflows/openhands-resolver.yml@main
if: github.event.label.name == 'fix-me'
with:
max_iterations: 50
issue_number: ${{ github.event.issue.number }}
secrets: inherit
+1 -1
View File
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
### 9. Use existing Docker image
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image. Follow these steps:
1. Set the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.13-nikolaik
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik
## Develop inside Docker container
+4 -5
View File
@@ -38,16 +38,15 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik
docker run -it --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
-e LOG_ALL_EVENTS=true \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.13
docker.all-hands.dev/all-hands-ai/openhands:0.12
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.13-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.9-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -2
View File
@@ -32,8 +32,7 @@ workspace_base = "./workspace"
# Enable saving and restoring the session when run from CLI
#enable_cli_session = false
# Path to store trajectories, can be a folder or a file
# If it's a folder, the session id will be used as the file name
# Path to store trajectories
#trajectories_path="./trajectories"
# File store path
+1 -1
View File
@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.13-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.9-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
@@ -161,7 +161,7 @@ Pour créer un workflow d'évaluation pour votre benchmark, suivez ces étapes :
instruction=instruction,
test_result=evaluation_result,
metadata=metadata,
history=compatibility_for_eval_history_pairs(state.history),
history=state.history.compatibility_for_eval_history_pairs(),
metrics=state.metrics.get() if state.metrics else None,
error=state.last_error if state and state.last_error else None,
)
@@ -260,7 +260,7 @@ def codeact_user_response(state: State | None) -> str:
# vérifier si l'agent a essayé de parler à l'utilisateur 3 fois, si oui, faire savoir à l'agent qu'il peut abandonner
user_msgs = [
event
for event in state.history
for event in state.history.get_events()
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:
@@ -279,3 +279,4 @@ Cette fonction fait ce qui suit :
3. Si l'agent a fait plusieurs tentatives, il lui donne la possibilité d'abandonner
En utilisant cette fonction, vous pouvez garantir un comportement cohérent sur plusieurs exécutions d'évaluation et empêcher l'agent de rester bloqué en attendant une entrée humaine.
@@ -158,7 +158,7 @@ OpenHands 的主要入口点在 `openhands/core/main.py` 中。以下是它工
instruction=instruction,
test_result=evaluation_result,
metadata=metadata,
history=compatibility_for_eval_history_pairs(state.history),
history=state.history.compatibility_for_eval_history_pairs(),
metrics=state.metrics.get() if state.metrics else None,
error=state.last_error if state and state.last_error else None,
)
@@ -257,7 +257,7 @@ def codeact_user_response(state: State | None) -> str:
# 检查代理是否已尝试与用户对话 3 次,如果是,让代理知道它可以放弃
user_msgs = [
event
for event in state.history
for event in state.history.get_events()
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:
@@ -58,3 +58,4 @@ docker run -it \
ghcr.io/all-hands-ai/openhands:0.11 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+2 -2
View File
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.13 \
docker.all-hands.dev/all-hands-ai/openhands:0.12 \
python -m openhands.core.cli
```
@@ -158,7 +158,7 @@ To create an evaluation workflow for your benchmark, follow these steps:
instruction=instruction,
test_result=evaluation_result,
metadata=metadata,
history=compatibility_for_eval_history_pairs(state.history),
history=state.history.compatibility_for_eval_history_pairs(),
metrics=state.metrics.get() if state.metrics else None,
error=state.last_error if state and state.last_error else None,
)
@@ -257,7 +257,7 @@ def codeact_user_response(state: State | None) -> str:
# check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
user_msgs = [
event
for event in state.history
for event in state.history.get_events()
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:
-9
View File
@@ -19,15 +19,6 @@ OpenHands provides a user-friendly Graphical User Interface (GUI) mode for inter
3. Enter the corresponding `API Key` for your chosen provider.
4. Click "Save" to apply the settings.
### GitHub Token Setup
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
1. Locally (OSS): The user directly inputs their GitHub token.
2. Online (SaaS): The token is obtained through GitHub OAuth authentication.
When you reach the `/app` route, the app checks if a token is present. If it finds one, it sets it in the environment for the agent to use.
### Advanced Settings
1. Toggle `Advanced Options` to access additional settings.
+2 -3
View File
@@ -44,16 +44,15 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
-e LLM_MODEL=$LLM_MODEL \
-e LOG_ALL_EVENTS=true \
-v $WORKSPACE_BASE:/opt/workspace_base \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.13 \
docker.all-hands.dev/all-hands-ai/openhands:0.12 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+3 -4
View File
@@ -11,16 +11,15 @@
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
-e LOG_ALL_EVENTS=true \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.13
docker.all-hands.dev/all-hands-ai/openhands:0.12
```
You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
+2 -2
View File
@@ -4,11 +4,11 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
## Model Recommendations
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and [this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
Based on a recent evaluation of language models for coding tasks (using the SWE-bench dataset), we can provide some recommendations for model selection. The full analysis can be found in [this blog article](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed).
When choosing a model, consider both the quality of outputs and the associated costs. Here's a summary of the findings:
- Claude 3.5 Sonnet is the best by a fair amount, achieving a 53% resolve rate on SWE-Bench Verified with the default agent in OpenHands.
- Claude 3.5 Sonnet is the best by a fair amount, achieving a 27% resolve rate with the default agent in OpenHands.
- GPT-4o lags behind, and o1-mini actually performed somewhat worse than GPT-4o. We went in and analyzed the results a little, and briefly it seemed like o1 was sometimes "overthinking" things, performing extra environment configuration tasks when it could just go ahead and finish the task.
- Finally, the strongest open models were Llama 3.1 405 B and deepseek-v2.5, and they performed reasonably, even besting some of the closed models.
+2
View File
@@ -60,6 +60,7 @@ docker run # ...
-e SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.app.all-hands.dev" \
-e SANDBOX_API_KEY="your-all-hands-api-key" \
-e SANDBOX_KEEP_REMOTE_RUNTIME_ALIVE="true" \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
# ...
```
@@ -74,4 +75,5 @@ docker run # ...
-e RUNTIME=modal \
-e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="your-secret" \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
```
+1255 -1454
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -15,10 +15,10 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.6.0",
"@docusaurus/plugin-content-pages": "^3.6.0",
"@docusaurus/preset-classic": "^3.6.0",
"@docusaurus/theme-mermaid": "^3.6.0",
"@docusaurus/core": "^3.5.2",
"@docusaurus/plugin-content-pages": "^3.5.2",
"@docusaurus/preset-classic": "^3.5.2",
"@docusaurus/theme-mermaid": "^3.5.2",
"@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.4.0",
@@ -29,7 +29,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.6.0",
"@docusaurus/tsconfig": "^3.5.2",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.6.3"
},
+1592 -1811
View File
File diff suppressed because it is too large Load Diff
+3 -6
View File
@@ -8,7 +8,6 @@ from evaluation.EDA.game import Q20Game, Q20GameCelebrity
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -35,8 +34,7 @@ def codeact_user_response_eda(state: State) -> str:
# retrieve the latest model message from history
if state.history:
last_agent_message = state.get_last_agent_message()
model_guess = last_agent_message.content if last_agent_message else ''
model_guess = state.history.get_last_agent_message()
assert game is not None, 'Game is not initialized.'
msg = game.generate_user_response(model_guess)
@@ -141,8 +139,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
last_agent_message = state.get_last_agent_message()
final_message = last_agent_message.content if last_agent_message else ''
final_message = state.history.get_last_agent_message()
logger.info(f'Final message: {final_message} | Ground truth: {instance["text"]}')
test_result = game.reward()
@@ -151,7 +148,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+1
View File
@@ -84,3 +84,4 @@ all the preprocessing/evaluation/analysis scripts.
- Raw data and experimental records should not be stored within this repo.
- For model outputs, they should be stored at [this huggingface space](https://huggingface.co/spaces/OpenHands/evaluation) for visualization.
- Important data files of manageable size and analysis scripts (e.g., jupyter notebooks) can be directly uploaded to this repo.
+2 -3
View File
@@ -16,7 +16,6 @@ from evaluation.agent_bench.helper import (
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -243,7 +242,7 @@ def process_instance(
raw_ans = ''
# retrieve the last agent message or thought
for event in reversed(state.history):
for event in state.history.get_events(reverse=True):
if event.source == 'agent':
if isinstance(event, AgentFinishAction):
raw_ans = event.thought
@@ -272,7 +271,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
metrics = state.metrics.get() if state.metrics else None
+1 -2
View File
@@ -15,7 +15,6 @@ from evaluation.aider_bench.helper import (
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -251,7 +250,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
metrics = state.metrics.get() if state.metrics else None
# Save the output
+1 -2
View File
@@ -13,7 +13,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -300,7 +299,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
test_result['generated'] = test_result['metadata']['1_copy_change_code']
+2 -3
View File
@@ -16,7 +16,6 @@ from tqdm import tqdm
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -47,7 +46,7 @@ def codeact_user_response(state: State) -> str:
# check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
user_msgs = [
event
for event in state.history
for event in state.history.get_events()
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) > 2:
@@ -432,7 +431,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+1 -2
View File
@@ -9,7 +9,6 @@ from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -90,7 +89,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# find the last delegate action
last_delegate_action = None
+3 -4
View File
@@ -15,7 +15,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -174,14 +173,14 @@ def initialize_runtime(runtime: Runtime, data_files: list[str]):
def get_last_agent_finish_action(state: State) -> AgentFinishAction:
for event in reversed(state.history):
for event in state.history.get_events(reverse=True):
if isinstance(event, AgentFinishAction):
return event
return None
def get_last_message_action(state: State) -> MessageAction:
for event in reversed(state.history):
for event in state.history.get_events(reverse=True):
if isinstance(event, MessageAction):
return event
return None
@@ -308,7 +307,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# DiscoveryBench Evaluation
eval_rec = run_eval_gold_vs_gen_NL_hypo_workflow(
+2 -3
View File
@@ -12,7 +12,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -167,7 +166,7 @@ def process_instance(
model_answer_raw = ''
# get the last message or thought from the agent
for event in reversed(state.history):
for event in state.history.get_events(reverse=True):
if event.source == 'agent':
if isinstance(event, AgentFinishAction):
model_answer_raw = event.thought
@@ -204,7 +203,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+2 -4
View File
@@ -10,7 +10,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -102,8 +101,7 @@ def process_instance(
raise ValueError('State should not be None.')
# retrieve the last message from the agent
last_agent_message = state.get_last_agent_message()
model_answer_raw = last_agent_message.content if last_agent_message else ''
model_answer_raw = state.history.get_last_agent_message()
# attempt to parse model_answer
ast_eval_fn = instance['ast_eval']
@@ -116,7 +114,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
output = EvalOutput(
instance_id=instance_id,
+2 -3
View File
@@ -28,7 +28,6 @@ from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -245,7 +244,7 @@ Ok now its time to start solving the question. Good luck!
'C': False,
'D': False,
}
for event in reversed(state.history):
for event in state.history.get_events(reverse=True):
if (
isinstance(event, AgentFinishAction)
and event.source != 'user'
@@ -301,7 +300,7 @@ Ok now its time to start solving the question. Good luck!
instance_id=str(instance.instance_id),
instruction=instruction,
metadata=metadata,
history=compatibility_for_eval_history_pairs(state.history),
history=state.history.compatibility_for_eval_history_pairs(),
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result={
+1 -2
View File
@@ -21,7 +21,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -256,7 +255,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+10 -7
View File
@@ -13,7 +13,6 @@ from evaluation.utils.shared import (
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 (
@@ -56,14 +55,18 @@ def get_config(
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_id
if metadata.llm_config.log_completions:
metadata.llm_config.log_completions_folder = os.path.join(
metadata.eval_output_dir, 'llm_completions', instance_id
)
)
logger.info(
f'Logging LLM completions for instance {instance_id} to '
f'{metadata.llm_config.log_completions_folder}'
)
config.set_llm_config(metadata.llm_config)
agent_config = AgentConfig(
codeact_enable_jupyter=True,
codeact_enable_browsing=True,
codeact_enable_browsing_delegate=True,
codeact_enable_llm_editor=False,
)
config.set_agent_config(agent_config)
@@ -129,7 +132,7 @@ def process_instance(
# # result evaluation
# # =============================================
histories = [event_to_dict(event) for event in state.history]
histories = [event_to_dict(event) for event in state.history.get_events()]
test_result: TestResult = test_class.verify_result(runtime, histories)
metrics = state.metrics.get() if state.metrics else None
@@ -1,44 +0,0 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import AgentFinishAction, MessageAction
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Look at https://github.com/All-Hands-AI/OpenHands/pull/8, and tell me what is happening there and what did @asadm suggest.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
pass
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the "The answer is OpenHands is all you need!" is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
for event in message_actions:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
elif isinstance(event, MessageAction):
content = event.content
else:
raise ValueError(f'Unknown event type: {type(event)}')
if (
'non-commercial' in content
or 'MIT' in content
or 'Apache 2.0' in content
):
return TestResult(success=True)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}. Messages: {message_actions}',
)
+2 -3
View File
@@ -8,7 +8,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -226,7 +225,7 @@ def process_instance(
raise ValueError('State should not be None.')
final_message = ''
for event in reversed(state.history):
for event in state.history.get_events(reverse=True):
if isinstance(event, AgentFinishAction):
final_message = event.thought
break
@@ -248,7 +247,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+11 -42
View File
@@ -10,13 +10,10 @@ import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
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 (
@@ -32,10 +29,7 @@ from openhands.events.action import (
CmdRunAction,
MessageAction,
)
from openhands.events.observation import (
BrowserOutputObservation,
CmdOutputObservation,
)
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
@@ -43,11 +37,7 @@ from openhands.runtime.browser.browser_env import (
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'BrowsingAgent', 'CodeActAgent'}
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
def get_config(
@@ -57,32 +47,25 @@ def get_config(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='xingyaoww/od-eval-miniwob:v1.0',
enable_auto_lint=True,
use_host_network=False,
browsergym_eval_env=env_id,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_remote_runtime_alive=False,
),
# 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, env_id
)
)
config.set_llm_config(metadata.llm_config)
return config
def initialize_runtime(
runtime: Runtime,
) -> tuple[str, BrowserOutputObservation]:
) -> str:
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
@@ -102,14 +85,8 @@ def initialize_runtime(
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
goal = obs.content
# Run noop to get the initial browser observation (e.g., the page URL & content)
action = BrowseInteractiveAction(browser_actions='noop(1000)')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
return goal, obs
return goal
def complete_runtime(
@@ -140,7 +117,7 @@ def process_instance(
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
env_id = instance.instance_id
env_id = instance.id
config = get_config(metadata, env_id)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
@@ -152,12 +129,7 @@ def process_instance(
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str, obs = initialize_runtime(runtime)
task_str += (
f'\nInitial browser state (output of `noop(1000)`):\n{obs.get_agent_obs_text()}'
)
task_str = initialize_runtime(runtime)
state: State | None = asyncio.run(
run_controller(
config=config,
@@ -165,9 +137,6 @@ def process_instance(
content=task_str
), # take output from initialize_runtime
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
@@ -183,19 +152,19 @@ def process_instance(
# Instruction is the first message from the USER
instruction = ''
for event in state.history:
for event in state.history.get_events():
if isinstance(event, MessageAction):
instruction = event.content
break
return_val = complete_runtime(runtime)
logger.info(f'Return value from complete_runtime: {return_val}')
reward = max(return_val['rewards'], default=0)
reward = max(return_val['rewards'])
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+2 -7
View File
@@ -13,7 +13,6 @@ from evaluation.mint.tasks import Task
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -29,7 +28,6 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import (
Action,
CmdRunAction,
MessageAction,
)
@@ -47,10 +45,7 @@ def codeact_user_response_mint(state: State, task: Task, task_config: dict[str,
task=task,
task_config=task_config,
)
last_action = next(
(event for event in reversed(state.history) if isinstance(event, Action)),
None,
)
last_action = state.history.get_last_action()
result_state: TaskState = env.step(last_action.message or '')
state.extra_data['task_state'] = result_state
@@ -207,7 +202,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+1 -2
View File
@@ -24,7 +24,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -257,7 +256,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+9 -9
View File
@@ -10,12 +10,10 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
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 (
@@ -78,13 +76,15 @@ def get_config(
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_id,
config.set_llm_config(metadata.llm_config)
if metadata.llm_config.log_completions:
metadata.llm_config.log_completions_folder = os.path.join(
metadata.eval_output_dir, 'llm_completions', instance_id
)
logger.info(
f'Logging LLM completions for instance {instance_id} to '
f'{metadata.llm_config.log_completions_folder}'
)
)
return config
@@ -233,7 +233,7 @@ If the program uses some packages that are incompatible, please figure out alter
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
-1
View File
@@ -83,7 +83,6 @@ def get_config(instance: pd.Series) -> AppConfig:
timeout=1800,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
remote_runtime_init_timeout=1800,
),
# do not mount workspace
workspace_base=None,
+11 -18
View File
@@ -20,7 +20,6 @@ from evaluation.utils.shared import (
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 (
@@ -41,7 +40,6 @@ from openhands.utils.async_utils import call_async_from_sync
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -81,7 +79,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
'</pr_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
'Your task is to make the minimal changes to non-tests files in the /repo directory to ensure the <pr_description> is satisfied.\n'
'Follow these steps to resolve the issue:\n'
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
@@ -90,13 +88,6 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
'5. Think about edgecases and make sure your fix handles them as well\n'
"Your thinking should be thorough and so it's fine if it's very long.\n"
)
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\n'
'You SHOULD NEVER attempt to browse the web. '
'</IMPORTANT!>\n'
)
return instruction
@@ -146,20 +137,23 @@ def get_config(
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_remote_runtime_alive=False,
remote_runtime_init_timeout=1800,
),
# 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']
if metadata.llm_config.log_completions:
metadata.llm_config.log_completions_folder = os.path.join(
metadata.eval_output_dir, 'llm_completions', instance['instance_id']
)
)
logger.info(
f'Logging LLM completions for instance {instance["instance_id"]} to '
f'{metadata.llm_config.log_completions_folder}'
)
config.set_llm_config(metadata.llm_config)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_browsing_delegate=False,
codeact_enable_llm_editor=False,
)
config.set_agent_config(agent_config)
@@ -444,8 +438,7 @@ def process_instance(
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]
histories = [event_to_dict(event) for event in state.history.get_events()]
metrics = state.metrics.get() if state.metrics else None
# Save the output
File diff suppressed because it is too large Load Diff
-11
View File
@@ -34,11 +34,6 @@ if [ -z "$USE_INSTANCE_IMAGE" ]; then
USE_INSTANCE_IMAGE=true
fi
if [ -z "$RUN_WITH_BROWSING" ]; then
echo "RUN_WITH_BROWSING not specified, use default false"
RUN_WITH_BROWSING=false
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
@@ -52,8 +47,6 @@ fi
export USE_INSTANCE_IMAGE=$USE_INSTANCE_IMAGE
echo "USE_INSTANCE_IMAGE: $USE_INSTANCE_IMAGE"
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
get_agent_version
@@ -74,10 +67,6 @@ 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
+2 -4
View File
@@ -9,7 +9,6 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -127,8 +126,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
raise ValueError('State should not be None.')
# retrieve the last message from the agent
last_agent_message = state.get_last_agent_message()
model_answer_raw = last_agent_message.content if last_agent_message else ''
model_answer_raw = state.history.get_last_agent_message()
# attempt to parse model_answer
correct = eval_answer(str(model_answer_raw), str(answer))
@@ -139,7 +137,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+2 -44
View File
@@ -18,9 +18,6 @@ from openhands.core.logger import get_console_handler
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import Action
from openhands.events.action.message import MessageAction
from openhands.events.event import Event
from openhands.events.serialization.event import event_to_dict
from openhands.events.utils import get_pairs_from_events
class EvalMetadata(BaseModel):
@@ -115,14 +112,7 @@ def codeact_user_response(
if state.history:
# check if the last action has an answer, if so, early exit
if try_parse is not None:
last_action = next(
(
event
for event in reversed(state.history)
if isinstance(event, Action)
),
None,
)
last_action = state.history.get_last_action()
ans = try_parse(last_action)
if ans is not None:
return '/exit'
@@ -130,7 +120,7 @@ def codeact_user_response(
# check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
user_msgs = [
event
for event in state.history
for event in state.history.get_events()
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:
@@ -421,35 +411,3 @@ def reset_logger_for_multiprocessing(
)
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
def update_llm_config_for_completions_logging(
llm_config: LLMConfig,
eval_output_dir: str,
instance_id: str,
) -> LLMConfig:
"""Update the LLM config for logging completions."""
if llm_config.log_completions:
llm_config.log_completions_folder = os.path.join(
eval_output_dir, 'llm_completions', instance_id
)
logger.info(
f'Logging LLM completions for instance {instance_id} to '
f'{llm_config.log_completions_folder}'
)
return llm_config
# history is now available as a filtered stream of events, rather than list of pairs of (Action, Observation)
# we rebuild the pairs here
# for compatibility with the existing output format in evaluations
# remove this when it's no longer necessary
def compatibility_for_eval_history_pairs(
history: list[Event],
) -> list[tuple[dict, dict]]:
history_pairs = []
for action, observation in get_pairs_from_events(history):
history_pairs.append((event_to_dict(action), event_to_dict(observation)))
return history_pairs
+2 -3
View File
@@ -10,7 +10,6 @@ import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -167,7 +166,7 @@ def process_instance(
# Instruction is the first message from the USER
instruction = ''
for event in state.history:
for event in state.history.get_events():
if isinstance(event, MessageAction):
instruction = event.content
break
@@ -179,7 +178,7 @@ def process_instance(
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
histories = state.history.compatibility_for_eval_history_pairs()
# Save the output
output = EvalOutput(
+1 -1
View File
@@ -84,4 +84,4 @@
}
}
]
}
}
+1 -6
View File
@@ -1,9 +1,4 @@
# i18n translation files make by script using `make build`
public/locales/**/*
src/i18n/declaration.ts
.env
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
.env
@@ -1,5 +1,5 @@
import userEvent from "@testing-library/user-event";
import { fireEvent, render, screen } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { describe, afterEach, vi, it, expect } from "vitest";
import { ChatInput } from "#/components/chat-input";
@@ -158,46 +158,4 @@ describe("ChatInput", () => {
await user.tab();
expect(onBlurMock).toHaveBeenCalledOnce();
});
it("should handle text paste correctly", () => {
const onSubmit = vi.fn();
const onChange = vi.fn();
render(<ChatInput onSubmit={onSubmit} onChange={onChange} />);
const input = screen.getByTestId("chat-input").querySelector("textarea");
expect(input).toBeTruthy();
// Fire paste event with text data
fireEvent.paste(input!, {
clipboardData: {
getData: (type: string) => type === 'text/plain' ? 'test paste' : '',
files: []
}
});
});
it("should handle image paste correctly", () => {
const onSubmit = vi.fn();
const onImagePaste = vi.fn();
render(<ChatInput onSubmit={onSubmit} onImagePaste={onImagePaste} />);
const input = screen.getByTestId("chat-input").querySelector("textarea");
expect(input).toBeTruthy();
// Create a paste event with an image file
const file = new File(["dummy content"], "image.png", { type: "image/png" });
// Fire paste event with image data
fireEvent.paste(input!, {
clipboardData: {
getData: () => '',
files: [file]
}
});
// Verify image paste was handled
expect(onImagePaste).toHaveBeenCalledWith([file]);
});
});
@@ -1,157 +1,20 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { act, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { ChatInterface } from "#/components/chat-interface";
import { addUserMessage } from "#/state/chatSlice";
import { SUGGESTIONS } from "#/utils/suggestions";
import * as ChatSlice from "#/state/chatSlice";
import { SocketProvider } from "#/context/socket";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renderChatInterface = (messages: (Message | ErrorMessage)[]) =>
renderWithProviders(<ChatInterface />);
describe("Empty state", () => {
const { send: sendMock } = vi.hoisted(() => ({
send: vi.fn(),
}));
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
useWsClient: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
}));
beforeAll(() => {
vi.mock("#/context/socket", async (importActual) => ({
...(await importActual<typeof import("#/context/ws-client-provider")>()),
useWsClient: useWsClientMock,
}));
});
afterEach(() => {
vi.clearAllMocks();
});
it("should render suggestions if empty", () => {
const { store } = renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
expect(screen.getByTestId("suggestions")).toBeInTheDocument();
act(() => {
store.dispatch(
addUserMessage({
content: "Hello",
imageUrls: [],
timestamp: new Date().toISOString(),
}),
);
});
expect(screen.queryByTestId("suggestions")).not.toBeInTheDocument();
});
it("should render the default suggestions", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
const suggestions = screen.getByTestId("suggestions");
const repoSuggestions = Object.keys(SUGGESTIONS.repo);
// check that there are at most 4 suggestions displayed
const displayedSuggestions = within(suggestions).getAllByRole("button");
expect(displayedSuggestions.length).toBeLessThanOrEqual(4);
// Check that each displayed suggestion is one of the repo suggestions
displayedSuggestions.forEach((suggestion) => {
expect(repoSuggestions).toContain(suggestion.textContent);
});
});
it.fails(
"should load the a user message to the input when selecting",
async () => {
// this is to test that the message is in the UI before the socket is called
useWsClientMock.mockImplementation(() => ({
send: sendMock,
runtimeActive: false, // mock an inactive runtime setup
}));
const addUserMessageSpy = vi.spyOn(ChatSlice, "addUserMessage");
const user = userEvent.setup();
const { store } = renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
const suggestions = screen.getByTestId("suggestions");
const displayedSuggestions = within(suggestions).getAllByRole("button");
const input = screen.getByTestId("chat-input");
await user.click(displayedSuggestions[0]);
// user message loaded to input
expect(addUserMessageSpy).not.toHaveBeenCalled();
expect(screen.queryByTestId("suggestions")).toBeInTheDocument();
expect(store.getState().chat.messages).toHaveLength(0);
expect(input).toHaveValue(displayedSuggestions[0].textContent);
},
);
it.fails(
"should send the message to the socket only if the runtime is active",
async () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
runtimeActive: false, // mock an inactive runtime setup
}));
const user = userEvent.setup();
const { rerender } = renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
const suggestions = screen.getByTestId("suggestions");
const displayedSuggestions = within(suggestions).getAllByRole("button");
await user.click(displayedSuggestions[0]);
expect(sendMock).not.toHaveBeenCalled();
useWsClientMock.mockImplementation(() => ({
send: sendMock,
runtimeActive: true, // mock an active runtime setup
}));
rerender(<ChatInterface />);
await waitFor(() =>
expect(sendMock).toHaveBeenCalledWith(expect.any(String)),
);
},
);
});
render(<ChatInterface />, { wrapper: SocketProvider });
describe.skip("ChatInterface", () => {
beforeAll(() => {
// mock useScrollToBottom hook
vi.mock("#/hooks/useScrollToBottom", () => ({
useScrollToBottom: vi.fn(() => ({
scrollDomToBottom: vi.fn(),
onChatBodyScroll: vi.fn(),
hitBottom: vi.fn(),
})),
}));
});
afterEach(() => {
vi.clearAllMocks();
});
it.todo("should render suggestions if empty");
it("should render messages", () => {
const messages: Message[] = [
{
@@ -265,14 +128,14 @@ describe.skip("ChatInterface", () => {
timestamp: new Date().toISOString(),
},
{
error: true,
id: "",
error: "Woops!",
message: "Something went wrong",
},
];
renderChatInterface(messages);
const error = screen.getByTestId("error-message");
expect(within(error).getByText("Woops!")).toBeInTheDocument();
expect(within(error).getByText("Something went wrong")).toBeInTheDocument();
});
@@ -25,21 +25,6 @@ describe("InteractiveChatBox", () => {
within(chatBox).getByTestId("upload-image-input");
});
it.fails("should set custom values", () => {
render(
<InteractiveChatBox
onSubmit={onSubmitMock}
onStop={onStopMock}
value="Hello, world!"
/>,
);
const chatBox = screen.getByTestId("interactive-chat-box");
const chatInput = within(chatBox).getByTestId("chat-input");
expect(chatInput).toHaveValue("Hello, world!");
});
it("should display the image previews when images are uploaded", async () => {
const user = userEvent.setup();
render(<InteractiveChatBox onSubmit={onSubmitMock} onStop={onStopMock} />);
@@ -1,30 +0,0 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SuggestionItem } from "#/components/suggestion-item";
describe("SuggestionItem", () => {
const suggestionItem = { label: "suggestion1", value: "a long text value" };
const onClick = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
it("should render a suggestion", () => {
render(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);
expect(screen.getByTestId("suggestion")).toBeInTheDocument();
expect(screen.getByText(/suggestion1/i)).toBeInTheDocument();
});
it("should call onClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);
const suggestion = screen.getByTestId("suggestion");
await user.click(suggestion);
expect(onClick).toHaveBeenCalledWith("a long text value");
});
});
@@ -1,60 +0,0 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Suggestions } from "#/components/suggestions";
describe("Suggestions", () => {
const firstSuggestion = {
label: "first-suggestion",
value: "value-of-first-suggestion",
};
const secondSuggestion = {
label: "second-suggestion",
value: "value-of-second-suggestion",
};
const suggestions = [firstSuggestion, secondSuggestion];
const onSuggestionClickMock = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
it("should render suggestions", () => {
render(
<Suggestions
suggestions={suggestions}
onSuggestionClick={onSuggestionClickMock}
/>,
);
expect(screen.getByTestId("suggestions")).toBeInTheDocument();
const suggestionElements = screen.getAllByTestId("suggestion");
expect(suggestionElements).toHaveLength(2);
expect(suggestionElements[0]).toHaveTextContent("first-suggestion");
expect(suggestionElements[1]).toHaveTextContent("second-suggestion");
});
it("should call onSuggestionClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render(
<Suggestions
suggestions={suggestions}
onSuggestionClick={onSuggestionClickMock}
/>,
);
const suggestionElements = screen.getAllByTestId("suggestion");
await user.click(suggestionElements[0]);
expect(onSuggestionClickMock).toHaveBeenCalledWith(
"value-of-first-suggestion",
);
await user.click(suggestionElements[1]);
expect(onSuggestionClickMock).toHaveBeenCalledWith(
"value-of-second-suggestion",
);
});
});
+4 -16
View File
@@ -2,9 +2,8 @@ import { beforeAll, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { afterEach } from "node:test";
import { useTerminal } from "#/hooks/useTerminal";
import { SocketProvider } from "#/context/socket";
import { Command } from "#/state/commandSlice";
import { WsClientProvider } from "#/context/ws-client-provider";
import { ReactNode } from "react";
interface TestTerminalComponentProps {
commands: Command[];
@@ -19,17 +18,6 @@ function TestTerminalComponent({
return <div ref={ref} />;
}
interface WrapperProps {
children: ReactNode;
}
function Wrapper({children}: WrapperProps) {
return (
<WsClientProvider enabled={true} token="NO_JWT" ghToken="NO_GITHUB" settings={null}>{children}</WsClientProvider>
)
}
describe("useTerminal", () => {
const mockTerminal = vi.hoisted(() => ({
loadAddon: vi.fn(),
@@ -62,7 +50,7 @@ describe("useTerminal", () => {
it("should render", () => {
render(<TestTerminalComponent commands={[]} secrets={[]} />, {
wrapper: Wrapper,
wrapper: SocketProvider,
});
});
@@ -73,7 +61,7 @@ describe("useTerminal", () => {
];
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
wrapper: Wrapper,
wrapper: SocketProvider,
});
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
@@ -97,7 +85,7 @@ describe("useTerminal", () => {
secrets={[secret, anotherSecret]}
/>,
{
wrapper: Wrapper,
wrapper: SocketProvider,
},
);
-17
View File
@@ -1,17 +0,0 @@
import { describe, it, expect } from "vitest";
import store from "../src/store";
import { setInitialQuery, clearInitialQuery } from "../src/state/initial-query-slice";
describe("Initial Query Behavior", () => {
it("should clear initial query when clearInitialQuery is dispatched", () => {
// Set up initial query in the store
store.dispatch(setInitialQuery("test query"));
expect(store.getState().initalQuery.initialQuery).toBe("test query");
// Clear the initial query
store.dispatch(clearInitialQuery());
// Verify initial query is cleared
expect(store.getState().initalQuery.initialQuery).toBeNull();
});
});
@@ -1,5 +0,0 @@
import { describe, it } from "vitest";
describe("App", () => {
it.todo("should render");
});
-53
View File
@@ -1,53 +0,0 @@
import { afterEach } from "node:test";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cache } from "#/utils/cache";
describe("Cache", () => {
const testKey = "key";
const testData = { message: "Hello, world!" };
const testTTL = 1000; // 1 second
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("gets data from memory if not expired", () => {
cache.set(testKey, testData, testTTL);
expect(cache.get(testKey)).toEqual(testData);
});
it("should expire after 5 minutes by default", () => {
cache.set(testKey, testData);
expect(cache.get(testKey)).not.toBeNull();
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
expect(cache.get(testKey)).toBeNull();
});
it("returns null if cached data is expired", () => {
cache.set(testKey, testData, testTTL);
vi.advanceTimersByTime(testTTL + 1);
expect(cache.get(testKey)).toBeNull();
});
it("deletes data from memory", () => {
cache.set(testKey, testData, testTTL);
cache.delete(testKey);
expect(cache.get(testKey)).toBeNull();
});
it("clears all data with the app prefix from memory", () => {
cache.set(testKey, testData, testTTL);
cache.set("anotherKey", { data: "More data" }, testTTL);
cache.clearAll();
expect(cache.get(testKey)).toBeNull();
expect(cache.get("anotherKey")).toBeNull();
});
});
@@ -59,9 +59,9 @@ describe("extractModelAndProvider", () => {
separator: "/",
});
expect(extractModelAndProvider("claude-3-5-sonnet-20240620")).toEqual({
expect(extractModelAndProvider("claude-3-5-sonnet-20241022")).toEqual({
provider: "anthropic",
model: "claude-3-5-sonnet-20240620",
model: "claude-3-5-sonnet-20241022",
separator: "/",
});
@@ -78,3 +78,4 @@ describe("extractModelAndProvider", () => {
});
});
});
@@ -15,7 +15,7 @@ test("organizeModelsAndProviders", () => {
"gpt-4o",
"together-ai-21.1b-41b",
"gpt-4o-mini",
"anthropic/claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-20241022",
"claude-3-haiku-20240307",
"claude-2",
"claude-2.1",
@@ -63,3 +63,4 @@ test("organizeModelsAndProviders", () => {
},
});
});
+2 -122
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.13.0",
"version": "0.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.13.0",
"version": "0.12.0",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8",
@@ -26,7 +26,6 @@
"isbot": "^5.1.17",
"jose": "^5.9.4",
"monaco-editor": "^0.52.0",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
@@ -46,7 +45,6 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
@@ -63,7 +61,6 @@
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
@@ -3381,21 +3378,6 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
"dev": true,
"dependencies": {
"playwright": "1.48.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
@@ -7882,16 +7864,6 @@
"node": ">=6.6.0"
}
},
"node_modules/core-js": {
"version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz",
"integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -7924,24 +7896,6 @@
}
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@@ -9712,11 +9666,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -19457,50 +19406,6 @@
"pathe": "^1.1.2"
}
},
"node_modules/playwright": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.48.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -19748,31 +19653,6 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/posthog-js": {
"version": "1.176.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.176.0.tgz",
"integrity": "sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.0"
}
},
"node_modules/posthog-js/node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
},
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+3 -7
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.13.0",
"version": "0.12.0",
"private": true,
"type": "module",
"engines": {
@@ -25,7 +25,6 @@
"isbot": "^5.1.17",
"jose": "^5.9.4",
"monaco-editor": "^0.52.0",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
@@ -45,12 +44,11 @@
"ws": "^8.18.0"
},
"scripts": {
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false remix vite:dev",
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true remix vite:dev",
"dev": "npm run make-i18n && VITE_MOCK_API=false remix vite:dev",
"dev:mock": "npm run make-i18n && VITE_MOCK_API=true remix vite:dev",
"build": "npm run make-i18n && tsc && remix vite:build",
"start": "npx sirv-cli build/ --single",
"test": "vitest run",
"test:e2e": "playwright test",
"test:coverage": "npm run make-i18n && vitest run --coverage",
"dev_wsl": "VITE_WATCH_USE_POLLING=true vite",
"preview": "vite preview",
@@ -72,7 +70,6 @@
]
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
@@ -89,7 +86,6 @@
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
-79
View File
@@ -1,79 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev:mock -- --port 3000",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
},
});
+27 -3
View File
@@ -122,9 +122,6 @@ export const retrieveGitHubUser = async (
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
@@ -139,6 +136,33 @@ export const retrieveGitHubUser = async (
return error;
};
/**
* Given a GitHub token and a repository name, creates a repository for the authenticated user
* @param token The GitHub token
* @param repositoryName Name of the repository to create
* @param description Description of the repository
* @param isPrivate Boolean indicating if the repository should be private
* @returns The created repository or an error response
*/
export const createGitHubRepository = async (
token: string,
repositoryName: string,
description?: string,
isPrivate = true,
): Promise<GitHubRepository | GitHubErrorReponse> => {
const response = await fetch("https://api.github.com/user/repos", {
method: "POST",
headers: generateGitHubAPIHeaders(token),
body: JSON.stringify({
name: repositoryName,
description,
private: isPrivate,
}),
});
return response.json();
};
export const retrieveLatestGitHubCommit = async (
token: string,
repository: string,
+4 -29
View File
@@ -1,5 +1,4 @@
import { request } from "#/services/api";
import { cache } from "#/utils/cache";
import {
SaveFileSuccessResponse,
FileUploadSuccessResponse,
@@ -16,13 +15,7 @@ class OpenHands {
* @returns List of models available
*/
static async getModels(): Promise<string[]> {
const cachedData = cache.get<string[]>("models");
if (cachedData) return cachedData;
const data = await request("/api/options/models");
cache.set("models", data);
return data;
return request("/api/options/models");
}
/**
@@ -30,13 +23,7 @@ class OpenHands {
* @returns List of agents available
*/
static async getAgents(): Promise<string[]> {
const cachedData = cache.get<string[]>("agents");
if (cachedData) return cachedData;
const data = await request(`/api/options/agents`);
cache.set("agents", data);
return data;
return request(`/api/options/agents`);
}
/**
@@ -44,23 +31,11 @@ class OpenHands {
* @returns List of security analyzers available
*/
static async getSecurityAnalyzers(): Promise<string[]> {
const cachedData = cache.get<string[]>("agents");
if (cachedData) return cachedData;
const data = await request(`/api/options/security-analyzers`);
cache.set("security-analyzers", data);
return data;
return request(`/api/options/security-analyzers`);
}
static async getConfig(): Promise<GetConfigResponse> {
const cachedData = cache.get<GetConfigResponse>("config");
if (cachedData) return cachedData;
const data = await request("/config.json");
cache.set("config", data);
return data;
return request("/config.json");
}
/**
@@ -2,4 +2,4 @@
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11.5304 6.46978L10.4697 7.53044L6.75006 3.81077L6.75006 17.0001H5.25006L5.25006 3.81077L1.53039 7.53044L0.469727 6.46978L6.00006 0.939453L11.5304 6.46978Z"
fill="white" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 334 B

@@ -32,4 +32,4 @@
<rect width="69" height="46" fill="white" transform="translate(0.5)" />
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -2,4 +2,4 @@
<path
d="M15.359 21V17.319C15.3974 16.8654 15.3314 16.4095 15.1651 15.9814C14.9989 15.5534 14.7363 15.1631 14.3949 14.8364C17.6154 14.5035 21 13.3716 21 8.17826C20.9997 6.85027 20.4489 5.57321 19.4615 4.61139C19.9291 3.44954 19.896 2.16532 19.3692 1.02548C19.3692 1.02548 18.159 0.692576 15.359 2.43321C13.0082 1.84237 10.5302 1.84237 8.17949 2.43321C5.37949 0.692576 4.16923 1.02548 4.16923 1.02548C3.64244 2.16532 3.60938 3.44954 4.07692 4.61139C3.08218 5.58034 2.53079 6.86895 2.53846 8.2068C2.53846 13.3621 5.92308 14.494 9.14359 14.865C8.80615 15.1883 8.54591 15.574 8.3798 15.9968C8.2137 16.4196 8.14544 16.8701 8.17949 17.319V21M8.17949 18.1465C3.05128 19.5732 3.05128 15.7686 1 15.293L8.17949 18.1465Z"
stroke="white" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 888 B

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

@@ -2,4 +2,4 @@
<path fill-rule="evenodd" clip-rule="evenodd"
d="M10.5 3.75C9.25736 3.75 8.25 4.75736 8.25 6V16.5C8.25 18.5711 9.92893 20.25 12 20.25C14.0711 20.25 15.75 18.5711 15.75 16.5V7H17.25V16.5C17.25 19.3995 14.8995 21.75 12 21.75C9.1005 21.75 6.75 19.3995 6.75 16.5V6C6.75 3.92893 8.42893 2.25 10.5 2.25C12.5711 2.25 14.25 3.92893 14.25 6V16C14.25 17.2426 13.2426 18.25 12 18.25C10.7574 18.25 9.75 17.2426 9.75 16V7H11.25V16C11.25 16.4142 11.5858 16.75 12 16.75C12.4142 16.75 12.75 16.4142 12.75 16V6C12.75 4.75736 11.7426 3.75 10.5 3.75Z"
fill="white" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 661 B

@@ -32,4 +32,4 @@
<rect width="54" height="75" fill="white" />
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

@@ -2,4 +2,4 @@
<path
d="M13.0919 10.5917C13.9089 9.94891 14.5052 9.06746 14.7979 8.06997C15.0906 7.07249 15.0652 6.00858 14.7251 5.02625C14.385 4.04391 13.7471 3.19202 12.9003 2.58907C12.0535 1.98612 11.0398 1.66211 10.0002 1.66211C8.9607 1.66211 7.947 1.98612 7.10018 2.58907C6.25336 3.19202 5.61553 4.04391 5.27542 5.02625C4.93531 6.00858 4.90984 7.07249 5.20254 8.06997C5.49525 9.06746 6.09158 9.94891 6.90858 10.5917C5.50864 11.1526 4.28715 12.0828 3.37432 13.2833C2.46149 14.4838 1.89154 15.9094 1.72524 17.4084C1.7132 17.5178 1.72284 17.6285 1.7536 17.7342C1.78435 17.8399 1.83563 17.9386 1.9045 18.0245C2.04359 18.1979 2.24589 18.309 2.46691 18.3334C2.68792 18.3577 2.90954 18.2932 3.08301 18.1541C3.25648 18.015 3.3676 17.8127 3.39191 17.5917C3.5749 15.9627 4.35165 14.4582 5.57376 13.3657C6.79587 12.2732 8.37766 11.6692 10.0169 11.6692C11.6562 11.6692 13.2379 12.2732 14.4601 13.3657C15.6822 14.4582 16.4589 15.9627 16.6419 17.5917C16.6646 17.7965 16.7623 17.9856 16.9162 18.1225C17.0701 18.2595 17.2692 18.3346 17.4752 18.3334H17.5669C17.7854 18.3082 17.985 18.1978 18.1224 18.0261C18.2597 17.8544 18.3237 17.6353 18.3002 17.4167C18.1332 15.9135 17.5601 14.4842 16.6426 13.2819C15.7251 12.0795 14.4977 11.1496 13.0919 10.5917ZM10.0002 10C9.34097 10 8.69651 9.80453 8.14834 9.43825C7.60018 9.07198 7.17294 8.55139 6.92064 7.9423C6.66835 7.33321 6.60234 6.66299 6.73096 6.01639C6.85957 5.36979 7.17704 4.77584 7.64322 4.30967C8.10939 3.84349 8.70334 3.52602 9.34994 3.39741C9.99654 3.26879 10.6668 3.3348 11.2759 3.58709C11.8849 3.83938 12.4055 4.26662 12.7718 4.81479C13.1381 5.36295 13.3336 6.00742 13.3336 6.66669C13.3336 7.55074 12.9824 8.39859 12.3573 9.02371C11.7321 9.64883 10.8843 10 10.0002 10Z"
fill="#262626" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -24,4 +24,4 @@
<rect width="23.33" height="18.67" fill="white" transform="translate(2.33496 4.66504)" />
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 319 B

@@ -4,4 +4,4 @@
<path
d="M4.33333 3.66667C3.59695 3.66667 3 4.26362 3 5V11.6667C3 12.403 3.59695 13 4.33333 13H11C11.7364 13 12.3333 12.403 12.3333 11.6667V9.66667H11.3333V11.6667C11.3333 11.8508 11.1841 12 11 12H4.33333C4.14924 12 4 11.8508 4 11.6667V5C4 4.81591 4.14924 4.66667 4.33333 4.66667H6.33333V3.66667H4.33333Z"
fill="#EEEEEE" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 551 B

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -2,4 +2,4 @@
<path
d="M11.3931 1.88021C10.78 1.37593 10.062 1.01489 9.29157 0.823438C8.52113 0.631981 7.71767 0.614934 6.9398 0.773542C5.90405 0.982756 4.95379 1.49492 4.20959 2.24506C3.46538 2.9952 2.96078 3.9495 2.7598 4.98688C2.61303 5.76469 2.6397 6.56531 2.83791 7.33163C3.03611 8.09795 3.40097 8.8111 3.90647 9.42021C4.37559 9.94959 4.64449 10.6266 4.66647 11.3335V13.3335C4.66647 13.864 4.87718 14.3727 5.25225 14.7478C5.62732 15.1228 6.13603 15.3335 6.66647 15.3335H9.33313C9.86356 15.3335 10.3723 15.1228 10.7473 14.7478C11.1224 14.3727 11.3331 13.864 11.3331 13.3335V11.4602C11.3555 10.6797 11.6423 9.92983 12.1465 9.33354C13.0299 8.24073 13.4463 6.84342 13.3052 5.44529C13.1642 4.04716 12.477 2.7612 11.3931 1.86687V1.88021ZM9.9998 13.3335C9.9998 13.5104 9.92956 13.6799 9.80454 13.8049C9.67951 13.93 9.50994 14.0002 9.33313 14.0002H6.66647C6.48965 14.0002 6.32009 13.93 6.19506 13.8049C6.07004 13.6799 5.9998 13.5104 5.9998 13.3335V12.6669H9.9998V13.3335ZM11.1131 8.50688C10.4428 9.30194 10.0517 10.2949 9.9998 11.3335H8.66647V9.33354C8.66647 9.15673 8.59623 8.98716 8.4712 8.86214C8.34618 8.73711 8.17661 8.66688 7.9998 8.66688C7.82299 8.66688 7.65342 8.73711 7.52839 8.86214C7.40337 8.98716 7.33313 9.15673 7.33313 9.33354V11.3335H5.9998C5.98221 10.3123 5.60443 9.33005 4.93313 8.56021C4.49023 8.02954 4.19239 7.39316 4.06867 6.71311C3.94495 6.03306 3.99957 5.33255 4.22719 4.6799C4.45481 4.02724 4.84768 3.4447 5.36748 2.98909C5.88728 2.53348 6.51627 2.22034 7.19313 2.08021C7.77483 1.96044 8.37591 1.9717 8.95271 2.11319C9.52952 2.25467 10.0676 2.52282 10.5278 2.89818C10.9881 3.27353 11.359 3.74666 11.6136 4.28322C11.8682 4.81978 12.0001 5.4063 11.9998 6.00021C12.0047 6.91345 11.6912 7.79985 11.1131 8.50688Z"
fill="white" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 924 B

After

Width:  |  Height:  |  Size: 924 B

@@ -1,4 +1,4 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63 33C63 16.4315 49.5685 3 33 3C16.4315 3 3 16.4315 3 33C3 49.5685 16.4315 63 33 63"
stroke="#007AFF" stroke-width="6" stroke-linecap="round" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 263 B

@@ -2,4 +2,4 @@
<path
d="M19.8335 8.16634H8.16683C7.85741 8.16634 7.56066 8.28926 7.34187 8.50805C7.12308 8.72684 7.00016 9.02359 7.00016 9.33301C7.00016 9.64243 7.12308 9.93917 7.34187 10.158C7.56066 10.3768 7.85741 10.4997 8.16683 10.4997H19.8335C20.1429 10.4997 20.4397 10.3768 20.6585 10.158C20.8772 9.93917 21.0002 9.64243 21.0002 9.33301C21.0002 9.02359 20.8772 8.72684 20.6585 8.50805C20.4397 8.28926 20.1429 8.16634 19.8335 8.16634ZM19.8335 12.833H8.16683C7.85741 12.833 7.56066 12.9559 7.34187 13.1747C7.12308 13.3935 7.00016 13.6903 7.00016 13.9997C7.00016 14.3091 7.12308 14.6058 7.34187 14.8246C7.56066 15.0434 7.85741 15.1663 8.16683 15.1663H19.8335C20.1429 15.1663 20.4397 15.0434 20.6585 14.8246C20.8772 14.6058 21.0002 14.3091 21.0002 13.9997C21.0002 13.6903 20.8772 13.3935 20.6585 13.1747C20.4397 12.9559 20.1429 12.833 19.8335 12.833ZM22.1668 2.33301H5.8335C4.90524 2.33301 4.015 2.70176 3.35862 3.35813C2.70225 4.01451 2.3335 4.90475 2.3335 5.83301V17.4997C2.3335 18.4279 2.70225 19.3182 3.35862 19.9745C4.015 20.6309 4.90524 20.9997 5.8335 20.9997H19.3552L23.6718 25.328C23.7808 25.4361 23.9101 25.5217 24.0523 25.5797C24.1944 25.6378 24.3466 25.6672 24.5002 25.6663C24.6532 25.6703 24.805 25.6383 24.9435 25.573C25.1565 25.4855 25.3389 25.3369 25.4677 25.1458C25.5964 24.9548 25.6657 24.73 25.6668 24.4997V5.83301C25.6668 4.90475 25.2981 4.01451 24.6417 3.35813C23.9853 2.70176 23.0951 2.33301 22.1668 2.33301ZM23.3335 21.688L20.6618 19.0047C20.5528 18.8965 20.4235 18.811 20.2814 18.7529C20.1392 18.6949 19.987 18.6655 19.8335 18.6663H5.8335C5.52408 18.6663 5.22733 18.5434 5.00854 18.3246C4.78975 18.1058 4.66683 17.8091 4.66683 17.4997V5.83301C4.66683 5.52359 4.78975 5.22684 5.00854 5.00805C5.22733 4.78926 5.52408 4.66634 5.8335 4.66634H22.1668C22.4762 4.66634 22.773 4.78926 22.9918 5.00805C23.2106 5.22684 23.3335 5.52359 23.3335 5.83301V21.688Z"
fill="white" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -3,4 +3,4 @@
<path
d="M20.4165 13.0837H14.9165V7.58366C14.9165 7.34054 14.8199 7.10739 14.648 6.93548C14.4761 6.76357 14.243 6.66699 13.9998 6.66699C13.7567 6.66699 13.5236 6.76357 13.3517 6.93548C13.1797 7.10739 13.0832 7.34054 13.0832 7.58366V13.0837H7.58317C7.34006 13.0837 7.1069 13.1802 6.93499 13.3521C6.76308 13.5241 6.6665 13.7572 6.6665 14.0003C6.6665 14.2434 6.76308 14.4766 6.93499 14.6485C7.1069 14.8204 7.34006 14.917 7.58317 14.917H13.0832V20.417C13.0832 20.6601 13.1797 20.8933 13.3517 21.0652C13.5236 21.2371 13.7567 21.3337 13.9998 21.3337C14.243 21.3337 14.4761 21.2371 14.648 21.0652C14.8199 20.8933 14.9165 20.6601 14.9165 20.417V14.917H20.4165C20.6596 14.917 20.8928 14.8204 21.0647 14.6485C21.2366 14.4766 21.3332 14.2434 21.3332 14.0003C21.3332 13.7572 21.2366 13.5241 21.0647 13.3521C20.8928 13.1802 20.6596 13.0837 20.4165 13.0837Z"
fill="black" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 649 B

After

Width:  |  Height:  |  Size: 649 B

@@ -2,4 +2,4 @@
<path
d="M4.26446 0.763647C4.39463 0.633472 4.60569 0.633472 4.73586 0.763647L5.73586 1.76365C5.86604 1.89382 5.86604 2.10488 5.73586 2.23505L4.73586 3.23505C4.60569 3.36523 4.39463 3.36523 4.26446 3.23505C4.13429 3.10488 4.13429 2.89382 4.26446 2.76365L4.69542 2.33268H4.16683C2.98426 2.33268 2.00016 3.31678 2.00016 4.49935C2.00016 5.68192 2.98426 6.66602 4.16683 6.66602C5.3494 6.66602 6.3335 5.68192 6.3335 4.49935C6.3335 4.31525 6.48273 4.16602 6.66683 4.16602C6.85092 4.16602 7.00016 4.31525 7.00016 4.49935C7.00016 6.05011 5.71759 7.33268 4.16683 7.33268C2.61607 7.33268 1.3335 6.05011 1.3335 4.49935C1.3335 2.94859 2.61607 1.66602 4.16683 1.66602H4.69542L4.26446 1.23505C4.13429 1.10488 4.13429 0.893821 4.26446 0.763647Z"
fill="white" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 855 B

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 811 B

+2 -2
View File
@@ -6,7 +6,7 @@ import PlayIcon from "#/assets/play";
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
import { RootState } from "#/store";
import AgentState from "#/types/AgentState";
import { useWsClient } from "#/context/ws-client-provider";
import { useSocket } from "#/context/socket";
const IgnoreTaskStateMap: Record<string, AgentState[]> = {
[AgentState.PAUSED]: [
@@ -72,7 +72,7 @@ function ActionButton({
}
function AgentControlBar() {
const { send } = useWsClient();
const { send } = useSocket();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const handleAction = (action: AgentState) => {
+7 -20
View File
@@ -1,7 +1,6 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import toast from "react-hot-toast";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import AgentState from "#/types/AgentState";
@@ -17,7 +16,7 @@ enum IndicatorColor {
}
function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
@@ -95,27 +94,15 @@ function AgentStatusBar() {
const [statusMessage, setStatusMessage] = React.useState<string>("");
React.useEffect(() => {
let message = curStatusMessage.message || "";
if (curStatusMessage?.id) {
const id = curStatusMessage.id.trim();
if (i18n.exists(id)) {
message = t(curStatusMessage.id.trim()) || message;
if (curAgentState === AgentState.LOADING) {
const trimmedCustomMessage = curStatusMessage.status.trim();
if (trimmedCustomMessage) {
setStatusMessage(t(trimmedCustomMessage));
return;
}
}
if (curStatusMessage?.type === "error") {
toast.error(message);
return;
}
if (curAgentState === AgentState.LOADING && message.trim()) {
setStatusMessage(message);
} else {
setStatusMessage(AgentStatusMap[curAgentState].message);
}
}, [curStatusMessage.id]);
React.useEffect(() => {
setStatusMessage(AgentStatusMap[curAgentState].message);
}, [curAgentState]);
}, [curAgentState, curStatusMessage.status]);
return (
<div className="flex flex-col items-center">
+3 -11
View File
@@ -25,11 +25,7 @@ function JupyterCell({ cell }: IJupyterCell): JSX.Element {
className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5"
style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }}
>
<SyntaxHighlighter
language="python"
style={atomOneDark}
wrapLongLines
>
<SyntaxHighlighter language="python" style={atomOneDark}>
{code}
</SyntaxHighlighter>
</pre>
@@ -82,11 +78,7 @@ function JupyterCell({ cell }: IJupyterCell): JSX.Element {
);
}
interface JupyterEditorProps {
maxWidth: number;
}
function JupyterEditor({ maxWidth }: JupyterEditorProps) {
function JupyterEditor(): JSX.Element {
const { t } = useTranslation();
const { cells } = useSelector((state: RootState) => state.jupyter);
@@ -96,7 +88,7 @@ function JupyterEditor({ maxWidth }: JupyterEditorProps) {
useScrollToBottom(jupyterRef);
return (
<div className="flex-1" style={{ maxWidth }}>
<div className="flex-1">
<div
className="overflow-y-auto h-full"
ref={jupyterRef}
@@ -1,42 +0,0 @@
import { useFetcher } from "@remix-run/react";
import { ModalBackdrop } from "./modals/modal-backdrop";
import ModalBody from "./modals/ModalBody";
import ModalButton from "./buttons/ModalButton";
import {
BaseModalTitle,
BaseModalDescription,
} from "./modals/confirmation-modals/BaseModal";
export function AnalyticsConsentFormModal() {
const fetcher = useFetcher({ key: "set-consent" });
return (
<ModalBackdrop>
<fetcher.Form
method="POST"
action="/set-consent"
className="flex flex-col gap-2"
>
<ModalBody>
<BaseModalTitle title="Your Privacy Preferences" />
<BaseModalDescription>
We use tools to understand how our application is used to improve
your experience. You can enable or disable analytics. Your
preferences will be stored and can be updated anytime.
</BaseModalDescription>
<label className="flex gap-2 items-center self-start">
<input name="analytics" type="checkbox" defaultChecked />
Send anonymous usage data
</label>
<ModalButton
type="submit"
text="Confirm Preferences"
className="bg-primary text-white w-full hover:opacity-80"
/>
</ModalBody>
</fetcher.Form>
</ModalBackdrop>
);
}
@@ -1,4 +1,4 @@
import Clip from "#/icons/clip.svg?react";
import Clip from "#/assets/clip.svg?react";
export function AttachImageLabel() {
return (
@@ -2,7 +2,6 @@ import clsx from "clsx";
import React from "react";
interface ModalButtonProps {
testId?: string;
variant?: "default" | "text-like";
onClick?: () => void;
text: string;
@@ -14,7 +13,6 @@ interface ModalButtonProps {
}
function ModalButton({
testId,
variant = "default",
onClick,
text,
@@ -26,7 +24,6 @@ function ModalButton({
}: ModalButtonProps) {
return (
<button
data-testid={testId}
type={type === "submit" ? "submit" : "button"}
disabled={disabled}
onClick={onClick}
+3 -54
View File
@@ -1,6 +1,6 @@
import React from "react";
import TextareaAutosize from "react-textarea-autosize";
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
import ArrowSendIcon from "#/assets/arrow-send.svg?react";
import { cn } from "#/utils/utils";
interface ChatInputProps {
@@ -16,7 +16,6 @@ interface ChatInputProps {
onChange?: (message: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onImagePaste?: (files: File[]) => void;
className?: React.HTMLAttributes<HTMLDivElement>["className"];
}
@@ -33,51 +32,9 @@ export function ChatInput({
onChange,
onFocus,
onBlur,
onImagePaste,
className,
}: ChatInputProps) {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
// Only handle paste if we have an image paste handler and there are files
if (onImagePaste && event.clipboardData.files.length > 0) {
const files = Array.from(event.clipboardData.files).filter((file) =>
file.type.startsWith("image/"),
);
// Only prevent default if we found image files to handle
if (files.length > 0) {
event.preventDefault();
onImagePaste(files);
}
}
// For text paste, let the default behavior handle it
};
const handleDragOver = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
if (event.dataTransfer.types.includes("Files")) {
setIsDraggingOver(true);
}
};
const handleDragLeave = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
setIsDraggingOver(false);
};
const handleDrop = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
setIsDraggingOver(false);
if (onImagePaste && event.dataTransfer.files.length > 0) {
const files = Array.from(event.dataTransfer.files).filter((file) =>
file.type.startsWith("image/"),
);
if (files.length > 0) {
onImagePaste(files);
}
}
};
const handleSubmitMessage = () => {
if (textareaRef.current?.value) {
@@ -110,20 +67,12 @@ export function ChatInput({
onChange={handleChange}
onFocus={onFocus}
onBlur={onBlur}
onPaste={handlePaste}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
value={value}
minRows={1}
maxRows={maxRows}
data-dragging-over={isDraggingOver}
className={cn(
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-none ring-0",
"transition-all duration-200 ease-in-out",
isDraggingOver
? "bg-neutral-600/50 rounded-lg px-2"
: "bg-transparent",
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none bg-transparent outline-none ring-0",
"transition-[height] duration-200 ease-in-out",
className,
)}
/>
+3 -37
View File
@@ -1,6 +1,6 @@
import { useDispatch, useSelector } from "react-redux";
import React from "react";
import posthog from "posthog-js";
import { useSocket } from "#/context/socket";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { ChatMessage } from "./chat-message";
import { FeedbackActions } from "./feedback-actions";
@@ -18,17 +18,13 @@ import ConfirmationButtons from "./chat/ConfirmationButtons";
import { ErrorMessage } from "./error-message";
import { ContinueButton } from "./continue-button";
import { ScrollToBottomButton } from "./scroll-to-bottom-button";
import { Suggestions } from "./suggestions";
import { SUGGESTIONS } from "#/utils/suggestions";
import BuildIt from "#/icons/build-it.svg?react";
import { useWsClient } from "#/context/ws-client-provider";
const isErrorMessage = (
message: Message | ErrorMessage,
): message is ErrorMessage => "error" in message;
export function ChatInterface() {
const { send } = useWsClient();
const { send } = useSocket();
const dispatch = useDispatch();
const scrollRef = React.useRef<HTMLDivElement>(null);
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
@@ -41,23 +37,17 @@ export function ChatInterface() {
"positive" | "negative"
>("positive");
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
const [messageToSend, setMessageToSend] = React.useState<string | null>(null);
const handleSendMessage = async (content: string, files: File[]) => {
posthog.capture("user_message_sent", {
current_message_count: messages.length,
});
const promises = files.map((file) => convertImageToBase64(file));
const imageUrls = await Promise.all(promises);
const timestamp = new Date().toISOString();
dispatch(addUserMessage({ content, imageUrls, timestamp }));
send(createChatMessage(content, imageUrls, timestamp));
setMessageToSend(null);
};
const handleStop = () => {
posthog.capture("stop_button_clicked");
send(generateAgentStateChangeEvent(AgentState.STOPPED));
};
@@ -74,28 +64,6 @@ export function ChatInterface() {
return (
<div className="h-full flex flex-col justify-between">
{messages.length === 0 && (
<div className="flex flex-col gap-6 h-full px-4 items-center justify-center">
<div className="flex flex-col items-center p-4 bg-neutral-700 rounded-xl w-full">
<BuildIt width={45} height={54} />
<span className="font-semibold text-[20px] leading-6 -tracking-[0.01em] gap-1">
Let&apos;s start building!
</span>
</div>
<Suggestions
suggestions={Object.entries(SUGGESTIONS.repo)
.slice(0, 4)
.map(([label, value]) => ({
label,
value,
}))}
onSuggestionClick={(value) => {
setMessageToSend(value);
}}
/>
</div>
)}
<div
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
@@ -105,7 +73,7 @@ export function ChatInterface() {
isErrorMessage(message) ? (
<ErrorMessage
key={index}
id={message.id}
error={message.error}
message={message.message}
/>
) : (
@@ -155,8 +123,6 @@ export function ChatInterface() {
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
value={messageToSend ?? undefined}
onChange={setMessageToSend}
/>
</div>
@@ -5,7 +5,7 @@ import RejectIcon from "#/assets/reject";
import { I18nKey } from "#/i18n/declaration";
import AgentState from "#/types/AgentState";
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
import { useWsClient } from "#/context/ws-client-provider";
import { useSocket } from "#/context/socket";
interface ActionTooltipProps {
type: "confirm" | "reject";
@@ -37,7 +37,7 @@ function ActionTooltip({ type, onClick }: ActionTooltipProps) {
function ConfirmationButtons() {
const { t } = useTranslation();
const { send } = useWsClient();
const { send } = useSocket();
const handleStateChange = (state: AgentState) => {
const event = generateAgentStateChangeEvent(state);
+1 -2
View File
@@ -6,7 +6,6 @@ type Message = {
};
type ErrorMessage = {
error: boolean;
id?: string;
error: string;
message: string;
};
+4 -31
View File
@@ -1,41 +1,14 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
interface ErrorMessageProps {
id?: string;
error: string;
message: string;
}
export function ErrorMessage({ id, message }: ErrorMessageProps) {
const { t, i18n } = useTranslation();
const [showDetails, setShowDetails] = useState(true);
const [headline, setHeadline] = useState("");
const [details, setDetails] = useState(message);
useEffect(() => {
if (id && i18n.exists(id)) {
setHeadline(t(id));
setDetails(message);
setShowDetails(false);
}
}, [id, message, i18n.language]);
export function ErrorMessage({ error, message }: ErrorMessageProps) {
return (
<div className="flex gap-2 items-center justify-start border-l-2 border-danger pl-2 my-2 py-2">
<div className="text-sm leading-4 flex flex-col gap-2">
{headline && <p className="text-danger font-bold">{headline}</p>}
{headline && (
<button
type="button"
onClick={() => setShowDetails(!showDetails)}
className="cursor-pointer text-left"
>
{showDetails
? t("ERROR_MESSAGE$HIDE_DETAILS")
: t("ERROR_MESSAGE$SHOW_DETAILS")}
</button>
)}
{showDetails && <p className="text-neutral-300">{details}</p>}
<p className="text-danger font-bold">{error}</p>
<p className="text-neutral-300">{message}</p>
</div>
</div>
);
-188
View File
@@ -1,188 +0,0 @@
import React from "react";
import {
useFetcher,
useLoaderData,
useRouteLoaderData,
} from "@remix-run/react";
import { useDispatch, useSelector } from "react-redux";
import toast from "react-hot-toast";
import posthog from "posthog-js";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { ErrorObservation } from "#/types/core/observations";
import { addErrorMessage, addUserMessage } from "#/state/chatSlice";
import { handleAssistantMessage } from "#/services/actions";
import {
getCloneRepoCommand,
getGitHubTokenCommand,
} from "#/services/terminalService";
import {
clearFiles,
clearSelectedRepository,
setImportedProjectZip,
} from "#/state/initial-query-slice";
import { clientLoader as appClientLoader } from "#/routes/_oh.app";
import store, { RootState } from "#/store";
import { createChatMessage } from "#/services/chatService";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import { isGitHubErrorReponse } from "#/api/github";
import OpenHands from "#/api/open-hands";
import { base64ToBlob } from "#/utils/base64-to-blob";
import { setCurrentAgentState } from "#/state/agentSlice";
import AgentState from "#/types/AgentState";
import { getSettings } from "#/services/settings";
interface ServerError {
error: boolean | string;
message: string;
[key: string]: unknown;
}
const isServerError = (data: object): data is ServerError => "error" in data;
const isErrorObservation = (data: object): data is ErrorObservation =>
"observation" in data && data.observation === "error";
export function EventHandler({ children }: React.PropsWithChildren) {
const { events, status, send } = useWsClient();
const statusRef = React.useRef<WsClientProviderStatus | null>(null);
const runtimeActive = status === WsClientProviderStatus.ACTIVE;
const fetcher = useFetcher();
const dispatch = useDispatch();
const { files, importedProjectZip } = useSelector(
(state: RootState) => state.initalQuery,
);
const { ghToken, repo } = useLoaderData<typeof appClientLoader>();
const initialQueryRef = React.useRef<string | null>(
store.getState().initalQuery.initialQuery,
);
const sendInitialQuery = (query: string, base64Files: string[]) => {
const timestamp = new Date().toISOString();
send(createChatMessage(query, base64Files, timestamp));
};
const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const userId = React.useMemo(() => {
if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id;
return null;
}, [data?.user]);
const userSettings = getSettings();
React.useEffect(() => {
if (!events.length) {
return;
}
const event = events[events.length - 1];
if (event.token) {
fetcher.submit({ token: event.token as string }, { method: "post" });
return;
}
if (isServerError(event)) {
if (event.error_code === 401) {
toast.error("Session expired.");
fetcher.submit({}, { method: "POST", action: "/end-session" });
return;
}
if (typeof event.error === "string") {
toast.error(event.error);
} else {
toast.error(event.message);
}
return;
}
if (isErrorObservation(event)) {
dispatch(
addErrorMessage({
id: event.extras?.error_id,
message: event.message,
}),
);
return;
}
handleAssistantMessage(event);
}, [events.length]);
React.useEffect(() => {
if (statusRef.current === status) {
return; // This is a check because of strict mode - if the status did not change, don't do anything
}
statusRef.current = status;
const initialQuery = initialQueryRef.current;
if (status === WsClientProviderStatus.ACTIVE) {
let additionalInfo = "";
if (ghToken && repo) {
send(getCloneRepoCommand(ghToken, repo));
additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`;
dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'?
}
// if there's an uploaded project zip, add it to the chat
else if (importedProjectZip) {
additionalInfo = `Files have been uploaded. Please check the /workspace for files.`;
}
if (initialQuery) {
if (additionalInfo) {
sendInitialQuery(`${initialQuery}\n\n[${additionalInfo}]`, files);
} else {
sendInitialQuery(initialQuery, files);
}
dispatch(clearFiles()); // reset selected files
initialQueryRef.current = null;
}
}
if (status === WsClientProviderStatus.OPENING && initialQuery) {
dispatch(
addUserMessage({
content: initialQuery,
imageUrls: files,
timestamp: new Date().toISOString(),
}),
);
}
if (status === WsClientProviderStatus.STOPPED) {
store.dispatch(setCurrentAgentState(AgentState.STOPPED));
}
}, [status]);
React.useEffect(() => {
if (runtimeActive && userId && ghToken) {
// Export if the user valid, this could happen mid-session so it is handled here
send(getGitHubTokenCommand(ghToken));
}
}, [userId, ghToken, runtimeActive]);
React.useEffect(() => {
(async () => {
if (runtimeActive && importedProjectZip) {
// upload files action
try {
const blob = base64ToBlob(importedProjectZip);
const file = new File([blob], "imported-project.zip", {
type: blob.type,
});
await OpenHands.uploadFiles([file]);
dispatch(setImportedProjectZip(null));
} catch (error) {
toast.error("Failed to upload project files.");
}
}
})();
}, [runtimeActive, importedProjectZip]);
React.useEffect(() => {
if (userSettings.LLM_API_KEY) {
posthog.capture("user_activated");
}
}, [userSettings.LLM_API_KEY]);
return children;
}
+1 -11
View File
@@ -1,6 +1,3 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface CustomInputProps {
name: string;
label: string;
@@ -16,19 +13,12 @@ export function CustomInput({
defaultValue,
type = "text",
}: CustomInputProps) {
const { t } = useTranslation();
return (
<label htmlFor={name} className="flex flex-col gap-2">
<span className="text-[11px] leading-4 tracking-[0.5px] font-[500] text-[#A3A3A3]">
{label}
{required && <span className="text-[#FF4D4F]">*</span>}
{!required && (
<span className="text-[#A3A3A3]">
{" "}
{t(I18nKey.CUSTOM_INPUT$OPTIONAL_LABEL)}
</span>
)}
{!required && <span className="text-[#A3A3A3]"> (optional)</span>}
</span>
<input
id={name}

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