Compare commits

...

110 Commits

Author SHA1 Message Date
openhands
1755e860f6 Fix issue #5229: [Documentation]: Micro-agents 2024-11-23 15:59:47 +00:00
Rohit Malhotra
a2779fe2f6 [Resolver] Invoking resolver when macro is present anywhere in comment body (#5197) 2024-11-22 23:22:38 -05:00
Rohit Malhotra
4b0faaf79b [Resolver]: Removing target branch param from resolve_issue.py in workflow definition (#5217) 2024-11-22 19:01:19 -05:00
Xingyao Wang
7db0a35305 feat(runtime): Set server process to run with highest system priority (#5206)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-11-23 05:31:19 +08:00
Raymond Xu
98b2994af2 [resolver] Select a non-main branch option (#5075)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-22 20:47:57 +00:00
Nan Jiang
463d4e9a46 eval: add commit0 benchmark (#5153)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-22 19:49:45 +00:00
Xingyao Wang
1021e0c416 chore(runtime): Add logging for /server_info endpoint (#5208)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-22 19:42:35 +00:00
sp.wack
becb17f0c8 feat(frontend): Utilize TanStack Query (#5096) 2024-11-22 23:38:27 +04:00
Xingyao Wang
bb8b4a0b18 feat(runtime): add system resource metrics to /server_info endpoint (#5207)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-23 02:28:32 +08:00
mamoodi
36e3dc5c19 Add eval workflow that triggers remote eval job (#5108) 2024-11-22 19:24:33 +01:00
Raymond Xu
24d5facec5 Show the link to the All Hands product roadmap (#5192)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-22 16:43:45 +00:00
Rohit Malhotra
135a62ca9c [Resolver]: Removing redundant checks (#5196)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-22 14:28:38 +00:00
dependabot[bot]
83add62991 Bump the eslint group across 1 directory with 2 updates (#5200)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 10:50:36 +04:00
niliy01
68d1e76ccd fix: remove repeated completion assignment in llm.py (#5167) 2024-11-22 01:55:26 +01:00
mamoodi
39dad706ca Release 0.14.2 (#5182) 2024-11-21 14:42:33 -05:00
diwu-sf
ea6809b283 rename github to github_utils to avoid import circular dependency pro… (#5180) 2024-11-21 14:17:58 -05:00
Engel Nyst
d08886f30e Fix non-function calls messages (#5026)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-21 18:18:49 +00:00
Cheng Yang
68e52a9c62 feat: add return type hints to LLM class methods (#5173) 2024-11-21 14:00:46 +01:00
Cheng Yang
7e38297732 fix: correct relative links in agenthub README.md (#5170) 2024-11-21 06:39:32 -05:00
Graham Neubig
12ed523c01 docs: Add note about organizational token policies (#5161)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-21 04:07:21 +00:00
OpenHands
ebce77ab56 Fix issue #5155: [Resolver] Could we get a .md of tips for the .openhands_instructions file? (#5163)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-20 23:03:22 -05:00
Rohit Malhotra
f4a2df859f [Bug][Resolver] Enable caching for reusable workflow (#5165) 2024-11-21 03:46:08 +00:00
Robert Brennan
94a8f58ece fix up logging in listen.py (#5145) 2024-11-20 22:42:13 -05:00
young010101
746722e1b5 style: remove extra newline in LLM wrapper function (#5149) 2024-11-20 22:41:51 -05:00
Robert Brennan
27f136b802 mitigate memory leak (#5152) 2024-11-20 22:40:30 -05:00
OpenHands
e211152f93 Fix issue #5159: [Bug]: lint-fix workflow terminates prematurely due to exit code 1 (#5160) 2024-11-21 02:36:47 +00:00
Graham Neubig
07b96cc8c9 docs: Add documentation on how to add new tools to codeact_agent (#5150)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-20 20:19:51 +00:00
young010101
3a65b7b07d docs: add missing toml_file parameter description in get_llm_config_a… (#5147) 2024-11-20 21:06:02 +01:00
young010101
5c83698524 Docs/fix logging param name (#5146) 2024-11-20 20:07:06 +01:00
Robert Brennan
cde7ce49be fix up lockup when long actions are run (#5144) 2024-11-20 15:42:02 +00:00
dependabot[bot]
24a83eb52d Bump the docusaurus group in /docs with 7 updates (#5140)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 14:48:40 +00:00
Rohit Malhotra
2a78b3323b Adding experimental option for resolver macro (#5131) 2024-11-19 17:42:49 -05:00
Robert Brennan
a3977621ed Add /health endpoint to server (#5136)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-19 17:40:20 -05:00
Robert Brennan
018080aae0 fix rate limiting (#5135) 2024-11-19 22:01:07 +00:00
mamoodi
302e41d7bb Release 0.14.1 (#5133) 2024-11-19 14:53:24 -05:00
Robert Brennan
3c61a9521b Simple initial rate limiting implementation (#4976) 2024-11-19 13:46:14 -05:00
Robert Brennan
c9ed9b166b handle exceptions more explicitly (#4971) 2024-11-19 13:46:03 -05:00
Robert Brennan
e052c25572 Fix GitHub prompt (#5123) 2024-11-19 12:49:20 -05:00
Rohit Malhotra
f0ca45c59e Add clarity for Openhands-resolver guide (#5124) 2024-11-19 12:26:11 -05:00
Rohit Malhotra
7f5022c8fe Refactor issue filtering (#5129) 2024-11-19 12:23:42 -05:00
Rohit Malhotra
de07fcfddc Moving resolver settings to repo variables (#5130) 2024-11-19 12:17:55 -05:00
Xingyao Wang
ff84a3eede chore: remove specified sid (#5127) 2024-11-19 16:41:27 +00:00
Rohit Malhotra
1f723293db Add macro invocations to example workflow (#5121) 2024-11-19 13:34:25 +00:00
Raymond Xu
2c580387c5 Allow to merge to a specific target branch instead of main (#5109) 2024-11-19 07:16:29 -05:00
young010101
ca64c69b4a Docs update runtime link (#5117) 2024-11-19 02:45:06 +00:00
Xingyao Wang
a531413d86 fix(eval): support setting hard timeout per evaluation instance (#5110) 2024-11-18 21:22:55 -05:00
Xingyao Wang
422104c877 fix #5111: add FunctionCallNotExistsError to handle cases where tool calling failed (#5113) 2024-11-18 21:21:46 -05:00
Rohit Malhotra
c75ca7d976 Bug/resolver context fix (#5115) 2024-11-18 17:53:46 -05:00
Robert Brennan
6b89386398 fix 404 issue for /config (#5114) 2024-11-18 22:34:18 +00:00
Graham Neubig
a87b8599eb fix: run only linting hooks in lint-fix workflow (#5107)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-18 18:38:29 +00:00
mamoodi
de821718fd Use How to join community as reference for slack, discord, issues links (#5097) 2024-11-18 15:41:56 +00:00
Faraz Shamim
088e895a3d Fix #4997 (#5006)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-17 13:50:30 +00:00
Graham Neubig
104f52bcdd Add a "community" page with maintainer info (#4962) 2024-11-16 08:10:56 -05:00
Ryan H. Tran
97f3249205 Move linter and diff utils to openhands-aci (#5020) 2024-11-16 06:58:26 +01:00
sp.wack
9d47ddba38 Reduce output from frontend tests (#5023) 2024-11-16 06:57:41 +01:00
OpenHands
f7652bd558 Fix issue #5080: [Bug]: lint-fix.yml github action doesn't work on a branch not from this repo (#5081) 2024-11-16 06:55:41 +01:00
OpenHands
2b7932b46c Fix issue #5070: [Bug]: lint-fix workflow is failing (#5078) 2024-11-16 01:43:49 +00:00
OpenHands
7074e45ec3 Fix issue #5059: [Bug]: Github resolver looking for wrong PR number (#5062)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-15 19:41:48 -05:00
Raymond Xu
a679fcc3b5 [docs] add tips from Graham Neubig on how to make good contributions (#5012)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-15 21:15:11 +00:00
Raymond Xu
8b1d5f5a3b Always push repo or make a PR, comment (#5063) 2024-11-15 21:14:47 +00:00
mamoodi
9882b62777 Update some OpenHands repo documentation and the official document site (#5060) 2024-11-15 20:48:02 +00:00
OpenHands
b49bdb9d85 Fix issue #5064: lint-fix github action (#5065) 2024-11-15 15:47:24 -05:00
mamoodi
00ffc33d1b Release 0.14.0 (#5027) 2024-11-15 16:02:02 +00:00
sp.wack
1acb66c2b3 feat(frontend): Create push to Github action button in chat interface (#4993) 2024-11-15 15:12:13 +00:00
Xingyao Wang
5b3db1bd33 feat: make add_in_context_learning_example configurable in fn call converter (#5018) 2024-11-15 23:05:05 +08:00
Xingyao Wang
bdc4513937 fix(swebench): handle error in eval_infer and run_infer (#5017) 2024-11-15 23:04:56 +08:00
sp.wack
ffc4d32440 feat(frontend): Keep prompt after project upload or repo selection (#4925) 2024-11-15 16:56:47 +02:00
sp.wack
9cd248d475 feat(frontend): Display runtime ID in the browser console if available (#4978) 2024-11-15 16:38:31 +02:00
OpenHands
5f52eebb40 Fix issue #5021: Add links to the resolver messages (#5022) 2024-11-15 13:05:25 +00:00
Graham Neubig
b0c4580999 Update openhands-resolver.yml with correct package name (#5014) 2024-11-15 06:48:18 -05:00
Robert Brennan
f3b35663e9 fix zip downloads (#5009) 2024-11-14 17:17:36 -05:00
OpenHands
be92965209 Fix issue #4944: [Bug]: Missing GitHub token link in account settings (#4946)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-11-14 22:21:02 +02:00
sp.wack
89b304ccb7 refactor(frontend): Improve chat input padding (#4928) 2024-11-14 22:19:04 +02:00
sp.wack
01cacf7c33 feat(frontend): Wait for events before rendering messages (#4994)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2024-11-14 22:09:29 +02:00
Engel Nyst
fac5237c69 Fix user commands in terminal with function calling (#4955)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-14 19:14:36 +00:00
Robert Brennan
c784151765 fix file descriptor leaks (#4988)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-14 14:06:33 -05:00
Graham Neubig
ce6f99d80e Add GITHUB_USERNAME env var to resolver step (#4999)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-14 18:42:59 +00:00
Ketan Ramaneti
852c90f64a [fix eval] Fix issues with miniwob remote runtime evaluation (#5001) 2024-11-14 18:00:48 +00:00
Ketan Ramaneti
42b49e6c43 [fix eval] Fix issues with aider_bench remote runtime evaluation (#5000) 2024-11-14 17:58:45 +00:00
Xingyao Wang
07f0d1ccb3 feat(llm): convert function call request for non-funcall OSS model (#4711)
Co-authored-by: Calvin Smith <email@cjsmith.io>
2024-11-15 00:40:09 +08:00
Robert Brennan
52a428d74a Fix markdown ordered list numbering (#4989)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-14 10:59:48 -05:00
OpenHands
27cd507cd2 Fix issue #4985: [Bug]: Cannot exit the session when on Jupyter or Browser tab in the UI (#4986) 2024-11-14 10:06:35 -05:00
Graham Neubig
a753babb7a Integrate OpenHands resolver into main repository (#4964)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2024-11-14 09:45:46 -05:00
Rohit Malhotra
38dc41ca42 Fix: [Bug] Do not render editor action buttons (save/discard) when displaying non-code files (#4903) 2024-11-14 09:09:28 +02:00
Engel Nyst
8dee334236 Context Window Exceeded fix (#4977) 2024-11-14 02:42:39 +00:00
Engel Nyst
a93f1402de Clean up file logs (#4979) 2024-11-13 20:17:21 +00:00
Robert Brennan
bc3f0ac24a fix imports (#4974) 2024-11-13 17:04:16 +00:00
Robert Brennan
f55ddbed0e fix docker leak (#4970) 2024-11-14 00:23:07 +08:00
Xingyao Wang
fd81670ba8 feat: add VSCode to OpenHands runtime and UI (#4745)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-11-14 00:20:49 +08:00
sp.wack
79ed4e3567 fix(frontend): Recover full message history if exists (#4961) 2024-11-13 15:38:30 +02:00
sp.wack
b3fbbbaa9d feat(frontend): Move posthog key to config and upgrade posthog-js (#4940) 2024-11-13 07:56:04 +00:00
tofarr
87c02177d7 Reconnecting websockets (#4954) 2024-11-13 09:38:26 +02:00
OpenHands
207df9dd30 Fix issue #4912: [Bug]: BedrockException: "The number of toolResult blocks at messages.2.content exceeds the number of toolUse blocks of previous turn.". (#4937)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2024-11-12 17:23:11 -05:00
tofarr
59f7093428 Fix max iterations (#4949) 2024-11-12 21:09:43 +00:00
sp.wack
123fb4b75d feat(posthog): Add saas login event (#4948) 2024-11-12 20:37:59 +00:00
mamoodi
40e2d28e87 Release 0.13.1 (#4947) 2024-11-12 15:08:10 -05:00
OpenHands
c555611d58 Fix issue #4941: [Bug]: Browser tab does not reset after starting a new session (#4945)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-11-12 19:40:12 +00:00
Calvin Smith
50e7da9c3d fix(evaluation): SWE-bench evaluation script supports multiprocessing (#4943) 2024-11-12 12:19:57 -07:00
sp.wack
0cfb132ab7 fix(frontend): Remove dotted outline on focus (#4926) 2024-11-12 18:27:06 +02:00
Robert Brennan
17f4c6e1a9 Refactor sessions a bit, and fix issue where runtimes get killed (#4900) 2024-11-12 16:20:36 +00:00
Xingyao Wang
910b283ac2 fix(llm): bedrock throw errors if content contains empty string (#4935) 2024-11-12 15:53:22 +00:00
OpenHands
b54724ac3f Fix issue #4931: Make use of microagents configurable in codeact_agent (#4932)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-12 15:42:13 +00:00
Robert Brennan
0633a99298 Fix resume runtime after a pause (#4904) 2024-11-12 09:03:02 -05:00
Ryan H. Tran
d9c5f11046 Replace file editor with openhands-aci (#4782) 2024-11-12 21:26:33 +08:00
Engel Nyst
32fdcd58e5 Update litellm (#4927) 2024-11-12 11:24:19 +00:00
sp.wack
de71b7cdb8 test(frontend): Fix failing e2e test due to mock delay (#4923) 2024-11-12 10:50:38 +00:00
sp.wack
04aeccfb69 fix(frontend): Remove quotes from suggestion (#4921) 2024-11-12 12:30:43 +02:00
Faraz Shamim
4eea1286d4 Issue #4399 : Replaced all occurences (#4878)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-11-12 10:58:09 +01:00
Robert Brennan
488a320ffd update to use github client lib (#4909) 2024-11-12 00:56:50 +00:00
Robert Brennan
377fadc2eb fix remote runtimes (#4902) 2024-11-12 00:02:34 +00:00
278 changed files with 19666 additions and 6725 deletions

View File

@@ -286,7 +286,6 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
SKIP_CONTAINER_LOGS=true \
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
@@ -364,7 +363,6 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
SKIP_CONTAINER_LOGS=true \
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \

62
.github/workflows/lint-fix.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Lint Fix
on:
pull_request:
types: [labeled]
jobs:
lint-fix:
if: github.event.label.name == 'lint-fix'
name: Fix linting issues
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# Frontend lint fixes
- name: Install Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install frontend dependencies
run: |
cd frontend
npm install --frozen-lockfile
- name: Fix frontend lint issues
run: |
cd frontend
npm run lint:fix
# Python lint fixes
- name: Set up python
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: 'pip'
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Fix python lint issues
run: |
# Run all pre-commit hooks and continue even if they modify files (exit code 1)
pre-commit run --config ./dev_config/python/.pre-commit-config.yaml --files openhands/**/* evaluation/**/* tests/**/* || true
# Commit and push changes if any
- name: Check for changes
id: git-check
run: |
git diff --quiet || echo "changes=true" >> $GITHUB_OUTPUT
- name: Commit and push if there are changes
if: steps.git-check.outputs.changes == 'true'
run: |
git config --local user.email "openhands@all-hands.dev"
git config --local user.name "OpenHands Bot"
git add -A
git commit -m "🤖 Auto-fix linting issues"
git push

View File

@@ -1,15 +1,291 @@
name: Resolve Issues with OpenHands
name: Auto-Fix Tagged Issue with OpenHands
on:
workflow_call:
inputs:
max_iterations:
required: false
type: number
default: 50
macro:
required: false
type: string
default: "@openhands-agent"
target_branch:
required: false
type: string
default: "main"
description: "Target branch to pull and create PR against"
secrets:
LLM_MODEL:
required: true
LLM_API_KEY:
required: true
LLM_BASE_URL:
required: false
PAT_TOKEN:
required: true
PAT_USERNAME:
required: true
issues:
types: [labeled]
pull_request:
types: [labeled]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
call-openhands-resolver:
uses: All-Hands-AI/openhands-resolver/.github/workflows/openhands-resolver.yml@main
if: github.event.label.name == 'fix-me'
with:
max_iterations: 50
secrets: inherit
auto-fix:
if: |
github.event_name == 'workflow_call' ||
github.event.label.name == 'fix-me' ||
github.event.label.name == 'fix-me-experimental' ||
(
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
contains(github.event.comment.body, inputs.macro || '@openhands-agent') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER')
) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, inputs.macro || '@openhands-agent') &&
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Get latest versions and create requirements.txt
run: |
python -m pip index versions openhands-ai > openhands_versions.txt
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
cat requirements.txt
- name: Cache pip dependencies
if: |
!(
github.event.label.name == 'fix-me-experimental' ||
(
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
contains(github.event.comment.body, '@openhands-agent-exp')
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@openhands-agent-exp')
)
)
uses: actions/cache@v3
with:
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
- name: Check required environment variables
env:
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
run: |
required_vars=("LLM_MODEL" "LLM_API_KEY" "PAT_TOKEN" "PAT_USERNAME")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "Error: Required environment variable $var is not set."
exit 1
fi
done
- name: Set environment variables
run: |
if [ -n "${{ github.event.review.body }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
elif [ -n "${{ github.event.issue.pull_request }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
elif [ -n "${{ github.event.pull_request.number }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
else
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
fi
if [ -n "${{ github.event.review.body }}" ]; then
echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV
else
echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV
fi
echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
# Set branch variables
echo "TARGET_BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
- name: Comment on issue with start message
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueType = process.env.ISSUE_TYPE;
github.rest.issues.createComment({
issue_number: ${{ env.ISSUE_NUMBER }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `[OpenHands](https://github.com/All-Hands-AI/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
});
- name: Install OpenHands
run: |
if [[ "${{ github.event.label.name }}" == "fix-me-experimental" ]] ||
([[ "${{ github.event_name }}" == "issue_comment" || "${{ github.event_name }}" == "pull_request_review_comment" ]] &&
[[ "${{ github.event.comment.body }}" == "@openhands-agent-exp"* ]]) ||
([[ "${{ github.event_name }}" == "pull_request_review" ]] &&
[[ "${{ github.event.review.body }}" == "@openhands-agent-exp"* ]]); then
python -m pip install --upgrade pip
pip install git+https://github.com/all-hands-ai/openhands.git
else
python -m pip install --upgrade -r requirements.txt
fi
- name: Attempt to resolve issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PYTHONPATH: ""
run: |
cd /tmp && python -m openhands.resolver.resolve_issue \
--repo ${{ github.repository }} \
--issue-number ${{ env.ISSUE_NUMBER }} \
--issue-type ${{ env.ISSUE_TYPE }} \
--max-iterations ${{ env.MAX_ITERATIONS }} \
--comment-id ${{ env.COMMENT_ID }} \
- name: Check resolution result
id: check_result
run: |
if cd /tmp && grep -q '"success":true' output/output.jsonl; then
echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT
else
echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT
fi
- name: Upload output.jsonl as artifact
uses: actions/upload-artifact@v4
if: always() # Upload even if the previous steps fail
with:
name: resolver-output
path: /tmp/output/output.jsonl
retention-days: 30 # Keep the artifact for 30 days
- name: Create draft PR or push branch
if: always() # Create PR or branch even if the previous steps fail
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PYTHONPATH: ""
run: |
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type draft | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type branch \
--send-on-failure | tee branch_result.txt && \
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
fi
- name: Comment on issue
uses: actions/github-script@v7
if: always() # Comment on issue even if the previous steps fail
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
const issueNumber = ${{ env.ISSUE_NUMBER }};
const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }};
let prNumber = '';
let branchName = '';
let logContent = '';
const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`;
try {
if (success){
logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim();
} else {
logContent = fs.readFileSync('/tmp/branch_result.txt', 'utf8').trim();
}
} catch (error) {
console.error('Error reading results file:', error);
}
try {
if (success) {
prNumber = fs.readFileSync('/tmp/pr_number.txt', 'utf8').trim();
} else {
branchName = fs.readFileSync('/tmp/branch_name.txt', 'utf8').trim();
}
} catch (error) {
console.error('Error reading file:', error);
}
if (logContent.includes(noChangesMessage)) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.`
});
} else if (success && prNumber) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.`
});
} else if (!success && branchName) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`
});
} else {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.`
});
}

53
.github/workflows/run-eval.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
# Run evaluation on a PR
name: Run Eval
# Runs when a PR is labeled with one of the "run-eval-" labels
on:
pull_request:
types: [labeled]
jobs:
trigger-job:
name: Trigger remote eval job
if: ${{ github.event.label.name == 'run-eval-xs' || github.event.label.name == 'run-eval-s' || github.event.label.name == 'run-eval-m' }}
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Trigger remote job
run: |
REPO_URL="https://github.com/${{ github.repository }}"
PR_BRANCH="${{ github.head_ref }}"
echo "Repository URL: $REPO_URL"
echo "PR Branch: $PR_BRANCH"
if [[ "${{ github.event.label.name }}" == "run-eval-xs" ]]; then
EVAL_INSTANCES="1"
elif [[ "${{ github.event.label.name }}" == "run-eval-s" ]]; then
EVAL_INSTANCES="5"
elif [[ "${{ github.event.label.name }}" == "run-eval-m" ]]; then
EVAL_INSTANCES="30"
fi
curl -X POST \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"main\", \"inputs\": {\"github-repo\": \"${REPO_URL}\", \"github-branch\": \"${PR_BRANCH}\", \"pr-number\": \"${{ github.event.pull_request.number }}\", \"eval-instances\": \"${EVAL_INSTANCES}\"}}" \
https://api.github.com/repos/All-Hands-AI/evaluation/actions/workflows/create-branch.yml/dispatches
# Send Slack message
PR_URL="https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
slack_text="PR $PR_URL has triggered evaluation on $EVAL_INSTANCES instances..."
curl -X POST -H 'Content-type: application/json' --data '{"text":"'"$slack_text"'"}' \
https://hooks.slack.com/services/${{ secrets.SLACK_TOKEN }}
- name: Comment on PR
uses: KeisukeYamashita/create-comment@v1
with:
unique: false
comment: |
Running evaluation on the PR. Once eval is done, the results will be posted.

4
.gitignore vendored
View File

@@ -175,6 +175,10 @@ evaluation/gaia/data
evaluation/gorilla/data
evaluation/toolqa/data
evaluation/scienceagentbench/benchmark
evaluation/commit0_bench/repos
# openhands resolver
output/
# frontend

43
COMMUNITY.md Normal file
View File

@@ -0,0 +1,43 @@
# 🙌 The OpenHands Community
The OpenHands community is built around the belief that (1) AI and AI agents are going to fundamentally change the way
we build software, and (2) if this is true, we should do everything we can to make sure that the benefits provided by
such powerful technology are accessible to everyone.
If this resonates with you, we'd love to have you join us in our quest!
## 🤝 How to Join
Check out our [How to Join the Community section.](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-join-the-community)
## 💪 Becoming a Contributor
We welcome contributions from everyone! Whether you're a developer, a researcher, or simply enthusiastic about advancing
the field of software engineering with AI, there are many ways to get involved:
- **Code Contributions:** Help us develop new core functionality, improve our agents, improve the frontend and other
interfaces, or anything else that would help make OpenHands better.
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in
evaluating the models, or suggest improvements.
- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability.
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
## Code of Conduct
We have a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect all contributors to adhere to.
Long story short, we are aiming for an open, welcoming, diverse, inclusive, and healthy community.
All contributors are expected to contribute to building this sort of community.
## 🛠️ Becoming a Maintainer
For contributors who have made significant and sustained contributions to the project, there is a possibility of joining
the maintainer team. The process for this is as follows:
1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any
maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated.
2. Once a maintainer nominates a new maintainer, there will be a discussion period among the maintainers for at least 3 days.
3. If no concerns are raised the nomination will be accepted by acclamation, and if concerns are raised there will be a discussion and possible vote.
Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking
at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md).

View File

@@ -54,7 +54,7 @@ The agent needs a place to run code and commands. When you run OpenHands on your
to do this by default. But there are other ways of creating a sandbox for the agent.
If you work for a company that provides a cloud-based runtime, you could help us add support for that runtime
by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/runtime.py).
by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/base.py).
#### Testing
When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites.
@@ -92,3 +92,32 @@ You may also check out previous PRs in the [PR list](https://github.com/All-Hand
If your changes are user-facing (e.g. a new feature in the UI, a change in behavior, or a bugfix)
please include a short message that we can add to our changelog.
## How to Make Effective Contributions
### Opening Issues
If you notice any bugs or have any feature requests please open them via the [issues page](https://github.com/All-Hands-AI/OpenHands/issues). We will triage based on how critical the bug is or how potentially useful the improvement is, discuss, and implement the ones that the community has interest/effort for.
Further, if you see an issue you like, please leave a "thumbs-up" or a comment, which will help us prioritize.
### Making Pull Requests
We're generally happy to consider all PRs, with the evaluation process varying based on the type of change:
#### For Small Improvements
Small improvements with few downsides are typically reviewed and approved quickly.
One thing to check when making changes is to ensure that all continuous integration tests pass, which you can check before getting a review.
#### For Core Agent Changes
We need to be more careful with changes to the core agent, as it is imperative to maintain high quality. These PRs are evaluated based on three key metrics:
1. **Accuracy**
2. **Efficiency**
3. **Code Complexity**
If it improves accuracy, efficiency, or both with only a minimal change to code quality, that's great we're happy to merge it in!
If there are bigger tradeoffs (e.g. helping efficiency a lot and hurting accuracy a little) we might want to put it behind a feature flag.
Either way, please feel free to discuss on github issues or slack, and we will give guidance and preliminary feedback.

View File

@@ -38,7 +38,9 @@ make build
```
### 3. Configuring the Language Model
OpenHands supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library. By default, we've chosen the mighty GPT-4 from OpenAI as our go-to model, but the world is your oyster! You can unleash the potential of Anthropic's suave Claude, the enigmatic Llama, or any other LM that piques your interest.
OpenHands supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library.
By default, we've chosen Claude Sonnet 3.5 as our go-to model, but the world is your oyster! You can unleash the
potential of any other LM that piques your interest.
To configure the LM of your choice, run:
@@ -52,10 +54,7 @@ To configure the LM of your choice, run:
Environment variables > config.toml variables > default variables
**Note on Alternative Models:**
Some alternative models may prove more challenging to tame than others. Fear not, brave adventurer! We shall soon unveil LLM-specific documentation to guide you on your quest.
And if you've already mastered the art of wielding a model other than OpenAI's GPT, we encourage you to share your setup instructions with us by creating instructions and adding it [to our documentation](https://github.com/All-Hands-AI/OpenHands/tree/main/docs/modules/usage/llms).
For a full list of the LM providers and models available, please consult the [litellm documentation](https://docs.litellm.ai/docs/providers).
See [our documentation](https://docs.all-hands.dev/modules/usage/llms) for recommended models.
### 4. Running the application
#### Option A: Run the Full Application
@@ -98,9 +97,10 @@ poetry run pytest ./tests/unit/test_*.py
2. Update the poetry.lock file via `poetry lock --no-update`
### 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
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.14-nikolaik`
## Develop inside Docker container

View File

@@ -6,9 +6,9 @@ These are the procedures and guidelines on how issues are triaged in this repo b
* Issues may be tagged with what it relates to (**backend**, **frontend**, **agent quality**, etc.)
## Severity
* **Low**: Minor issues, single user report
* **Medium**: Affecting multiple users
* **Critical**: Affecting all users or potential security issues
* **Low**: Minor issues or affecting single user.
* **Medium**: Affecting multiple users.
* **Critical**: Affecting all users or potential security issues.
## Effort
* Issues may be estimated with effort required (**small effort**, **medium effort**, **large effort**)
@@ -17,9 +17,9 @@ These are the procedures and guidelines on how issues are triaged in this repo b
* Issues with low implementation difficulty may be tagged with **good first issue**
## Not Enough Information
* User is asked to provide more information (logs, how to reproduce, etc.) when the issue is not clear
* If an issue is unclear and the author does not provide more information or respond to a request, the issue may be closed as **not planned** (Usually after a week)
* User is asked to provide more information (logs, how to reproduce, etc.) when the issue is not clear.
* If an issue is unclear and the author does not provide more information or respond to a request, the issue may be closed as **not planned** (Usually after a week).
## Multiple Requests/Fixes in One Issue
* These issues will be narrowed down to one request/fix so the issue is more easily tracked and fixed
* Issues may be broken down into multiple issues if required
* These issues will be narrowed down to one request/fix so the issue is more easily tracked and fixed.
* Issues may be broken down into multiple issues if required.

View File

@@ -38,16 +38,16 @@ 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.14-nikolaik
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.14-nikolaik \
-e LOG_ALL_EVENTS=true \
-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.14
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
@@ -61,7 +61,7 @@ works best, but you have [many options](https://docs.all-hands.dev/modules/usage
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://github.com/All-Hands-AI/OpenHands-resolver).
or run it on tagged issues with [a github action](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md).
Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
@@ -77,28 +77,21 @@ To learn more about the project, and for tips on using OpenHands,
There you'll find resources on how to use different LLM providers,
troubleshooting resources, and advanced configuration options.
## 🤝 How to Contribute
## 🤝 How to Join the Community
OpenHands is a community-driven project, and we welcome contributions from everyone.
Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of
software engineering with AI, there are many ways to get involved:
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- **Code Contributions:** Help us develop new agents, core functionality, the frontend and other interfaces, or sandboxing solutions.
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements.
- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability.
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
## 🤖 Join Our Community
Whether you're a developer, a researcher, or simply enthusiastic about OpenHands, we'd love to have you in our community.
Let's make software engineering better together!
- [Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development.
- [Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md).
## 📈 Progress
See the monthly OpenHands roadmap [here](https://github.com/orgs/All-Hands-AI/projects/1) (updated at the maintainer's meeting at the end of each month).
<p align="center">
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">

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.14-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

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.14-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -14,4 +14,4 @@ Pour utiliser l'Action GitHub OpenHands dans le dépôt OpenHands, un mainteneur
## Installation de l'Action dans un nouveau dépôt
Pour installer l'Action GitHub OpenHands dans votre propre dépôt, suivez les [instructions dans le dépôt OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands-resolver?tab=readme-ov-file#using-the-github-actions-workflow).
Pour installer l'Action GitHub OpenHands dans votre propre dépôt, suivez les [instructions dans le dépôt OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md).

View File

@@ -12,4 +12,4 @@
## 在新仓库中安装 Action
要在你自己的仓库中安装 OpenHands GitHub Action请按照 [OpenHands Resolver 仓库中的说明](https://github.com/All-Hands-AI/OpenHands-resolver?tab=readme-ov-file#using-the-github-actions-workflow) 进行操作。
要在你自己的仓库中安装 OpenHands GitHub Action请按照 [OpenHands Resolver 仓库中的说明](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md) 进行操作。

View File

@@ -1,6 +1,6 @@
# 📚 Misc
# About OpenHands
## ⭐️ Research Strategy
## Research Strategy
Achieving full replication of production-grade applications with LLMs is a complex endeavor. Our strategy involves:
@@ -9,34 +9,11 @@ Achieving full replication of production-grade applications with LLMs is a compl
3. **Task Planning:** Developing capabilities for bug detection, codebase management, and optimization
4. **Evaluation:** Establishing comprehensive evaluation metrics to better understand and improve our models
## 🚧 Default Agent
## Default Agent
Our default Agent is currently the [CodeActAgent](agents), which is capable of generating code and handling files.
## 🤝 How to Contribute
OpenHands is a community-driven project, and we welcome contributions from everyone. Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of software engineering with AI, there are many ways to get involved:
- **Code Contributions:** Help us develop the core functionalities, frontend interface, or sandboxing solutions
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements
- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability
For details, please check [this document](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md).
## 🤖 Join Our Community
We have both Slack workspace for the collaboration on building OpenHands and Discord server for discussion about anything related, e.g., this project, LLM, agent, etc.
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA)
- [Discord server](https://discord.gg/ESHStjSjD4)
If you would love to contribute, feel free to join our community. Let's simplify software engineering together!
🐚 **Code less, make more with OpenHands.**
[![Star History Chart](https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date)](https://star-history.com/#All-Hands-AI/OpenHands&Date)
## 🛠️ Built With
## Built With
OpenHands is built using a combination of powerful frameworks and libraries, providing a robust foundation for its development. Here are the key technologies used in the project:
@@ -44,6 +21,6 @@ OpenHands is built using a combination of powerful frameworks and libraries, pro
Please note that the selection of these technologies is in progress, and additional technologies may be added or existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to enhance the capabilities of OpenHands.
## 📜 License
## License
Distributed under the MIT License. See [our license](https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE) for more information.
Distributed under MIT [License](https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE).

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.14-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.14 \
python -m openhands.core.cli
```

View File

@@ -62,25 +62,3 @@ Run OpenHands by running ```make run``` in the top level directory.
## Technical Explanation
Please refer to [custom docker image section of the runtime documentation](https://docs.all-hands.dev/modules/usage/architecture/runtime#advanced-how-openhands-builds-and-maintains-od-runtime-images) for more details.
## Troubleshooting / Errors
### Error: ```useradd: UID 1000 is not unique```
If you see this error in the console output it is because OpenHands is trying to create the openhands user in the sandbox with a UID of 1000, however this UID is already being used in the image (for some reason). To fix this change the sandbox_user_id field in the config.toml file to a different value:
```toml
[core]
workspace_base="./workspace"
run_as_openhands=true
sandbox_base_container_image="custom_image"
sandbox_user_id="1001"
```
### Port use errors
If you see an error about a port being in use or unavailable, try deleting all running Docker Containers (run `docker ps` and `docker rm` relevant containers) and then re-running ```make run``` .
## Discuss
For other issues or questions join the [Slack](https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA) or [Discord](https://discord.gg/ESHStjSjD4) and ask!

View File

@@ -4,12 +4,92 @@ This guide explains how to use the OpenHands GitHub Action, both within the Open
## Using the Action in the OpenHands Repository
To use the OpenHands GitHub Action in the OpenHands repository, an OpenHands maintainer can:
To use the OpenHands GitHub Action in a repository, you can:
1. Create an issue in the repository.
2. Add the `fix-me` label to the issue.
3. The action will automatically trigger and attempt to resolve the issue.
2. Add the `fix-me` label to the issue or leave a comment on the issue starting with `@openhands-agent`.
The action will automatically trigger and attempt to resolve the issue.
## Installing the Action in a New Repository
To install the OpenHands GitHub Action in your own repository, follow the [directions in the OpenHands Resolver repo](https://github.com/All-Hands-AI/OpenHands-resolver?tab=readme-ov-file#using-the-github-actions-workflow).
To install the OpenHands GitHub Action in your own repository, follow
the [README for the OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md).
## Usage Tips
### Iterative resolution
1. Create an issue in the repository.
2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent`
3. Review the attempt to resolve the issue by checking the pull request
4. Follow up with feedback through general comments, review comments, or inline thread comments
5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent`
### Label versus Macro
- Label (`fix-me`): Requests OpenHands to address the **entire** issue or pull request.
- Macro (`@openhands-agent`): Requests OpenHands to consider only the issue/pull request description and **the specific comment**.
## Advanced Settings
### Add custom repository settings
You can provide custom directions for OpenHands by following the [README for the resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md#providing-custom-instructions).
### Configure custom macro
To customize the default macro (`@openhands-agent`):
1. [Create a repository variable](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) named `OPENHANDS_MACRO`
2. Assign the variable a custom value
## Writing Effective .openhands_instructions Files
The `.openhands_instructions` file is a file that you can put in the root directory of your repository to guide OpenHands in understanding and working with your repository effectively. Here are key tips for writing high-quality instructions:
### Core Principles
1. **Concise but Informative**: Provide a clear, focused overview of the repository that emphasizes the most common actions OpenHands will need to perform.
2. **Repository Structure**: Explain the key directories and their purposes, especially highlighting where different types of code (e.g., frontend, backend) are located.
3. **Development Workflows**: Document the essential commands for:
- Building and setting up the project
- Running tests
- Linting and code quality checks
- Any environment-specific requirements
4. **Testing Guidelines**: Specify:
- Where tests are located
- How to run specific test suites
- Any testing conventions or requirements
### Example Structure
```markdown
# Repository Overview
[Brief description of the project]
## General Setup
- Main build command
- Development environment setup
- Pre-commit checks
## Backend
- Location and structure
- Testing instructions
- Environment requirements
## Frontend
- Setup prerequisites
- Build and test commands
- Environment variables
## Additional Guidelines
- Code style requirements
- Special considerations
- Common workflows
```
For a real-world example, refer to the [OpenHands repository's .openhands_instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands_instructions).

View File

@@ -44,7 +44,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.14-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -54,6 +54,6 @@ 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.14 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -11,16 +11,16 @@
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.14-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.14-nikolaik \
-e LOG_ALL_EVENTS=true \
-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.14
```
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).

View File

@@ -0,0 +1,20 @@
# LiteLLM Proxy
OpenHands supports using the [LiteLLM proxy](https://docs.litellm.ai/docs/proxy/quick_start) to access various LLM providers.
## Configuration
To use LiteLLM proxy with OpenHands, you need to:
1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start))
2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* Enable `Advanced Options`
* `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`)
* `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`)
* `API Key` to your LiteLLM proxy API key
## Supported Models
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy is configured to handle.
Refer to your LiteLLM proxy configuration for the list of available models and their names.

View File

@@ -63,6 +63,7 @@ We have a few guides for running OpenHands with specific model providers:
- [Azure](llms/azure-llms)
- [Google](llms/google-llms)
- [Groq](llms/groq)
- [LiteLLM Proxy](llms/litellm-proxy)
- [OpenAI](llms/openai-llms)
- [OpenRouter](llms/openrouter)

View File

@@ -49,7 +49,7 @@ but seems to work well on most systems.
## All Hands Runtime
The All Hands Runtime is currently in beta. You can request access by joining
the #remote-runtime-limited-beta channel on Slack (see the README for an invite).
the #remote-runtime-limited-beta channel on Slack ([see the README](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-join-our-community) for an invite).
To use the All Hands Runtime, set the following environment variables when
starting OpenHands:
@@ -59,14 +59,14 @@ docker run # ...
-e RUNTIME=remote \
-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_KEEP_RUNTIME_ALIVE="true" \
# ...
```
## Modal Runtime
Our partners at [Modal](https://modal.com/) have also provided a runtime for OpenHands.
To use the Modal Runtime, create an account, and then [create an API key](https://modal.com/settings)
To use the Modal Runtime, create an account, and then [create an API key.](https://modal.com/settings)
You'll then need to set the following environment variables when starting OpenHands:
```bash

2668
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

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.6.2",
"@docusaurus/plugin-content-pages": "^3.6.2",
"@docusaurus/preset-classic": "^3.6.2",
"@docusaurus/theme-mermaid": "^3.6.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.6.2",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.6.3"
},

View File

@@ -76,6 +76,11 @@ const sidebars: SidebarsConfig = {
label: 'Groq',
id: 'usage/llms/groq',
},
{
type: 'doc',
label: 'LiteLLM Proxy',
id: 'usage/llms/litellm-proxy',
},
{
type: 'doc',
label: 'OpenAI',

File diff suppressed because it is too large Load Diff

View File

@@ -87,9 +87,7 @@ class Q20Game:
# others
bingo, anwser_reply = self.judge_winner(response)
if bingo:
return (
'You are bingo! quit now, run: <execute_bash> exit </execute_bash>.\n'
)
return 'You are bingo! Use the "finish" tool to finish the interaction.\n'
if self.curr_turn == self.num_turns - 2:
anwser_reply += " You must guess now, what's it?"
return anwser_reply

View File

@@ -56,6 +56,20 @@ You can update the arguments in the script
./evaluation/aider_bench/scripts/run_infer.sh eval_gpt35_turbo HEAD CodeActAgent 100 1 "1,3,10"
```
### Run Inference on `RemoteRuntime` (experimental)
This is in limited beta. Contact Xingyao over slack if you want to try this out!
```bash
./evaluation/aider_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [eval-num-workers] [eval_ids]
# Example - This runs evaluation on CodeActAgent for 133 instances on aider_bench test set, with 2 workers running in parallel
export ALLHANDS_API_KEY="YOUR-API-KEY"
export RUNTIME=remote
export SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev"
./evaluation/aider_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 133 2
```
## Summarize Results
```bash

View File

@@ -58,6 +58,9 @@ def get_config(
use_host_network=False,
timeout=100,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=1800,
),
# do not mount workspace
workspace_base=None,

View File

@@ -40,7 +40,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}
FILE_EXT_MAP = {

File diff suppressed because one or more lines are too long

View File

@@ -40,7 +40,7 @@ from openhands.utils.async_utils import call_async_from_sync
def codeact_user_response(state: State) -> str:
msg = (
'Please continue working on the task on whatever approach you think is suitable.\n'
'If you think you have completed the SQL, please run the following command: <execute_bash> exit </execute_bash>.\n'
'If you think you have completed the SQL, please finish the interaction using the "finish" tool.\n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP OR USE THE INTERNET TO SOLVE THIS TASK.\n'
)
if state.history:
@@ -54,7 +54,7 @@ def codeact_user_response(state: State) -> str:
# let the agent know that it can give up when it has tried 3 times
return (
msg
+ 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
+ 'If you want to give up, use the "finish" tool to finish the interaction.\n'
)
return msg
@@ -64,7 +64,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}

View File

@@ -0,0 +1,82 @@
# Commit0 Evaluation with OpenHands
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](TBD)).
The evaluation consists of three steps:
1. Environment setup: [install python environment](../README.md#development-environment), [configure LLM config](../README.md#configure-openhands-and-your-llm).
2. [Run Evaluation](#run-inference-on-commit0-instances): Generate a edit patch for each Commit0 Repo, and get the evaluation results
## Setup Environment and LLM Configuration
Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## OpenHands Commit0 Instance-level Docker Support
OpenHands supports using the Commit0 Docker for **[inference](#run-inference-on-commit0-instances).
This is now the default behavior.
## Run Inference on Commit0 Instances
Make sure your Docker daemon is running, and you have ample disk space (at least 200-500GB, depends on the Commit0 set you are running on) for the [instance-level docker image](#openhands-commit0-instance-level-docker-support).
When the `run_infer.sh` script is started, it will automatically pull the `lite` split in Commit0. For example, for instance ID `commit-0/minitorch`, it will try to pull our pre-build docker image `wentingzhao/minitorch` from DockerHub. This image will be used create an OpenHands runtime image where the agent will operate on.
```bash
./evaluation/commit0_bench/scripts/run_infer.sh [repo_split] [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split]
# Example
./evaluation/commit0_bench/scripts/run_infer.sh lite llm.eval_sonnet HEAD CodeActAgent 16 100 8 wentingzhao/commit0_combined test
```
where `model_config` is mandatory, and the rest are optional.
- `repo_split`, e.g. `lite`, is the split of the Commit0 dataset you would like to evaluate on. Available options are `lite`, `all` and each individual repo.
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
LLM settings, as defined in your `config.toml`.
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version you would
like to evaluate. It could also be a release tag like `0.6.2`.
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
to `CodeActAgent`.
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances. By
default, the script evaluates the `lite` split of the Commit0 dataset (16 repos). Note:
in order to use `eval_limit`, you must also set `agent`.
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
default, it is set to 30.
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
default, it is set to 1.
- `dataset`, a huggingface dataset name. e.g. `wentingzhao/commit0_combined`, specifies which dataset to evaluate on.
- `dataset_split`, split for the huggingface dataset. Notice only `test` is supported for Commit0.
Note that the `USE_INSTANCE_IMAGE` environment variable is always set to `true` for Commit0.
Let's say you'd like to run 10 instances using `llm.eval_sonnet` and CodeActAgent,
then your command would be:
```bash
./evaluation/commit0_bench/scripts/run_infer.sh lite llm.eval_sonnet HEAD CodeActAgent 10 30 1 wentingzhao/commit0_combined test
```
### Run Inference on `RemoteRuntime` (experimental)
This is in limited beta. Contact Xingyao over slack if you want to try this out!
```bash
./evaluation/commit0_bench/scripts/run_infer.sh [repo_split] [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split]
# Example - This runs evaluation on CodeActAgent for 10 instances on "wentingzhao/commit0_combined"'s test set, with max 30 iteration per instances, with 1 number of workers running in parallel
ALLHANDS_API_KEY="YOUR-API-KEY" RUNTIME=remote SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev" EVAL_DOCKER_IMAGE_PREFIX="docker.io/wentingzhao" \
./evaluation/commit0_bench/scripts/run_infer.sh lite llm.eval_sonnet HEAD CodeActAgent 10 30 1 wentingzhao/commit0_combined test
```
To clean-up all existing runtime you've already started, run:
```bash
ALLHANDS_API_KEY="YOUR-API-KEY" ./evaluation/commit0_bench/scripts/cleanup_remote_runtime.sh
```
### Specify a subset of tasks to run infer
If you would like to specify a list of tasks you'd like to benchmark on, you just need to pass selected repo through `repo_split` option.

View File

@@ -0,0 +1,606 @@
import asyncio
import json
import os
from collections import Counter
from typing import Any
import pandas as pd
from commit0.harness.constants import SPLIT
from datasets import load_dataset
import openhands.agenthub
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
assert_and_raise,
codeact_user_response,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
SandboxConfig,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
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,
'CodeActCommit0Agent': codeact_user_response,
}
def _get_commit0_workspace_dir_name(instance: pd.Series) -> str:
return instance['repo'].split('/')[1]
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
workspace_dir_name = _get_commit0_workspace_dir_name(instance)
# Prepare instruction
test_cmd = instance['test']['test_cmd']
test_dir = instance['test']['test_dir']
# Instruction based on Anthropic's official trajectory
# https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs
instruction = (
'<uploaded_files>\n'
f'/workspace/{workspace_dir_name}\n'
'</uploaded_files>\n'
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Here is your task:\n\n"
'Here is your task:\n\n'
' You need to complete the implementations for all functions (i.e., those with pass\n'
' statements) and pass the unit tests.\n\n'
' Do not change the names of existing functions or classes, as they may be referenced\n'
' from other code like unit tests, etc.\n\n'
' When you generate code, you must maintain the original formatting of the function\n'
' stubs (such as whitespaces), otherwise we will not able to search/replace blocks\n'
' for code modifications, and therefore you will receive a score of 0 for your generated\n'
' code.'
'\n\n'
'Here is the command to run the unit tests:\n'
'<test_command>\n'
f'{test_cmd} {test_dir}\n'
'</test_command>\n\n'
'Make a local git commit for each agent step for all code changes. If there is not change in current step, do not make a commit.'
)
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\n'
'You SHOULD NEVER attempt to browse the web. '
'</IMPORTANT!>\n'
)
return instruction
# TODO: migrate all swe-bench docker to ghcr.io/openhands
DOCKER_IMAGE_PREFIX = os.environ.get(
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/wentingzhao/'
)
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
def get_instance_docker_image(repo_name: str) -> str:
return (DOCKER_IMAGE_PREFIX.rstrip('/') + '/' + repo_name).lower() + ':v0'
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
# COMMIT0_CONTAINER_IMAGE = 'wentingzhao/'
assert USE_INSTANCE_IMAGE
# We use a different instance image for the each instance of commit0 eval
repo_name = instance['repo'].split('/')[1]
base_container_image = get_instance_docker_image(repo_name)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
# else:
# raise
# base_container_image = SWE_BENCH_CONTAINER_IMAGE
# logger.info(f'Using swe-bench container image: {base_container_image}')
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
base_container_image=base_container_image,
enable_auto_lint=True,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=300,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
),
# 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']
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
)
config.set_agent_config(agent_config)
return config
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
):
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Initialization Fn')
logger.info('-' * 30)
workspace_dir_name = _get_commit0_workspace_dir_name(instance)
obs: CmdOutputObservation
action = CmdRunAction(
command=f'git clone -b commit0_combined https://github.com/{instance["repo"]}.git'
)
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to git clone -b commit0_combined https://github.com/{instance["repo"]}.git: {str(obs)}',
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git checkout -b openhands')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0, f'Failed to git checkout new branch openhands: {str(obs)}'
)
# Install commit0
action = CmdRunAction(command='/root/.cargo/bin/uv pip install commit0')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to install commit0: {str(obs)}',
)
logger.info('-' * 30)
logger.info('END Runtime Initialization Fn')
logger.info('-' * 30)
def complete_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
) -> dict[str, Any]:
"""Complete the runtime for the agent.
This function is called before the runtime is used to run the agent.
If you need to do something in the sandbox to get the correctness metric after
the agent has run, modify this function.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Completion Fn')
logger.info('-' * 30)
obs: CmdOutputObservation
workspace_dir_name = _get_commit0_workspace_dir_name(instance)
action = CmdRunAction(command='git add .')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git add -A: {str(obs)}',
)
action = CmdRunAction(command='git commit -m "openhands edits"')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation)
and (obs.exit_code == 0 or obs.exit_code == 1),
f'Failed to git commit -m "openhands": {str(obs)}',
)
# Generate diff patch compared to base commit, excluding spec.pdf.bz2 files
n_retries = 0
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f"git diff {instance['base_commit']} HEAD -- . ':(exclude)spec.pdf.bz2'"
)
action.timeout = 600 + 100 * n_retries
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
n_retries += 1
if isinstance(obs, CmdOutputObservation):
if obs.exit_code == 0:
git_patch = obs.content.strip()
break
else:
logger.info('Failed to get git diff, retrying...')
sleep_if_should_continue(10)
elif isinstance(obs, ErrorObservation):
logger.error(f'Error occurred: {obs.content}. Retrying...')
sleep_if_should_continue(10)
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
test_dir = instance['test']['test_dir']
action = CmdRunAction(
command=f"{instance['test']['test_cmd']} --json-report --json-report-file=report.json --continue-on-collection-errors {test_dir} > test_output.txt 2>&1"
)
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation),
f'Failed to run test command: {str(obs)}',
)
# Read test output
action = CmdRunAction(command='cat test_output.txt')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation),
f'Failed to read test output: {str(obs)}',
)
test_output = obs.content.strip()
# logger.info(f'Test output: {test_output}')
# Save pytest exit code
action = CmdRunAction(command='echo $?')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to save pytest exit code: {str(obs)}',
)
pytest_exit_code = obs.content.strip()
# logger.info(f'Pytest exit code: {pytest_exit_code}')
# Read the test report
action = CmdRunAction(command='cat report.json')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation),
f'Failed to read test report: {str(obs)}',
)
# Get test IDs from instance
repo_name = instance['repo'].split('/')[1]
repo_name = repo_name.replace('.', '-')
action = CmdRunAction(command=f'commit0 get-tests {repo_name}')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
test_ids = obs.content.strip().split('\n')
try:
report = json.loads(obs.content)
tests = {x['nodeid']: x['call'] for x in report['tests'] if 'call' in x}
# Calculate test statistics
status = []
runtimes = []
no_runs = 0
for test_id in test_ids:
if test_id in tests and tests[test_id] is not None:
status.append(tests[test_id]['outcome'])
runtimes.append(tests[test_id]['duration'])
no_runs += 1
else:
status.append('failed')
runtimes.append(0)
status_counts = Counter(status)
total_runtime = sum(runtimes) if no_runs > 0 else 0
num_passed = status_counts.get('passed', 0) + status_counts.get('xfail', 0)
passed_ratio = num_passed / len(status) if status else 0
eval_result = {
'name': workspace_dir_name,
'sum': total_runtime,
'passed': passed_ratio,
'num_passed': num_passed,
'num_tests': len(test_ids),
}
except json.JSONDecodeError:
logger.error('Failed to parse test report JSON')
eval_result = {
'name': workspace_dir_name,
'sum': 0,
'passed': 0,
'num_passed': 0,
'num_tests': len(test_ids),
}
# Create tarball of workspace
temp_zip = runtime.copy_from(f'/workspace/{workspace_dir_name}')
commit0_dir = os.path.dirname(__file__)
persistent_zip = os.path.join(commit0_dir, f'{workspace_dir_name}.zip')
with open(temp_zip, 'rb') as src, open(persistent_zip, 'wb') as dst:
dst.write(src.read())
zip_file = persistent_zip
return {
'eval_result': eval_result,
'git_patch': git_patch,
'test_output': test_output,
'pytest_exit_code': pytest_exit_code,
'zip_file': zip_file,
}
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
config = get_config(instance, metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)
instruction = get_instruction(instance, metadata)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
# if fatal error, throw EvalError to trigger re-run
if (
state.last_error
and 'fatal error during agent execution' in state.last_error
and 'stuck in a loop' not in state.last_error
):
raise EvalException('Fatal error detected: ' + state.last_error)
# ======= THIS IS Commit0 specific =======
# Get git patch
return_val = complete_runtime(runtime, instance)
eval_result = return_val['eval_result']
git_patch = return_val['git_patch']
test_output = return_val['test_output']
pytest_exit_code = return_val['pytest_exit_code']
zip_file = return_val['zip_file']
repo_name = instance['repo'].split('/')[1]
zip_dest = os.path.join(
metadata.eval_output_dir, 'repos', repo_name, f'{repo_name}.zip'
)
patch_file = os.path.join(
metadata.eval_output_dir, 'repos', repo_name, f'{repo_name}_patch.diff'
)
test_output_file = os.path.join(
metadata.eval_output_dir, 'repos', repo_name, f'{repo_name}_test_output.txt'
)
pytest_exit_code_file = os.path.join(
metadata.eval_output_dir,
'repos',
repo_name,
f'{repo_name}_pytest_exit_code.txt',
)
os.makedirs(os.path.dirname(zip_dest), exist_ok=True)
os.rename(zip_file, zip_dest)
write_targets = [
(patch_file, git_patch),
(test_output_file, test_output),
(pytest_exit_code_file, pytest_exit_code),
]
for write_target in write_targets:
with open(write_target[0], 'w') as f:
f.write(write_target[1])
logger.info(
f'Got evaluation result for repo {instance.instance_id}:\n--------\n{eval_result}\n--------'
)
finally:
runtime.close()
# ==========================================
# ======= Attempt to evaluate the agent's edits =======
# we use eval_infer.sh to evaluate the agent's edits, not here
# because the agent may alter the environment / testcases
test_result = {
'eval_result': eval_result,
}
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
# Save the output
output = EvalOutput(
instance_id=instance.instance_id,
instruction=instruction,
instance=instance.to_dict(),
test_result=test_result,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
)
return output
def commit0_setup(dataset: pd.DataFrame, repo_split: str) -> pd.DataFrame:
"""Setup Commit0 dataset based on split type.
Args:
dataset: Full Commit0 dataset
repo_split: Split type ('all', 'lite' or specific repo name)
Returns:
Filtered dataset based on split type
"""
filtered_dataset = pd.concat(
[
dataset[dataset['repo'].str.split('/').str[1] == repo]
for repo in SPLIT.get(repo_split, [])
]
)
# Drop setup column if it exists
if 'setup' in filtered_dataset.columns:
filtered_dataset = filtered_dataset.drop('setup', axis=1)
# Replace all forward slashes in instance_id with hyphens
filtered_dataset['instance_id'] = filtered_dataset['repo'].str.split('/').str[1]
return filtered_dataset
if __name__ == '__main__':
parser = get_parser()
parser.add_argument(
'--dataset',
type=str,
default='wentingzhao/commit0_combined',
help='dataset to evaluate on, only test split exists for this HF dataset',
)
parser.add_argument(
'--split',
type=str,
default='test',
help='this is the HF dataset split',
)
parser.add_argument(
'--repo-split',
type=str,
default='lite',
help='all, lite, or each repo name',
)
args, _ = parser.parse_known_args()
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
# so we don't need to manage file uploading to OpenHands's repo
dataset = load_dataset(args.dataset, split=args.split)
commit0_datasets = commit0_setup(dataset.to_pandas(), args.repo_split)
logger.info(f'Loaded dataset {args.dataset} with reposplit {args.repo_split}')
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
llm_config.log_completions = True
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
details = {}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
dataset_descrption = (
args.dataset.replace('/', '__') + '-' + args.repo_split.replace('/', '__')
)
metadata = make_metadata(
llm_config,
dataset_descrption,
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
details=details,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
instances = prepare_dataset(commit0_datasets, output_file, args.eval_n_limit)
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
)

View File

@@ -0,0 +1,33 @@
#!/bin/bash
# API base URL
BASE_URL="https://runtime.eval.all-hands.dev"
# Get the list of runtimes
response=$(curl --silent --location --request GET "${BASE_URL}/list" \
--header "X-API-Key: ${ALLHANDS_API_KEY}")
n_runtimes=$(echo $response | jq -r '.total')
echo "Found ${n_runtimes} runtimes. Stopping them..."
runtime_ids=$(echo $response | jq -r '.runtimes | .[].runtime_id')
# Function to stop a single runtime
stop_runtime() {
local runtime_id=$1
local counter=$2
echo "Stopping runtime ${counter}/${n_runtimes}: ${runtime_id}"
curl --silent --location --request POST "${BASE_URL}/stop" \
--header "X-API-Key: ${ALLHANDS_API_KEY}" \
--header "Content-Type: application/json" \
--data-raw "{\"runtime_id\": \"${runtime_id}\"}"
echo
}
export -f stop_runtime
export BASE_URL ALLHANDS_API_KEY n_runtimes
# Use GNU Parallel to stop runtimes in parallel
echo "$runtime_ids" | parallel -j 16 --progress stop_runtime {} {#}
echo "All runtimes have been stopped."

View File

@@ -0,0 +1,125 @@
#!/bin/bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
REPO_SPLIT=$1
MODEL_CONFIG=$2
COMMIT_HASH=$3
AGENT=$4
EVAL_LIMIT=$5
MAX_ITER=$6
NUM_WORKERS=$7
DATASET=$8
SPLIT=$9
N_RUNS=${10}
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
fi
if [ -z "$USE_INSTANCE_IMAGE" ]; then
echo "USE_INSTANCE_IMAGE not specified, use default true"
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 wentingzhao/commit0_combined"
DATASET="wentingzhao/commit0_combined"
fi
if [ -z "$REPO_SPLIT" ]; then
echo "REPO_SPLIT not specified, use default lite"
REPO_SPLIT=0
fi
if [ -z "$SPLIT" ]; then
echo "HF SPLIT not specified, use default test"
SPLIT="test"
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
echo "AGENT: $AGENT"
echo "AGENT_VERSION: $AGENT_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
echo "DATASET: $DATASET"
echo "HF SPLIT: $SPLIT"
echo "REPO SPLIT: $REPO_SPLIT"
# Default to NOT use Hint
if [ -z "$USE_HINT_TEXT" ]; then
export USE_HINT_TEXT=false
fi
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
EVAL_NOTE="$AGENT_VERSION"
# if not using Hint, add -no-hint to the eval note
if [ "$USE_HINT_TEXT" = false ]; then
EVAL_NOTE="$EVAL_NOTE-no-hint"
fi
if [ "$RUN_WITH_BROWSING" = true ]; then
EVAL_NOTE="$EVAL_NOTE-with-browsing"
fi
if [ -n "$EXP_NAME" ]; then
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
fi
function run_eval() {
local eval_note=$1
COMMAND="poetry run python evaluation/commit0_bench/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations $MAX_ITER \
--eval-num-workers $NUM_WORKERS \
--eval-note $eval_note \
--dataset $DATASET \
--split $SPLIT \
--repo-split $REPO_SPLIT"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
}
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
if [ -z "$N_RUNS" ]; then
N_RUNS=1
echo "N_RUNS not specified, use default $N_RUNS"
fi
for i in $(seq 1 $N_RUNS); do
current_eval_note="$EVAL_NOTE-run_$i"
echo "EVAL_NOTE: $current_eval_note"
run_eval $current_eval_note
done
checkout_original_branch

View File

@@ -55,7 +55,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}
@@ -250,9 +250,6 @@ def process_instance(
config = get_config(metadata)
# use a session id for concurrent evaluation
sid = 'ID_' + str(instance.instance_id)
# Setup the logger properly, so you can run
# multi-processing to parallelize the evaluation
if reset_logger:
@@ -284,7 +281,7 @@ def process_instance(
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config, sid=sid)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance.data_files)

View File

@@ -33,7 +33,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have completed the request, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have completed the request, please finish the interaction using the "finish" tool.\n'
}

View File

@@ -87,11 +87,10 @@ def gpqa_codeact_user_response(
msg = (
'Please continue working on the task on whatever approach you think is suitable.\n'
'Feel free to use all tools for calculations and solving the problem, and web-search for finding relevant facts during the process if needed\n'
'If you have finished reporting the answer in the expected format, (and only once that is done), please run the following command to submit: <execute_bash> exit </execute_bash>.\n'
'If you have finished reporting the answer in the expected format, (and only once that is done), please use the "finish" tool to finish the interaction.\n'
'Again you are being told a million times to first report the answer in the requested format (see again below for reference) before exiting. DO NOT EXIT WITHOUT REPORTING THE ANSWER FIRST.\n'
'That is, when you have decided on the answer report in the following format:\n'
f'{ACTION_FORMAT}\n'
'<execute_bash> exit </execute_bash>\n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP TO SOLVE THIS TASK.\n'
)
return msg
@@ -100,7 +99,7 @@ def gpqa_codeact_user_response(
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {'CodeActAgent': gpqa_codeact_user_response}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': '\n\n SUPER IMPORTANT: When you think you have solved the question, first report it back to the user in the requested format. Only once that is done, in the next turn, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': '\n\n SUPER IMPORTANT: When you think you have solved the question, first report it back to the user in the requested format. Only once that is done, in the next turn, please finish the interaction using the "finish" tool.\n'
}
@@ -205,12 +204,11 @@ Additional Instructions:
- Do not try to solve the question in a single step. Break it down into smaller steps.
- You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.
- SUPER IMPORTANT: When you have reported the answer to the user in the requested format, (and only once that is done) in the next turn, please run the following command: <execute_bash> exit </execute_bash>.
- SUPER IMPORTANT: When you have reported the answer to the user in the requested format, (and only once that is done) in the next turn, please finish the interaction using the "finish" tool.
- Again you are being told a million times to first report the answer in the requested format (see again below for reference) before exiting. DO NOT EXIT WITHOUT REPORTING THE ANSWER FIRST.
That is, when you have decided on the answer report in the following format:
{ACTION_FORMAT}
<execute_bash> exit </execute_bash>
Again do not quit without reporting the answer first.
Ok now its time to start solving the question. Good luck!

View File

@@ -23,7 +23,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
```
{
"task_id": "Python/2",
"instruction": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
"instruction": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n",
"metadata": {
"agent_class": "CodeActAgent",
"model_name": "gpt-4",
@@ -38,10 +38,10 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
"id": 27,
"timestamp": "2024-05-22T20:57:24.688651",
"source": "user",
"message": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
"message": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n",
"action": "message",
"args": {
"content": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
"content": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n",
"wait_for_response": false
}
},

View File

@@ -75,7 +75,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}

View File

@@ -16,6 +16,20 @@ Access with browser the above MiniWoB URLs and see if they load correctly.
./evaluation/miniwob/scripts/run_infer.sh llm.claude-35-sonnet-eval
```
### Run Inference on `RemoteRuntime` (experimental)
This is in limited beta. Contact Xingyao over slack if you want to try this out!
```bash
./evaluation/miniwob/scripts/run_infer.sh [model_config] [git-version] [agent] [note] [eval_limit] [num_workers]
# Example - This runs evaluation on BrowsingAgent for 125 instances on miniwob, with 2 workers running in parallel
export ALLHANDS_API_KEY="YOUR-API-KEY"
export RUNTIME=remote
export SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev"
./evaluation/miniwob/scripts/run_infer.sh llm.eval HEAD BrowsingAgent "" 125 2
```
Results will be in `evaluation/evaluation_outputs/outputs/miniwob/`
To calculate the average reward, run:

View File

@@ -23,7 +23,7 @@ if __name__ == '__main__':
data = json.loads(line)
actual_num += 1
total_cost += data['metrics']['accumulated_cost']
total_reward += data['test_result']
total_reward += data['test_result']['reward']
avg_reward = total_reward / total_num
print('Avg Reward: ', avg_reward)

View File

@@ -47,6 +47,7 @@ SUPPORTED_AGENT_CLS = {'BrowsingAgent', 'CodeActAgent'}
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
'BrowsingAgent': 'Continue the task. IMPORTANT: do not talk to the user until you have finished the task',
}
@@ -66,7 +67,9 @@ def get_config(
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,
remote_runtime_init_timeout=1800,
keep_runtime_alive=False,
timeout=120,
),
# do not mount workspace
workspace_base=None,

View File

@@ -33,7 +33,7 @@ echo "MODEL_CONFIG: $MODEL_CONFIG"
EVAL_NOTE="${AGENT_VERSION}_${NOTE}"
COMMAND="poetry run python evaluation/miniwob/run_infer.py \
COMMAND="export PYTHONPATH=evaluation/miniwob:\$PYTHONPATH && poetry run python evaluation/miniwob/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 10 \

View File

@@ -70,7 +70,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': '\nIMPORTANT: When your answer is confirmed by the user to be correct, you can exit using the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'IMPORTANT: When your answer is confirmed by the user to be correct, you can use the "finish" tool to finish the interaction.\n'
}
with open(os.path.join(os.path.dirname(__file__), 'requirements.txt'), 'r') as f:

View File

@@ -55,7 +55,7 @@ Here's an example of the evaluation output for a single task instance:
{
"instance_id": 3,
"repo": "https://github.com/dmlc/dgl",
"instruction": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n",
"instruction": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please finish the interaction using the "finish" tool.\n",
"metadata": {
"agent_class": "CodeActAgent",
"model_name": "gpt-4-1106-preview",
@@ -70,10 +70,10 @@ Here's an example of the evaluation output for a single task instance:
"id": 0,
"timestamp": "2024-05-26T17:40:41.060009",
"source": "user",
"message": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n",
"message": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please finish the interaction using the "finish" tool.\n",
"action": "message",
"args": {
"content": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n",
"content": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please finish the interaction using the "finish" tool.\n",
"wait_for_response": false
}
},

View File

@@ -52,7 +52,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have completed the task, please finish the interaction using the "finish" tool.\n'
}
ID2CONDA = {

View File

@@ -72,7 +72,7 @@ def get_config(
timeout=300,
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,
keep_runtime_alive=False,
),
# do not mount workspace
workspace_base=None,

View File

@@ -1,6 +1,7 @@
import os
import tempfile
import time
from functools import partial
import pandas as pd
from swebench.harness.grading import get_eval_report
@@ -83,7 +84,7 @@ 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,
remote_runtime_init_timeout=3600,
),
# do not mount workspace
workspace_base=None,
@@ -94,13 +95,28 @@ def get_config(instance: pd.Series) -> AppConfig:
def process_instance(
instance: pd.Series,
metadata: EvalMetadata | None = None,
metadata: EvalMetadata,
reset_logger: bool = True,
log_dir: str | None = None,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
Note that this signature differs from the expected input to `run_evaluation`. Use
`functools.partial` to provide optional arguments before passing to the evaluation harness.
Args:
log_dir (str | None, default=None): Path to directory where log files will be written. Must
be provided if `reset_logger` is set.
Raises:
AssertionError: if the `reset_logger` flag is set without a provided log directory.
"""
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
global output_file
log_dir = output_file.replace('.jsonl', '.logs')
assert (
log_dir is not None
), "Can't reset logger without a provided log directory."
os.makedirs(log_dir, exist_ok=True)
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
@@ -127,6 +143,7 @@ def process_instance(
return EvalOutput(
instance_id=instance_id,
test_result=instance['test_result'],
metadata=metadata,
)
runtime = create_runtime(config)
@@ -176,6 +193,7 @@ def process_instance(
return EvalOutput(
instance_id=instance_id,
test_result=instance['test_result'],
metadata=metadata,
)
elif 'APPLY_PATCH_PASS' in apply_patch_output:
logger.info(f'[{instance_id}] {APPLY_PATCH_PASS}:\n{apply_patch_output}')
@@ -245,23 +263,29 @@ def process_instance(
test_output_path = os.path.join(log_dir, 'test_output.txt')
with open(test_output_path, 'w') as f:
f.write(test_output)
_report = get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
'instance_id': instance_id,
},
log_path=test_output_path,
include_tests_status=True,
)
report = _report[instance_id]
logger.info(
f"[{instance_id}] report: {report}\nResult for {instance_id}: resolved: {report['resolved']}"
)
instance['test_result']['report']['resolved'] = report[
'resolved'
]
try:
_report = get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
'instance_id': instance_id,
},
log_path=test_output_path,
include_tests_status=True,
)
report = _report[instance_id]
logger.info(
f"[{instance_id}] report: {report}\nResult for {instance_id}: resolved: {report['resolved']}"
)
instance['test_result']['report']['resolved'] = report[
'resolved'
]
except Exception as e:
logger.error(
f'[{instance_id}] Error when getting eval report: {e}'
)
instance['test_result']['report']['resolved'] = False
instance['test_result']['report']['error_eval'] = True
else:
logger.info(f'[{instance_id}] Error when starting eval:\n{obs.content}')
instance['test_result']['report']['error_eval'] = True
@@ -269,6 +293,7 @@ def process_instance(
return EvalOutput(
instance_id=instance_id,
test_result=instance['test_result'],
metadata=metadata,
)
else:
logger.info(
@@ -336,7 +361,7 @@ if __name__ == '__main__':
if 'model_patch' not in predictions.columns:
predictions['model_patch'] = predictions['test_result'].apply(
lambda x: x['git_patch']
lambda x: x.get('git_patch', '')
)
assert {'instance_id', 'model_patch'}.issubset(
set(predictions.columns)
@@ -355,12 +380,26 @@ if __name__ == '__main__':
output_file = args.input_file.replace('.jsonl', '.swebench_eval.jsonl')
instances = prepare_dataset(predictions, output_file, args.eval_n_limit)
# If possible, load the relevant metadata to avoid issues with `run_evaluation`.
metadata: EvalMetadata | None = None
metadata_filepath = os.path.join(os.path.dirname(args.input_file), 'metadata.json')
if os.path.exists(metadata_filepath):
with open(metadata_filepath, 'r') as metadata_file:
data = metadata_file.read()
metadata = EvalMetadata.model_validate_json(data)
# The evaluation harness constrains the signature of `process_instance_func` but we need to
# pass extra information. Build a new function object to avoid issues with multiprocessing.
process_instance_func = partial(
process_instance, log_dir=output_file.replace('.jsonl', '.logs')
)
run_evaluation(
instances,
metadata=None,
metadata=metadata,
output_file=output_file,
num_workers=args.eval_num_workers,
process_instance_func=process_instance,
process_instance_func=process_instance_func,
)
# Load evaluated predictions & print number of resolved predictions

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
CODEACT_SWE_PROMPT = """Now, you're going to solve this issue on your own. Your terminal session has started and you're in the repository's root directory. You can use any bash commands or the special interface to help you. Edit all the files you need to and run any checks or tests that you want.
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
When you're satisfied with all of the changes you've made, you can run the following command: <execute_bash> exit </execute_bash>.
When you're satisfied with all of the changes you've made, you can use the "finish" tool to finish the interaction.
Note however that you cannot use any interactive session commands (e.g. vim) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
NOTE ABOUT THE EDIT COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!

View File

@@ -36,8 +36,8 @@ from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.runtime.utils.shutdown_listener import sleep_if_should_continue
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
@@ -145,8 +145,8 @@ def get_config(
platform='linux/amd64',
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,
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
),
# do not mount workspace
workspace_base=None,
@@ -534,5 +534,10 @@ if __name__ == '__main__':
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation(
instances, metadata, output_file, args.eval_num_workers, process_instance
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
)

View File

@@ -34,7 +34,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have completed the request, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have completed the request, please finish the interaction using the "finish" tool.\n'
}

View File

@@ -3,9 +3,11 @@ import logging
import multiprocessing as mp
import os
import pathlib
import signal
import subprocess
import time
import traceback
from contextlib import contextmanager
from typing import Any, Awaitable, Callable, TextIO
import pandas as pd
@@ -92,6 +94,27 @@ class EvalException(Exception):
pass
class EvalTimeoutException(Exception):
pass
@contextmanager
def timeout(seconds: int):
def timeout_handler(signum, frame):
raise EvalTimeoutException(f'Function timed out after {seconds} seconds')
# Set up the signal handler
original_handler = signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(seconds)
try:
yield
finally:
# Restore the original handler and disable the alarm
signal.alarm(0)
signal.signal(signal.SIGALRM, original_handler)
def codeact_user_response(
state: State,
encapsulate_solution: bool = False,
@@ -137,7 +160,7 @@ def codeact_user_response(
# let the agent know that it can give up when it has tried 3 times
return (
msg
+ 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
+ 'If you want to give up, use the "finish" tool to finish the interaction.\n'
)
return msg
@@ -280,15 +303,33 @@ def _process_instance_wrapper(
metadata: EvalMetadata,
use_mp: bool,
max_retries: int = 5,
timeout_seconds: int | None = None,
) -> EvalOutput:
"""Wrap the process_instance_func to handle retries and errors.
Retry an instance up to max_retries times if it fails (e.g., due to transient network/runtime issues).
"""
"""Wrap the process_instance_func to handle retries and errors."""
for attempt in range(max_retries + 1):
try:
result = process_instance_func(instance, metadata, use_mp)
if timeout_seconds is not None:
with timeout(timeout_seconds):
result = process_instance_func(instance, metadata, use_mp)
else:
result = process_instance_func(instance, metadata, use_mp)
return result
except EvalTimeoutException as e:
error = f'Timeout after {timeout_seconds} seconds'
stacktrace = traceback.format_exc()
msg = (
'-' * 10
+ '\n'
+ f'Timeout ({timeout_seconds} seconds) in instance [{instance.instance_id}], Stopped evaluation for this instance.'
+ '\n'
+ '-' * 10
)
logger.exception(e)
return EvalOutput(
instance_id=instance.instance_id,
test_result={},
error=error,
)
except Exception as e:
error = str(e)
stacktrace = traceback.format_exc()
@@ -337,6 +378,7 @@ def run_evaluation(
[pd.Series, EvalMetadata, bool], Awaitable[EvalOutput]
],
max_retries: int = 5, # number of retries for each instance
timeout_seconds: int | None = None,
):
use_multiprocessing = num_workers > 1
@@ -346,6 +388,7 @@ def run_evaluation(
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.\n'
)
else:
logger.warning('Running evaluation without metadata.')
logger.info(f'Evaluation started with {num_workers} workers.')
total_instances = len(dataset)
@@ -356,7 +399,14 @@ def run_evaluation(
if use_multiprocessing:
with mp.Pool(num_workers) as pool:
args_iter = (
(process_instance_func, instance, metadata, True, max_retries)
(
process_instance_func,
instance,
metadata,
True,
max_retries,
timeout_seconds,
)
for _, instance in dataset.iterrows()
)
results = pool.imap_unordered(_process_instance_wrapper_mp, args_iter)

View File

@@ -10,7 +10,8 @@
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
"plugin:react-hooks/recommended",
"plugin:@tanstack/query/recommended"
],
"plugins": [
"prettier"

View File

@@ -21,6 +21,11 @@ describe("Empty state", () => {
}));
beforeAll(() => {
vi.mock("@remix-run/react", async (importActual) => ({
...(await importActual<typeof import("@remix-run/react")>()),
useRouteLoaderData: vi.fn(() => ({})),
}));
vi.mock("#/context/socket", async (importActual) => ({
...(await importActual<typeof import("#/context/ws-client-provider")>()),
useWsClient: useWsClientMock,

View File

@@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/feedback-form";
describe("FeedbackForm", () => {
@@ -12,7 +13,9 @@ describe("FeedbackForm", () => {
});
it("should render correctly", () => {
render(<FeedbackForm polarity="positive" onClose={onCloseMock} />);
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
screen.getByLabelText("Email");
screen.getByLabelText("Private");
@@ -23,7 +26,9 @@ describe("FeedbackForm", () => {
});
it("should switch between private and public permissions", async () => {
render(<FeedbackForm polarity="positive" onClose={onCloseMock} />);
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
const privateRadio = screen.getByLabelText("Private");
const publicRadio = screen.getByLabelText("Public");
@@ -40,10 +45,11 @@ describe("FeedbackForm", () => {
});
it("should call onClose when the close button is clicked", async () => {
render(<FeedbackForm polarity="positive" onClose={onCloseMock} />);
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCloseMock).toHaveBeenCalled();
});
});

View File

@@ -16,16 +16,13 @@ vi.mock("../../services/fileService", async () => ({
}));
const renderFileExplorerWithRunningAgentState = () =>
renderWithProviders(
<FileExplorer error={null} isOpen onToggle={() => {}} />,
{
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
},
renderWithProviders(<FileExplorer isOpen onToggle={() => {}} />, {
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
},
},
);
});
describe.skip("FileExplorer", () => {
afterEach(() => {

View File

@@ -1,7 +1,6 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, test, vi, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import * as Remix from "@remix-run/react";
import { UserActions } from "#/components/user-actions";
describe("UserActions", () => {
@@ -9,14 +8,9 @@ describe("UserActions", () => {
const onClickAccountSettingsMock = vi.fn();
const onLogoutMock = vi.fn();
const useFetcherSpy = vi.spyOn(Remix, "useFetcher");
// @ts-expect-error - Only returning the relevant properties for the test
useFetcherSpy.mockReturnValue({ state: "idle" });
afterEach(() => {
onClickAccountSettingsMock.mockClear();
onLogoutMock.mockClear();
useFetcherSpy.mockClear();
});
it("should render", () => {
@@ -111,10 +105,8 @@ describe("UserActions", () => {
expect(onLogoutMock).not.toHaveBeenCalled();
});
it("should display the loading spinner", () => {
// @ts-expect-error - Only returning the relevant properties for the test
useFetcherSpy.mockReturnValue({ state: "loading" });
// FIXME: Spinner now provided through useQuery
it.skip("should display the loading spinner", () => {
render(
<UserActions
onClickAccountSettings={onClickAccountSettingsMock}

View File

@@ -0,0 +1,93 @@
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useRate } from "#/utils/use-rate";
describe("useRate", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should initialize", () => {
const { result } = renderHook(() => useRate());
expect(result.current.items).toHaveLength(0);
expect(result.current.rate).toBeNull();
expect(result.current.lastUpdated).toBeNull();
expect(result.current.isUnderThreshold).toBe(true);
});
it("should handle the case of a single element", () => {
const { result } = renderHook(() => useRate());
act(() => {
result.current.record(123);
});
expect(result.current.items).toHaveLength(1);
expect(result.current.lastUpdated).not.toBeNull();
});
it("should return the difference between the last two elements", () => {
const { result } = renderHook(() => useRate());
vi.setSystemTime(500);
act(() => {
result.current.record(4);
});
vi.advanceTimersByTime(500);
act(() => {
result.current.record(9);
});
expect(result.current.items).toHaveLength(2);
expect(result.current.rate).toBe(5);
expect(result.current.lastUpdated).toBe(1000);
});
it("should update isUnderThreshold after [threshold]ms of no activity", () => {
const { result } = renderHook(() => useRate({ threshold: 500 }));
expect(result.current.isUnderThreshold).toBe(true);
act(() => {
// not sure if fake timers is buggy with intervals,
// but I need to call it twice to register
vi.advanceTimersToNextTimer();
vi.advanceTimersToNextTimer();
});
expect(result.current.isUnderThreshold).toBe(false);
});
it("should return an isUnderThreshold boolean", () => {
const { result } = renderHook(() => useRate({ threshold: 500 }));
vi.setSystemTime(500);
act(() => {
result.current.record(400);
});
act(() => {
result.current.record(1000);
});
expect(result.current.isUnderThreshold).toBe(false);
act(() => {
result.current.record(1500);
});
expect(result.current.isUnderThreshold).toBe(true);
act(() => {
vi.advanceTimersToNextTimer();
vi.advanceTimersToNextTimer();
});
expect(result.current.isUnderThreshold).toBe(false);
});
});

View File

@@ -1,35 +1,153 @@
import { describe, it, test } from "vitest";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createRemixStub } from "@remix-run/testing";
import { screen, waitFor, within } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import userEvent from "@testing-library/user-event";
import MainApp from "#/routes/_oh";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import i18n from "#/i18n";
describe("frontend/routes/_oh", () => {
describe("brand logo", () => {
it.todo("should not do anything if the user is in the main screen");
it.todo(
"should be clickable and redirect to the main screen if the user is not in the main screen",
);
const RemixStub = createRemixStub([{ Component: MainApp, path: "/" }]);
const { userIsAuthenticatedMock, settingsAreUpToDateMock } = vi.hoisted(
() => ({
userIsAuthenticatedMock: vi.fn(),
settingsAreUpToDateMock: vi.fn(),
}),
);
beforeAll(() => {
vi.mock("#/utils/user-is-authenticated", () => ({
userIsAuthenticated: userIsAuthenticatedMock.mockReturnValue(true),
}));
vi.mock("#/services/settings", async (importOriginal) => ({
...(await importOriginal<typeof import("#/services/settings")>()),
settingsAreUpToDate: settingsAreUpToDateMock,
}));
});
describe("user menu", () => {
it.todo("should open the user menu when clicked");
afterEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
describe("logged out", () => {
it.todo("should display a placeholder");
test.todo("the logout option in the user menu should be disabled");
});
it("should render", async () => {
renderWithProviders(<RemixStub />);
await screen.findByTestId("root-layout");
});
describe("logged in", () => {
it.todo("should display the user's avatar");
it.todo("should log the user out when the logout option is clicked");
it("should render the AI config modal if the user is authed", async () => {
// Our mock return value is true by default
renderWithProviders(<RemixStub />);
await screen.findByTestId("ai-config-modal");
});
it("should render the AI config modal if settings are not up-to-date", async () => {
settingsAreUpToDateMock.mockReturnValue(false);
renderWithProviders(<RemixStub />);
await screen.findByTestId("ai-config-modal");
});
it("should not render the AI config modal if the settings are up-to-date", async () => {
settingsAreUpToDateMock.mockReturnValue(true);
renderWithProviders(<RemixStub />);
await waitFor(() => {
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
});
});
describe("config", () => {
it.todo("should open the config modal when clicked");
it.todo(
"should not save the config and close the config modal when the close button is clicked",
it("should capture the user's consent", async () => {
const user = userEvent.setup();
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
);
it.todo(
"should save the config when the save button is clicked and close the modal",
);
it.todo("should warn the user about saving the config when in /app");
renderWithProviders(<RemixStub />);
// The user has not consented to tracking
const consentForm = await screen.findByTestId("user-capture-consent-form");
expect(handleCaptureConsentSpy).not.toHaveBeenCalled();
expect(localStorage.getItem("analytics-consent")).toBeNull();
const submitButton = within(consentForm).getByRole("button", {
name: /confirm preferences/i,
});
await user.click(submitButton);
// The user has now consented to tracking
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(localStorage.getItem("analytics-consent")).toBe("true");
expect(
screen.queryByTestId("user-capture-consent-form"),
).not.toBeInTheDocument();
});
it("should not render the user consent form if the user has already made a decision", async () => {
localStorage.setItem("analytics-consent", "true");
renderWithProviders(<RemixStub />);
await waitFor(() => {
expect(
screen.queryByTestId("user-capture-consent-form"),
).not.toBeInTheDocument();
});
});
it("should render a new project button if a token is set", async () => {
localStorage.setItem("token", "test-token");
const { rerender } = renderWithProviders(<RemixStub />);
await screen.findByTestId("new-project-button");
localStorage.removeItem("token");
rerender(<RemixStub />);
await waitFor(() => {
expect(
screen.queryByTestId("new-project-button"),
).not.toBeInTheDocument();
});
});
// TODO: Move to e2e tests
it.skip("should update the i18n language when the language settings change", async () => {
const changeLanguageSpy = vi.spyOn(i18n, "changeLanguage");
const { rerender } = renderWithProviders(<RemixStub />);
// The default language is English
expect(changeLanguageSpy).toHaveBeenCalledWith("en");
localStorage.setItem("LANGUAGE", "es");
rerender(<RemixStub />);
expect(changeLanguageSpy).toHaveBeenCalledWith("es");
rerender(<RemixStub />);
// The language has not changed, so the spy should not have been called again
expect(changeLanguageSpy).toHaveBeenCalledTimes(2);
});
// FIXME: logoutCleanup has been replaced with a hook
it.skip("should call logoutCleanup after a logout", async () => {
const user = userEvent.setup();
localStorage.setItem("ghToken", "test-token");
// const logoutCleanupSpy = vi.spyOn(LogoutCleanup, "logoutCleanup");
renderWithProviders(<RemixStub />);
const userActions = await screen.findByTestId("user-actions");
const userAvatar = within(userActions).getByTestId("user-avatar");
await user.click(userAvatar);
const logout = within(userActions).getByRole("button", { name: /logout/i });
await user.click(logout);
// expect(logoutCleanupSpy).toHaveBeenCalled();
expect(localStorage.getItem("ghToken")).toBeNull();
});
});

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();
});
});

View File

@@ -0,0 +1,13 @@
import { expect, test } from "vitest";
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
test("extractNextPageFromLink", () => {
const link = `<https://api.github.com/repositories/1300192/issues?page=2>; rel="prev", <https://api.github.com/repositories/1300192/issues?page=4>; rel="next", <https://api.github.com/repositories/1300192/issues?page=515>; rel="last", <https://api.github.com/repositories/1300192/issues?page=1>; rel="first"`;
expect(extractNextPageFromLink(link)).toBe(4);
const noNextLink = `<https://api.github.com/repositories/1300192/issues?page=2>; rel="prev", <https://api.github.com/repositories/1300192/issues?page=1>; rel="first"`;
expect(extractNextPageFromLink(noNextLink)).toBeNull();
const extra = `<https://api.github.com/user/repos?sort=pushed&page=2&per_page=3>; rel="next", <https://api.github.com/user/repos?sort=pushed&page=22&per_page=3>; rel="last"`;
expect(extractNextPageFromLink(extra)).toBe(2);
});

View File

@@ -0,0 +1,44 @@
import posthog from "posthog-js";
import { afterEach, describe, expect, it, vi } from "vitest";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
describe("handleCaptureConsent", () => {
const optInSpy = vi.spyOn(posthog, "opt_in_capturing");
const optOutSpy = vi.spyOn(posthog, "opt_out_capturing");
const hasOptedInSpy = vi.spyOn(posthog, "has_opted_in_capturing");
const hasOptedOutSpy = vi.spyOn(posthog, "has_opted_out_capturing");
afterEach(() => {
vi.clearAllMocks();
});
it("should opt out of of capturing", () => {
handleCaptureConsent(false);
expect(optOutSpy).toHaveBeenCalled();
expect(optInSpy).not.toHaveBeenCalled();
});
it("should opt in to capturing if the user consents", () => {
handleCaptureConsent(true);
expect(optInSpy).toHaveBeenCalled();
expect(optOutSpy).not.toHaveBeenCalled();
});
it("should not opt in to capturing if the user is already opted in", () => {
hasOptedInSpy.mockReturnValueOnce(true);
handleCaptureConsent(true);
expect(optInSpy).not.toHaveBeenCalled();
expect(optOutSpy).not.toHaveBeenCalled();
});
it("should not opt out of capturing if the user is already opted out", () => {
hasOptedOutSpy.mockReturnValueOnce(true);
handleCaptureConsent(false);
expect(optOutSpy).not.toHaveBeenCalled();
expect(optInSpy).not.toHaveBeenCalled();
});
});

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.13.0",
"version": "0.14.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.13.0",
"version": "0.14.2",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8",
@@ -15,6 +15,7 @@
"@remix-run/node": "^2.11.2",
"@remix-run/react": "^2.11.2",
"@remix-run/serve": "^2.11.2",
"@tanstack/react-query": "^5.60.5",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -26,7 +27,7 @@
"isbot": "^5.1.17",
"jose": "^5.9.4",
"monaco-editor": "^0.52.0",
"posthog-js": "^1.176.0",
"posthog-js": "^1.184.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
@@ -50,6 +51,7 @@
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.60.1",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
@@ -69,9 +71,9 @@
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.1",
@@ -5812,6 +5814,143 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.60.1",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.60.1.tgz",
"integrity": "sha512-oCaWtFKa6WwX14fm/Sp486eTFXXgadiDzEYxhM/tiAlM+xzvPwp6ZHgR6sndmvYK+s/jbksDCTLIPS0PCH8L2g==",
"dev": true,
"dependencies": {
"@typescript-eslint/utils": "^8.3.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz",
"integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "8.14.0",
"@typescript-eslint/visitor-keys": "8.14.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz",
"integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz",
"integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "8.14.0",
"@typescript-eslint/visitor-keys": "8.14.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz",
"integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.14.0",
"@typescript-eslint/types": "8.14.0",
"@typescript-eslint/typescript-estree": "8.14.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
}
},
"node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz",
"integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "8.14.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.60.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.5.tgz",
"integrity": "sha512-jiS1aC3XI3BJp83ZiTuDLerTmn9P3U95r6p+6/SNauLJaYxfIC4dMuWygwnBHIZxjn2zJqEpj3nysmPieoxfPQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.60.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.60.5.tgz",
"integrity": "sha512-M77bOsPwj1wYE56gk7iJvxGAr4IC12NWdIDhT+Eo8ldkWRHMvIR8I/rufIvT1OXoV/bl7EECwuRuMlxxWtvW2Q==",
"dependencies": {
"@tanstack/query-core": "5.60.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -8161,38 +8300,6 @@
"node": ">=6"
}
},
"node_modules/deep-equal": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
"integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
"dev": true,
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.5",
"es-get-iterator": "^1.1.3",
"get-intrinsic": "^1.2.2",
"is-arguments": "^1.1.1",
"is-array-buffer": "^3.0.2",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"isarray": "^2.0.5",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.5.1",
"side-channel": "^1.0.4",
"which-boxed-primitive": "^1.0.2",
"which-collection": "^1.0.1",
"which-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -8593,26 +8700,6 @@
"node": ">= 0.4"
}
},
"node_modules/es-get-iterator": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
"integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
"has-symbols": "^1.0.3",
"is-arguments": "^1.1.1",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.7",
"isarray": "^2.0.5",
"stop-iteration-iterator": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-iterator-helpers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz",
@@ -9062,12 +9149,12 @@
}
},
"node_modules/eslint-plugin-jsx-a11y": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz",
"integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==",
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
"dev": true,
"dependencies": {
"aria-query": "~5.1.3",
"aria-query": "^5.3.2",
"array-includes": "^3.1.8",
"array.prototype.flatmap": "^1.3.2",
"ast-types-flow": "^0.0.8",
@@ -9075,14 +9162,13 @@
"axobject-query": "^4.1.0",
"damerau-levenshtein": "^1.0.8",
"emoji-regex": "^9.2.2",
"es-iterator-helpers": "^1.0.19",
"hasown": "^2.0.2",
"jsx-ast-utils": "^3.3.5",
"language-tags": "^1.0.9",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"safe-regex-test": "^1.0.3",
"string.prototype.includes": "^2.0.0"
"string.prototype.includes": "^2.0.1"
},
"engines": {
"node": ">=4.0"
@@ -9092,12 +9178,12 @@
}
},
"node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"dependencies": {
"deep-equal": "^2.0.5"
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
@@ -9153,9 +9239,9 @@
}
},
"node_modules/eslint-plugin-react": {
"version": "7.37.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz",
"integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==",
"version": "7.37.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz",
"integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.8",
@@ -9163,7 +9249,7 @@
"array.prototype.flatmap": "^1.3.2",
"array.prototype.tosorted": "^1.1.4",
"doctrine": "^2.1.0",
"es-iterator-helpers": "^1.0.19",
"es-iterator-helpers": "^1.1.0",
"estraverse": "^5.3.0",
"hasown": "^2.0.2",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
@@ -18934,22 +19020,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -19749,9 +19819,9 @@
"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==",
"version": "1.184.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.184.1.tgz",
"integrity": "sha512-q/1Kdard5SZnL2smrzeKcD+RuUi2PnbidiN4D3ThK20bNrhy5Z2heIy9SnRMvEiARY5lcQ7zxmDCAKPBKGSOtQ==",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
@@ -22698,18 +22768,6 @@
"integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
"dev": true
},
"node_modules/stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
"integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
"dev": true,
"dependencies": {
"internal-slot": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.13.0",
"version": "0.14.2",
"private": true,
"type": "module",
"engines": {
@@ -14,6 +14,7 @@
"@remix-run/node": "^2.11.2",
"@remix-run/react": "^2.11.2",
"@remix-run/serve": "^2.11.2",
"@tanstack/react-query": "^5.60.5",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -25,7 +26,7 @@
"isbot": "^5.1.17",
"jose": "^5.9.4",
"monaco-editor": "^0.52.0",
"posthog-js": "^1.176.0",
"posthog-js": "^1.184.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
@@ -76,6 +77,7 @@
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.60.1",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
@@ -95,9 +97,9 @@
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.1",

View File

@@ -26,7 +26,7 @@ export default defineConfig({
/* 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",
baseURL: "http://localhost:3001/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
@@ -72,8 +72,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev:mock -- --port 3000",
url: "http://127.0.0.1:3000",
command: "npm run dev:mock -- --port 3001",
url: "http://localhost:3001/",
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -1,4 +1,5 @@
{
"APP_MODE": "oss",
"GITHUB_CLIENT_ID": ""
}
"GITHUB_CLIENT_ID": "",
"POSTHOG_CLIENT_KEY": "phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA"
}

View File

@@ -27,82 +27,19 @@ export const isGitHubErrorReponse = <T extends object | Array<unknown>>(
*/
export const retrieveGitHubUserRepositories = async (
token: string,
per_page = 30,
page = 1,
per_page = 30,
): Promise<Response> => {
const url = new URL("https://api.github.com/user/repos");
url.searchParams.append("sort", "pushed"); // sort by most recently pushed
url.searchParams.append("per_page", per_page.toString());
url.searchParams.append("page", page.toString());
url.searchParams.append("per_page", per_page.toString());
return fetch(url.toString(), {
headers: generateGitHubAPIHeaders(token),
});
};
/**
* Given a GitHub token, retrieves all repositories of the authenticated user
* @param token The GitHub token
* @returns A list of repositories or an error response
*/
export const retrieveAllGitHubUserRepositories = async (
token: string,
): Promise<GitHubRepository[] | GitHubErrorReponse> => {
const repositories: GitHubRepository[] = [];
// Fetch the first page to extract the last page number and get the first batch of data
const firstPageResponse = await retrieveGitHubUserRepositories(token, 100, 1);
if (!firstPageResponse.ok) {
return {
message: "Failed to fetch repositories",
documentation_url:
"https://docs.github.com/rest/reference/repos#list-repositories-for-the-authenticated-user",
status: firstPageResponse.status,
};
}
const firstPageData = await firstPageResponse.json();
repositories.push(...firstPageData);
// Check for pagination and extract the last page number
const link = firstPageResponse.headers.get("link");
const lastPageMatch = link?.match(/page=(\d+)>; rel="last"/);
const lastPage = lastPageMatch ? parseInt(lastPageMatch[1], 10) : 1;
// If there is only one page, return the fetched repositories
if (lastPage === 1) {
return repositories;
}
// Create an array of promises for the remaining pages
const promises = [];
for (let page = 2; page <= lastPage; page += 1) {
promises.push(retrieveGitHubUserRepositories(token, 100, page));
}
// Fetch all pages in parallel
const responses = await Promise.all(promises);
for (const response of responses) {
if (response.ok) {
// TODO: Is there a way to avoid using await within a loop?
// eslint-disable-next-line no-await-in-loop
const data = await response.json();
repositories.push(...data);
} else {
return {
message: "Failed to fetch repositories",
documentation_url:
"https://docs.github.com/rest/reference/repos#list-repositories-for-the-authenticated-user",
status: response.status,
};
}
}
return repositories;
};
/**
* Given a GitHub token, retrieves the authenticated user
* @param token The GitHub token
@@ -114,6 +51,11 @@ export const retrieveGitHubUser = async (
const response = await fetch("https://api.github.com/user", {
headers: generateGitHubAPIHeaders(token),
});
if (!response.ok) {
throw new Error("Failed to retrieve user data");
}
const data = await response.json();
if (!isGitHubErrorReponse(data)) {
@@ -149,5 +91,9 @@ export const retrieveLatestGitHubCommit = async (
headers: generateGitHubAPIHeaders(token),
});
if (!response.ok) {
throw new Error("Failed to retrieve latest commit");
}
return response.json();
};

View File

@@ -1,5 +1,4 @@
import { request } from "#/services/api";
import { cache } from "#/utils/cache";
import {
SaveFileSuccessResponse,
FileUploadSuccessResponse,
@@ -8,6 +7,7 @@ import {
GitHubAccessTokenResponse,
ErrorResponse,
GetConfigResponse,
GetVSCodeUrlResponse,
} from "./open-hands.types";
class OpenHands {
@@ -16,13 +16,13 @@ class OpenHands {
* @returns List of models available
*/
static async getModels(): Promise<string[]> {
const cachedData = cache.get<string[]>("models");
if (cachedData) return cachedData;
const response = await fetch("/api/options/models");
const data = await request("/api/options/models");
cache.set("models", data);
if (!response.ok) {
throw new Error("Failed to fetch models");
}
return data;
return response.json();
}
/**
@@ -30,13 +30,13 @@ class OpenHands {
* @returns List of agents available
*/
static async getAgents(): Promise<string[]> {
const cachedData = cache.get<string[]>("agents");
if (cachedData) return cachedData;
const response = await fetch("/api/options/agents");
const data = await request(`/api/options/agents`);
cache.set("agents", data);
if (!response.ok) {
throw new Error("Failed to fetch agents");
}
return data;
return response.json();
}
/**
@@ -44,23 +44,23 @@ class OpenHands {
* @returns List of security analyzers available
*/
static async getSecurityAnalyzers(): Promise<string[]> {
const cachedData = cache.get<string[]>("agents");
if (cachedData) return cachedData;
const response = await fetch("/api/options/security-analyzers");
const data = await request(`/api/options/security-analyzers`);
cache.set("security-analyzers", data);
if (!response.ok) {
throw new Error("Failed to fetch security analyzers");
}
return data;
return response.json();
}
static async getConfig(): Promise<GetConfigResponse> {
const cachedData = cache.get<GetConfigResponse>("config");
if (cachedData) return cachedData;
const response = await fetch("/config.json");
const data = await request("/config.json");
cache.set("config", data);
if (!response.ok) {
throw new Error("Failed to fetch config");
}
return data;
return response.json();
}
/**
@@ -68,10 +68,21 @@ class OpenHands {
* @param path Path to list files from
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(path?: string): Promise<string[]> {
let url = "/api/list-files";
if (path) url += `?path=${encodeURIComponent(path)}`;
return request(url);
static async getFiles(token: string, path?: string): Promise<string[]> {
const url = new URL("/api/list-files", window.location.origin);
if (path) url.searchParams.append("path", path);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to fetch files");
}
return response.json();
}
/**
@@ -79,9 +90,21 @@ class OpenHands {
* @param path Full path of the file to retrieve
* @returns Content of the file
*/
static async getFile(path: string): Promise<string> {
const url = `/api/select-file?file=${encodeURIComponent(path)}`;
const data = await request(url);
static async getFile(token: string, path: string): Promise<string> {
const url = new URL("/api/select-file", window.location.origin);
url.searchParams.append("file", path);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to fetch file");
}
const data = await response.json();
return data.code;
}
@@ -92,16 +115,32 @@ class OpenHands {
* @returns Success message or error message
*/
static async saveFile(
token: string,
path: string,
content: string,
): Promise<SaveFileSuccessResponse | ErrorResponse> {
return request(`/api/save-file`, {
): Promise<SaveFileSuccessResponse> {
const response = await fetch("/api/save-file", {
method: "POST",
body: JSON.stringify({ filePath: path, content }),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to save file");
}
const data = (await response.json()) as
| SaveFileSuccessResponse
| ErrorResponse;
if ("error" in data) {
throw new Error(data.error);
}
return data;
}
/**
@@ -110,15 +149,78 @@ class OpenHands {
* @returns Success message or error message
*/
static async uploadFiles(
file: File[],
): Promise<FileUploadSuccessResponse | ErrorResponse> {
token: string,
files: File[],
): Promise<FileUploadSuccessResponse> {
const formData = new FormData();
file.forEach((f) => formData.append("files", f));
files.forEach((file) => formData.append("files", file));
return request(`/api/upload-files`, {
const response = await fetch("/api/upload-files", {
method: "POST",
body: formData,
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to upload files");
}
const data = (await response.json()) as
| FileUploadSuccessResponse
| ErrorResponse;
if ("error" in data) {
throw new Error(data.error);
}
return data;
}
/**
* Send feedback to the server
* @param data Feedback data
* @returns The stored feedback data
*/
static async submitFeedback(
token: string,
feedback: Feedback,
): Promise<FeedbackResponse> {
const response = await fetch("/api/submit-feedback", {
method: "POST",
body: JSON.stringify(feedback),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error("Failed to submit feedback");
}
return response.json();
}
/**
* Authenticate with GitHub token
* @returns Response with authentication status and user info if successful
*/
static async authenticate(
gitHubToken: string,
appMode: GetConfigResponse["APP_MODE"],
): Promise<boolean> {
if (appMode === "oss") return true;
const response = await fetch("/api/authenticate", {
method: "POST",
headers: {
"X-GitHub-Token": gitHubToken,
},
});
return response.ok;
}
/**
@@ -130,21 +232,6 @@ class OpenHands {
return response.blob();
}
/**
* Send feedback to the server
* @param data Feedback data
* @returns The stored feedback data
*/
static async submitFeedback(data: Feedback): Promise<FeedbackResponse> {
return request(`/api/submit-feedback`, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
}
/**
* @param code Code provided by GitHub
* @returns GitHub access token
@@ -152,27 +239,33 @@ class OpenHands {
static async getGitHubAccessToken(
code: string,
): Promise<GitHubAccessTokenResponse> {
return request(`/api/github/callback`, {
const response = await fetch("/api/github/callback", {
method: "POST",
body: JSON.stringify({ code }),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to get GitHub access token");
}
return response.json();
}
/**
* Authenticate with GitHub token
* @returns Response with authentication status and user info if successful
* Get the VSCode URL
* @returns VSCode URL
*/
static async authenticate(): Promise<Response> {
return request(
`/api/authenticate`,
{
method: "POST",
},
true,
);
static async getVSCodeUrl(): Promise<GetVSCodeUrlResponse> {
return request(`/api/vscode-url`, {}, false, false, 1);
}
static async getRuntimeId(): Promise<{ runtime_id: string }> {
const data = await request("/api/conversation");
return data;
}
}

View File

@@ -43,5 +43,11 @@ export interface Feedback {
export interface GetConfigResponse {
APP_MODE: "saas" | "oss";
GITHUB_CLIENT_ID: string | null;
GITHUB_CLIENT_ID: string;
POSTHOG_CLIENT_KEY: string;
}
export interface GetVSCodeUrlResponse {
vscode_url: string | null;
error?: string;
}

View File

@@ -0,0 +1,57 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g filter="url(#filter0_d)">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.5723C72.4869 100.189 74.2828 100.15 75.8725 99.3807L96.4604 89.4231C98.624 88.3771 100 86.1762 100 83.7616V16.2392C100 13.8247 98.624 11.6238 96.4604 10.5774L75.8725 0.619067C73.7862 -0.389991 71.3446 -0.142885 69.5135 1.19527C69.252 1.38636 69.0028 1.59985 68.769 1.83502L29.3551 37.9795L12.1872 24.88C10.5891 23.6607 8.35365 23.7606 6.86938 25.1178L1.36302 30.1525C-0.452603 31.8127 -0.454583 34.6837 1.35854 36.3466L16.2471 50.0001L1.35854 63.6536C-0.454583 65.3164 -0.452603 68.1876 1.36302 69.8477L6.86938 74.8824C8.35365 76.2395 10.5891 76.34 12.1872 75.1201L29.3551 62.0207L68.769 98.1651C69.3925 98.7923 70.1246 99.2645 70.9119 99.5723ZM75.0152 27.1813L45.1092 50.0001L75.0152 72.8189V27.1813Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M96.4614 10.593L75.8567 0.62085C73.4717 -0.533437 70.6215 -0.0465506 68.7498 1.83492L1.29834 63.6535C-0.515935 65.3164 -0.513852 68.1875 1.30281 69.8476L6.8125 74.8823C8.29771 76.2395 10.5345 76.339 12.1335 75.1201L93.3604 13.18C96.0854 11.102 100 13.0557 100 16.4939V16.2535C100 13.84 98.6239 11.64 96.4614 10.593Z" fill="#D9D9D9"/>
<g filter="url(#filter1_d)">
<path d="M96.4614 89.4074L75.8567 99.3797C73.4717 100.534 70.6215 100.047 68.7498 98.1651L1.29834 36.3464C-0.515935 34.6837 -0.513852 31.8125 1.30281 30.1524L6.8125 25.1177C8.29771 23.7605 10.5345 23.6606 12.1335 24.88L93.3604 86.8201C96.0854 88.8985 100 86.9447 100 83.5061V83.747C100 86.1604 98.6239 88.3603 96.4614 89.4074Z" fill="#E6E6E6"/>
</g>
<g filter="url(#filter2_d)">
<path d="M75.8578 99.3807C73.4721 100.535 70.6219 100.047 68.75 98.1651C71.0564 100.483 75 98.8415 75 95.5631V4.43709C75 1.15852 71.0565 -0.483493 68.75 1.83492C70.6219 -0.0467614 73.4721 -0.534276 75.8578 0.618963L96.4583 10.5773C98.6229 11.6237 100 13.8246 100 16.2391V83.7616C100 86.1762 98.6229 88.3761 96.4583 89.4231L75.8578 99.3807Z" fill="white"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.25">
<path style="mix-blend-mode:overlay" opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M70.8508 99.5723C72.4258 100.189 74.2218 100.15 75.8115 99.3807L96.4 89.4231C98.5635 88.3771 99.9386 86.1762 99.9386 83.7616V16.2391C99.9386 13.8247 98.5635 11.6239 96.4 10.5774L75.8115 0.618974C73.7252 -0.390085 71.2835 -0.142871 69.4525 1.19518C69.1909 1.38637 68.9418 1.59976 68.7079 1.83493L29.2941 37.9795L12.1261 24.88C10.528 23.6606 8.2926 23.7605 6.80833 25.1177L1.30198 30.1524C-0.51354 31.8126 -0.515625 34.6837 1.2975 36.3465L16.186 50L1.2975 63.6536C-0.515625 65.3164 -0.51354 68.1875 1.30198 69.8476L6.80833 74.8824C8.2926 76.2395 10.528 76.339 12.1261 75.1201L29.2941 62.0207L68.7079 98.1651C69.3315 98.7923 70.0635 99.2645 70.8508 99.5723ZM74.9542 27.1812L45.0481 50L74.9542 72.8188V27.1812Z" fill="url(#paint0_linear)"/>
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_d" x="-6.25" y="-4.16667" width="112.5" height="112.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="2.08333"/>
<feGaussianBlur stdDeviation="3.125"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="-8.39436" y="15.6951" width="116.728" height="92.6376" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter2_d" x="60.4167" y="-8.33346" width="47.9167" height="116.667" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<linearGradient id="paint0_linear" x1="49.939" y1="-5.19792e-05" x2="49.939" y2="100.001" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0">
<rect width="100" height="100" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,4 +1,3 @@
import { useFetcher } from "@remix-run/react";
import { ModalBackdrop } from "./modals/modal-backdrop";
import ModalBody from "./modals/ModalBody";
import ModalButton from "./buttons/ModalButton";
@@ -6,15 +5,31 @@ import {
BaseModalTitle,
BaseModalDescription,
} from "./modals/confirmation-modals/BaseModal";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
export function AnalyticsConsentFormModal() {
const fetcher = useFetcher({ key: "set-consent" });
interface AnalyticsConsentFormModalProps {
onClose: () => void;
}
export function AnalyticsConsentFormModal({
onClose,
}: AnalyticsConsentFormModalProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const analytics = formData.get("analytics") === "on";
handleCaptureConsent(analytics);
localStorage.setItem("analytics-consent", analytics.toString());
onClose();
};
return (
<ModalBackdrop>
<fetcher.Form
method="POST"
action="/set-consent"
<form
data-testid="user-capture-consent-form"
onSubmit={handleSubmit}
className="flex flex-col gap-2"
>
<ModalBody>
@@ -36,7 +51,7 @@ export function AnalyticsConsentFormModal() {
className="bg-primary text-white w-full hover:opacity-80"
/>
</ModalBody>
</fetcher.Form>
</form>
</ModalBackdrop>
);
}

View File

@@ -18,6 +18,7 @@ interface ChatInputProps {
onBlur?: () => void;
onImagePaste?: (files: File[]) => void;
className?: React.HTMLAttributes<HTMLDivElement>["className"];
buttonClassName?: React.HTMLAttributes<HTMLButtonElement>["className"];
}
export function ChatInput({
@@ -35,6 +36,7 @@ export function ChatInput({
onBlur,
onImagePaste,
className,
buttonClassName,
}: ChatInputProps) {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
@@ -100,7 +102,7 @@ export function ChatInput({
return (
<div
data-testid="chat-input"
className="flex items-end justify-end grow gap-1 min-h-6"
className="flex items-end justify-end grow gap-1 min-h-6 w-full"
>
<TextareaAutosize
ref={textareaRef}
@@ -128,7 +130,7 @@ export function ChatInput({
)}
/>
{showButton && (
<>
<div className={buttonClassName}>
{button === "submit" && (
<button
aria-label="Send"
@@ -152,7 +154,7 @@ export function ChatInput({
<div className="w-[10px] h-[10px] bg-white" />
</button>
)}
</>
</div>
)}
</div>
);

View File

@@ -21,14 +21,23 @@ 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";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import OpenHands from "#/api/open-hands";
import { downloadWorkspace } from "#/utils/download-workspace";
import { SuggestionItem } from "./suggestion-item";
import { useAuth } from "#/context/auth-context";
const isErrorMessage = (
message: Message | ErrorMessage,
): message is ErrorMessage => "error" in message;
export function ChatInterface() {
const { send } = useWsClient();
const { gitHubToken } = useAuth();
const { send, status, isLoadingMessages } = useWsClient();
const dispatch = useDispatch();
const scrollRef = React.useRef<HTMLDivElement>(null);
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
@@ -42,6 +51,24 @@ export function ChatInterface() {
>("positive");
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
const [messageToSend, setMessageToSend] = React.useState<string | null>(null);
const [isDownloading, setIsDownloading] = React.useState(false);
React.useEffect(() => {
if (status === WsClientProviderStatus.ACTIVE) {
try {
OpenHands.getRuntimeId().then(({ runtime_id }) => {
// eslint-disable-next-line no-console
console.log(
"Runtime ID: %c%s",
"background: #444; color: #ffeb3b; font-weight: bold; padding: 2px 4px; border-radius: 4px;",
runtime_id,
);
});
} catch (e) {
console.warn("Runtime ID not available in this environment");
}
}
}, [status]);
const handleSendMessage = async (content: string, files: File[]) => {
posthog.capture("user_message_sent", {
@@ -72,6 +99,17 @@ export function ChatInterface() {
setFeedbackPolarity(polarity);
};
const handleDownloadWorkspace = async () => {
setIsDownloading(true);
try {
await downloadWorkspace();
} catch (error) {
// TODO: Handle error
} finally {
setIsDownloading(false);
}
};
return (
<div className="h-full flex flex-col justify-between">
{messages.length === 0 && (
@@ -101,29 +139,64 @@ export function ChatInterface() {
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
>
{messages.map((message, index) =>
isErrorMessage(message) ? (
<ErrorMessage
key={index}
id={message.id}
message={message.message}
/>
) : (
<ChatMessage
key={index}
type={message.sender}
message={message.content}
>
{message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
{messages.length - 1 === index &&
message.sender === "assistant" &&
curAgentState === AgentState.AWAITING_USER_CONFIRMATION && (
<ConfirmationButtons />
{isLoadingMessages && (
<div className="flex justify-center">
<div className="w-6 h-6 border-2 border-t-[4px] border-primary-500 rounded-full animate-spin" />
</div>
)}
{!isLoadingMessages &&
messages.map((message, index) =>
isErrorMessage(message) ? (
<ErrorMessage
key={index}
id={message.id}
message={message.message}
/>
) : (
<ChatMessage
key={index}
type={message.sender}
message={message.content}
>
{message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
</ChatMessage>
),
{messages.length - 1 === index &&
message.sender === "assistant" &&
curAgentState === AgentState.AWAITING_USER_CONFIRMATION && (
<ConfirmationButtons />
)}
</ChatMessage>
),
)}
{(curAgentState === AgentState.AWAITING_USER_INPUT ||
curAgentState === AgentState.FINISHED) && (
<div className="flex flex-col gap-2 mb-2">
{gitHubToken ? (
<SuggestionItem
suggestion={{
label: "Push to GitHub",
value:
"Please push the changes to GitHub and open a pull request.",
}}
onClick={(value) => {
handleSendMessage(value, []);
}}
/>
) : (
<SuggestionItem
suggestion={{
label: !isDownloading
? "Download .zip"
: "Downloading, please wait...",
value: "Download .zip",
}}
onClick={handleDownloadWorkspace}
/>
)}
</div>
)}
</div>

View File

@@ -1,12 +1,11 @@
import { IoLockClosed } from "react-icons/io5";
import { useRouteLoaderData } from "@remix-run/react";
import React from "react";
import { useSelector } from "react-redux";
import AgentControlBar from "./AgentControlBar";
import AgentStatusBar from "./AgentStatusBar";
import { ProjectMenuCard } from "./project-menu/ProjectMenuCard";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import { clientLoader as appClientLoader } from "#/routes/_oh.app";
import { isGitHubErrorReponse } from "#/api/github";
import { useAuth } from "#/context/auth-context";
import { RootState } from "#/store";
interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
@@ -19,22 +18,21 @@ export function Controls({
showSecurityLock,
lastCommitData,
}: ControlsProps) {
const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const appData = useRouteLoaderData<typeof appClientLoader>("routes/_oh.app");
const { gitHubToken } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initalQuery,
);
const projectMenuCardData = React.useMemo(
() =>
rootData?.user &&
!isGitHubErrorReponse(rootData.user) &&
appData?.repo &&
lastCommitData
selectedRepository && lastCommitData
? {
avatar: rootData.user.avatar_url,
repoName: appData.repo,
repoName: selectedRepository,
lastCommit: lastCommitData,
avatar: null, // TODO: fetch repo avatar
}
: null,
[rootData, appData, lastCommitData],
[selectedRepository, lastCommitData],
);
return (
@@ -55,7 +53,7 @@ export function Controls({
</div>
<ProjectMenuCard
isConnectedToGitHub={!!rootData?.ghToken}
isConnectedToGitHub={!!gitHubToken}
githubData={projectMenuCardData}
/>
</div>

View File

@@ -1,12 +1,6 @@
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,
@@ -14,26 +8,28 @@ import {
} 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,
clearInitialQuery,
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";
import { generateAgentStateChangeEvent } from "#/services/agentStateService";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
import { useAuth } from "#/context/auth-context";
import { useEndSession } from "#/hooks/use-end-session";
import { useUserPrefs } from "#/context/user-prefs-context";
interface ServerError {
error: boolean | string;
@@ -47,44 +43,48 @@ const isErrorObservation = (data: object): data is ErrorObservation =>
"observation" in data && data.observation === "error";
export function EventHandler({ children }: React.PropsWithChildren) {
const { setToken, gitHubToken } = useAuth();
const { settings } = useUserPrefs();
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(
const { files, importedProjectZip, initialQuery } = useSelector(
(state: RootState) => state.initalQuery,
);
const { ghToken, repo } = useLoaderData<typeof appClientLoader>();
const initialQueryRef = React.useRef<string | null>(
store.getState().initalQuery.initialQuery,
const endSession = useEndSession();
// FIXME: Bad practice - should be handled with state
const { selectedRepository } = useSelector(
(state: RootState) => state.initalQuery,
);
const { data: user } = useGitHubUser();
const { mutate: uploadFiles } = useUploadFiles();
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;
if (user && !isGitHubErrorReponse(user)) return user.id;
return null;
}, [data?.user]);
const userSettings = getSettings();
}, [user]);
React.useEffect(() => {
if (!events.length) {
return;
}
const event = events[events.length - 1];
if (event.token) {
fetcher.submit({ token: event.token as string }, { method: "post" });
if (event.token && typeof event.token === "string") {
setToken(event.token);
return;
}
if (isServerError(event)) {
if (event.error_code === 401) {
toast.error("Session expired.");
fetcher.submit({}, { method: "POST", action: "/end-session" });
endSession();
return;
}
@@ -96,6 +96,14 @@ export function EventHandler({ children }: React.PropsWithChildren) {
return;
}
if (event.type === "error") {
const message: string = `${event.message}`;
if (message.startsWith("Agent reached maximum")) {
// We set the agent state to paused here - if the user clicks resume, it auto updates the max iterations
send(generateAgentStateChangeEvent(AgentState.PAUSED));
}
}
if (isErrorObservation(event)) {
dispatch(
addErrorMessage({
@@ -103,9 +111,7 @@ export function EventHandler({ children }: React.PropsWithChildren) {
message: event.message,
}),
);
return;
}
handleAssistantMessage(event);
}, [events.length]);
React.useEffect(() => {
@@ -113,13 +119,12 @@ export function EventHandler({ children }: React.PropsWithChildren) {
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.`;
if (gitHubToken && selectedRepository) {
send(getCloneRepoCommand(gitHubToken, selectedRepository));
additionalInfo = `Repository ${selectedRepository} 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
@@ -134,7 +139,7 @@ export function EventHandler({ children }: React.PropsWithChildren) {
sendInitialQuery(initialQuery, files);
}
dispatch(clearFiles()); // reset selected files
initialQueryRef.current = null;
dispatch(clearInitialQuery()); // reset initial query
}
}
@@ -154,35 +159,35 @@ export function EventHandler({ children }: React.PropsWithChildren) {
}, [status]);
React.useEffect(() => {
if (runtimeActive && userId && ghToken) {
if (runtimeActive && userId && gitHubToken) {
// Export if the user valid, this could happen mid-session so it is handled here
send(getGitHubTokenCommand(ghToken));
send(getGitHubTokenCommand(gitHubToken));
}
}, [userId, ghToken, runtimeActive]);
}, [userId, gitHubToken, 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.");
}
}
})();
if (runtimeActive && importedProjectZip) {
const blob = base64ToBlob(importedProjectZip);
const file = new File([blob], "imported-project.zip", {
type: blob.type,
});
uploadFiles(
{ files: [file] },
{
onError: () => {
toast.error("Failed to upload project files.");
},
},
);
dispatch(setImportedProjectZip(null));
}
}, [runtimeActive, importedProjectZip]);
React.useEffect(() => {
if (userSettings.LLM_API_KEY) {
if (settings.LLM_API_KEY) {
posthog.capture("user_activated");
}
}, [userSettings.LLM_API_KEY]);
}, [settings.LLM_API_KEY]);
return children;
}

View File

@@ -2,7 +2,7 @@ import React from "react";
import hotToast from "react-hot-toast";
import ModalButton from "./buttons/ModalButton";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { useSubmitFeedback } from "#/hooks/mutation/use-submit-feedback";
const FEEDBACK_VERSION = "1.0";
const VIEWER_PAGE = "https://www.all-hands.dev/share";
@@ -13,8 +13,6 @@ interface FeedbackFormProps {
}
export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const copiedToClipboardToast = () => {
hotToast("Password copied to clipboard", {
icon: "📋",
@@ -53,10 +51,11 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
);
};
const { mutate: submitFeedback, isPending } = useSubmitFeedback();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
const formData = new FormData(event.currentTarget);
setIsSubmitting(true);
const email = formData.get("email")?.toString() || "";
const permissions = (formData.get("permissions")?.toString() ||
@@ -71,11 +70,17 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
token: "",
};
const response = await OpenHands.submitFeedback(feedback);
const { message, feedback_id, password } = response.body; // eslint-disable-line
const link = `${VIEWER_PAGE}?share_id=${feedback_id}`;
shareFeedbackToast(message, link, password);
setIsSubmitting(false);
submitFeedback(
{ feedback },
{
onSuccess: (data) => {
const { message, feedback_id, password } = data.body; // eslint-disable-line
const link = `${VIEWER_PAGE}?share_id=${feedback_id}`;
shareFeedbackToast(message, link, password);
onClose();
},
},
);
};
return (
@@ -109,13 +114,13 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
<div className="flex gap-2">
<ModalButton
disabled={isSubmitting}
disabled={isPending}
type="submit"
text="Submit"
className="bg-[#4465DB] grow"
/>
<ModalButton
disabled={isSubmitting}
disabled={isPending}
text="Cancel"
onClick={onClose}
className="bg-[#737373] grow"

View File

@@ -5,21 +5,22 @@ import {
IoIosRefresh,
IoIosCloudUpload,
} from "react-icons/io";
import { useRevalidator } from "@remix-run/react";
import { useDispatch, useSelector } from "react-redux";
import { IoFileTray } from "react-icons/io5";
import { useTranslation } from "react-i18next";
import { twMerge } from "tailwind-merge";
import AgentState from "#/types/AgentState";
import { setRefreshID } from "#/state/codeSlice";
import { addAssistantMessage } from "#/state/chatSlice";
import IconButton from "../IconButton";
import ExplorerTree from "./ExplorerTree";
import toast from "#/utils/toast";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import OpenHands from "#/api/open-hands";
import { useFiles } from "#/context/files";
import { isOpenHandsErrorResponse } from "#/api/open-hands.utils";
import VSCodeIcon from "#/assets/vscode-alt.svg?react";
import { useListFiles } from "#/hooks/query/use-list-files";
import { FileUploadSuccessResponse } from "#/api/open-hands.types";
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
interface ExplorerActionsProps {
onRefresh: () => void;
@@ -93,13 +94,9 @@ function ExplorerActions({
interface FileExplorerProps {
isOpen: boolean;
onToggle: () => void;
error: string | null;
}
function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
const { revalidate } = useRevalidator();
const { paths, setPaths } = useFiles();
function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
const [isDragging, setIsDragging] = React.useState(false);
const { curAgentState } = useSelector((state: RootState) => state.agent);
@@ -110,60 +107,84 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
fileInputRef.current?.click(); // Trigger the file browser
};
const refreshWorkspace = () => {
if (
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.STOPPED
) {
return;
const { data: paths, refetch, error } = useListFiles();
const handleUploadSuccess = (data: FileUploadSuccessResponse) => {
const uploadedCount = data.uploaded_files.length;
const skippedCount = data.skipped_files.length;
if (uploadedCount > 0) {
toast.success(
`upload-success-${new Date().getTime()}`,
t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, {
count: uploadedCount,
}),
);
}
if (skippedCount > 0) {
const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, {
count: skippedCount,
});
toast.info(message);
}
if (uploadedCount === 0 && skippedCount === 0) {
toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE));
}
dispatch(setRefreshID(Math.random()));
OpenHands.getFiles().then(setPaths);
revalidate();
};
const uploadFileData = async (files: FileList) => {
const handleUploadError = (e: Error) => {
toast.error(
`upload-error-${new Date().getTime()}`,
e.message || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
);
};
const { mutate: uploadFiles } = useUploadFiles();
const refreshWorkspace = () => {
if (
curAgentState !== AgentState.LOADING &&
curAgentState !== AgentState.STOPPED
) {
refetch();
}
};
const uploadFileData = (files: FileList) => {
uploadFiles(
{ files: Array.from(files) },
{ onSuccess: handleUploadSuccess, onError: handleUploadError },
);
refreshWorkspace();
};
const handleVSCodeClick = async (e: React.MouseEvent) => {
e.preventDefault();
try {
const result = await OpenHands.uploadFiles(Array.from(files));
if (isOpenHandsErrorResponse(result)) {
// Handle error response
toast.error(
`upload-error-${new Date().getTime()}`,
result.error || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
const response = await OpenHands.getVSCodeUrl();
if (response.vscode_url) {
dispatch(
addAssistantMessage(
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
),
);
return;
}
const uploadedCount = result.uploaded_files.length;
const skippedCount = result.skipped_files.length;
if (uploadedCount > 0) {
toast.success(
`upload-success-${new Date().getTime()}`,
t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, {
count: uploadedCount,
window.open(response.vscode_url, "_blank");
} else {
toast.error(
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: response.error,
}),
);
}
if (skippedCount > 0) {
const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, {
count: skippedCount,
});
toast.info(message);
}
if (uploadedCount === 0 && skippedCount === 0) {
toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE));
}
refreshWorkspace();
} catch (e) {
// Handle unexpected errors (network issues, etc.)
} catch (exp_error) {
toast.error(
`upload-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: String(exp_error),
}),
);
}
};
@@ -210,7 +231,7 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
!isOpen ? "w-12" : "w-60",
)}
>
<div className="flex flex-col relative h-full px-3 py-2">
<div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
<div className="sticky top-0 bg-neutral-800">
<div
className={twMerge(
@@ -232,17 +253,38 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
</div>
</div>
{!error && (
<div className="overflow-auto flex-grow">
<div className="overflow-auto flex-grow min-h-0">
<div style={{ display: !isOpen ? "none" : "block" }}>
<ExplorerTree files={paths} />
<ExplorerTree files={paths || []} />
</div>
</div>
)}
{error && (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-neutral-300 text-sm">{error}</p>
<p className="text-neutral-300 text-sm">{error.message}</p>
</div>
)}
{isOpen && (
<button
type="button"
onClick={handleVSCodeClick}
disabled={
curAgentState === AgentState.INIT ||
curAgentState === AgentState.LOADING
}
className={twMerge(
"mt-auto mb-2 w-full h-10 text-white rounded flex items-center justify-center gap-2 transition-colors",
curAgentState === AgentState.INIT ||
curAgentState === AgentState.LOADING
? "bg-neutral-600 cursor-not-allowed"
: "bg-[#4465DB] hover:bg-[#3451C7]",
)}
aria-label="Open in VS Code"
>
<VSCodeIcon width={20} height={20} />
Open in VS Code
</button>
)}
</div>
<input
data-testid="file-input"

View File

@@ -1,12 +1,10 @@
import React from "react";
import { useSelector } from "react-redux";
import toast from "react-hot-toast";
import { RootState } from "#/store";
import FolderIcon from "../FolderIcon";
import FileIcon from "../FileIcons";
import OpenHands from "#/api/open-hands";
import { useFiles } from "#/context/files";
import { cn } from "#/utils/utils";
import { useListFiles } from "#/hooks/query/use-list-files";
import { useListFile } from "#/hooks/query/use-list-file";
interface TitleProps {
name: string;
@@ -44,50 +42,34 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
selectedPath,
} = useFiles();
const [isOpen, setIsOpen] = React.useState(defaultOpen);
const [children, setChildren] = React.useState<string[] | null>(null);
const refreshID = useSelector((state: RootState) => state.code.refreshID);
const isDirectory = path.endsWith("/");
const { data: paths } = useListFiles({
path,
enabled: isDirectory && isOpen,
});
const { data: fileContent, refetch } = useListFile({ path });
React.useEffect(() => {
if (fileContent) {
const code = modifiedFiles[path] || files[path];
if (!code || fileContent !== files[path]) {
setFileContent(path, fileContent);
}
}
}, [fileContent, path]);
const fileParts = path.split("/");
const filename =
fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2];
const isDirectory = path.endsWith("/");
const refreshChildren = async () => {
if (!isDirectory || !isOpen) {
setChildren(null);
return;
}
try {
const newChildren = await OpenHands.getFiles(path);
setChildren(newChildren);
} catch (error) {
toast.error("Failed to fetch files");
}
};
React.useEffect(() => {
(async () => {
await refreshChildren();
})();
}, [refreshID, isOpen]);
const handleClick = async () => {
if (isDirectory) {
setIsOpen((prev) => !prev);
} else {
const code = modifiedFiles[path] || files[path];
try {
const fetchedCode = await OpenHands.getFile(path);
setSelectedPath(path);
if (!code || fetchedCode !== files[path]) {
setFileContent(path, fetchedCode);
}
} catch (error) {
toast.error("Failed to fetch file");
}
if (isDirectory) setIsOpen((prev) => !prev);
else {
setSelectedPath(path);
await refetch();
}
};
@@ -116,9 +98,9 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
)}
</button>
{isOpen && children && (
{isOpen && paths && (
<div className="ml-5">
{children.map((child, index) => (
{paths.map((child, index) => (
<TreeNode key={index} path={child} />
))}
</div>

View File

@@ -4,19 +4,26 @@ import {
Input,
Switch,
} from "@nextui-org/react";
import { useFetcher, useLocation, useNavigate } from "@remix-run/react";
import { useLocation } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import React from "react";
import posthog from "posthog-js";
import { organizeModelsAndProviders } from "#/utils/organizeModelsAndProviders";
import { ModelSelector } from "#/components/modals/settings/ModelSelector";
import { Settings } from "#/services/settings";
import { getDefaultSettings, Settings } from "#/services/settings";
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
import { clientAction } from "#/routes/settings";
import { extractModelAndProvider } from "#/utils/extractModelAndProvider";
import ModalButton from "../buttons/ModalButton";
import { DangerModal } from "../modals/confirmation-modals/danger-modal";
import { I18nKey } from "#/i18n/declaration";
import {
extractSettings,
saveSettingsView,
updateSettingsVersion,
} from "#/utils/settings-utils";
import { useEndSession } from "#/hooks/use-end-session";
import { useUserPrefs } from "#/context/user-prefs-context";
interface SettingsFormProps {
disabled?: boolean;
@@ -35,19 +42,36 @@ export function SettingsForm({
securityAnalyzers,
onClose,
}: SettingsFormProps) {
const { saveSettings } = useUserPrefs();
const endSession = useEndSession();
const location = useLocation();
const navigate = useNavigate();
const { t } = useTranslation();
const fetcher = useFetcher<typeof clientAction>();
const formRef = React.useRef<HTMLFormElement>(null);
React.useEffect(() => {
if (fetcher.data?.success) {
navigate("/");
const resetOngoingSession = () => {
if (location.pathname.startsWith("/app")) {
endSession();
onClose();
}
}, [fetcher.data, navigate, onClose]);
};
const handleFormSubmission = (formData: FormData) => {
const keys = Array.from(formData.keys());
const isUsingAdvancedOptions = keys.includes("use-advanced-options");
const newSettings = extractSettings(formData);
saveSettings(newSettings);
saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic");
updateSettingsVersion();
resetOngoingSession();
posthog.capture("settings_saved", {
LLM_MODEL: newSettings.LLM_MODEL,
LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET",
});
};
const advancedAlreadyInUse = React.useMemo(() => {
if (models.length > 0) {
@@ -83,20 +107,17 @@ export function SettingsForm({
React.useState(false);
const [showWarningModal, setShowWarningModal] = React.useState(false);
const submitForm = (formData: FormData) => {
if (location.pathname === "/app") formData.set("end-session", "true");
fetcher.submit(formData, { method: "POST", action: "/settings" });
};
const handleConfirmResetSettings = () => {
const formData = new FormData(formRef.current ?? undefined);
formData.set("intent", "reset");
submitForm(formData);
saveSettings(getDefaultSettings());
resetOngoingSession();
posthog.capture("settings_reset");
onClose();
};
const handleConfirmEndSession = () => {
const formData = new FormData(formRef.current ?? undefined);
submitForm(formData);
handleFormSubmission(formData);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -106,10 +127,11 @@ export function SettingsForm({
if (!apiKey) {
setShowWarningModal(true);
} else if (location.pathname === "/app") {
} else if (location.pathname.startsWith("/app")) {
setConfirmEndSessionModalOpen(true);
} else {
submitForm(formData);
handleFormSubmission(formData);
onClose();
}
};
@@ -117,18 +139,15 @@ export function SettingsForm({
const formData = new FormData(formRef.current ?? undefined);
const apiKey = formData.get("api-key");
if (!apiKey) {
setShowWarningModal(true);
} else {
onClose();
}
if (!apiKey) setShowWarningModal(true);
else onClose();
};
const handleWarningConfirm = () => {
setShowWarningModal(false);
const formData = new FormData(formRef.current ?? undefined);
formData.set("api-key", ""); // Set null value for API key
submitForm(formData);
handleFormSubmission(formData);
onClose();
};
@@ -138,11 +157,9 @@ export function SettingsForm({
return (
<div>
<fetcher.Form
<form
ref={formRef}
data-testid="settings-form"
method="POST"
action="/settings"
className="flex flex-col gap-6"
onSubmit={handleSubmit}
>
@@ -267,9 +284,7 @@ export function SettingsForm({
aria-label="Agent"
data-testid="agent-input"
name="agent"
defaultSelectedKey={
fetcher.formData?.get("agent")?.toString() ?? settings.AGENT
}
defaultSelectedKey={settings.AGENT}
isClearable={false}
inputProps={{
classNames: {
@@ -302,10 +317,7 @@ export function SettingsForm({
id="security-analyzer"
name="security-analyzer"
aria-label="Security Analyzer"
defaultSelectedKey={
fetcher.formData?.get("security-analyzer")?.toString() ??
settings.SECURITY_ANALYZER
}
defaultSelectedKey={settings.SECURITY_ANALYZER}
inputProps={{
classNames: {
inputWrapper:
@@ -346,7 +358,7 @@ export function SettingsForm({
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<ModalButton
disabled={disabled || fetcher.state === "submitting"}
disabled={disabled}
type="submit"
text={t(I18nKey.SETTINGS_FORM$SAVE_LABEL)}
className="bg-[#4465DB] w-full"
@@ -367,7 +379,7 @@ export function SettingsForm({
}}
/>
</div>
</fetcher.Form>
</form>
{confirmResetDefaultsModalOpen && (
<ModalBackdrop>

View File

@@ -1,8 +1,5 @@
import React from "react";
import {
isGitHubErrorReponse,
retrieveAllGitHubUserRepositories,
} from "#/api/github";
import { isGitHubErrorReponse } from "#/api/github";
import { SuggestionBox } from "#/routes/_oh._index/suggestion-box";
import { ConnectToGitHubModal } from "./modals/connect-to-github-modal";
import { ModalBackdrop } from "./modals/modal-backdrop";
@@ -10,40 +7,15 @@ import { GitHubRepositorySelector } from "#/routes/_oh._index/github-repo-select
import ModalButton from "./buttons/ModalButton";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
interface GitHubAuthProps {
onConnectToGitHub: () => void;
repositories: GitHubRepository[];
isLoggedIn: boolean;
}
function GitHubAuth({
onConnectToGitHub,
repositories,
isLoggedIn,
}: GitHubAuthProps) {
if (isLoggedIn) {
return <GitHubRepositorySelector repositories={repositories} />;
}
return (
<ModalButton
text="Connect to GitHub"
icon={<GitHubLogo width={20} height={20} />}
className="bg-[#791B80] w-full"
onClick={onConnectToGitHub}
/>
);
}
interface GitHubRepositoriesSuggestionBoxProps {
repositories: Awaited<
ReturnType<typeof retrieveAllGitHubUserRepositories>
> | null;
handleSubmit: () => void;
repositories: GitHubRepository[];
gitHubAuthUrl: string | null;
user: GitHubErrorReponse | GitHubUser | null;
}
export function GitHubRepositoriesSuggestionBox({
handleSubmit,
repositories,
gitHubAuthUrl,
user,
@@ -70,16 +42,26 @@ export function GitHubRepositoriesSuggestionBox({
);
}
const isLoggedIn = !!user && !isGitHubErrorReponse(user);
return (
<>
<SuggestionBox
title="Open a Repo"
content={
<GitHubAuth
isLoggedIn={!!user && !isGitHubErrorReponse(user)}
repositories={repositories || []}
onConnectToGitHub={handleConnectToGitHub}
/>
isLoggedIn ? (
<GitHubRepositorySelector
onSelect={handleSubmit}
repositories={repositories}
/>
) : (
<ModalButton
text="Connect to GitHub"
icon={<GitHubLogo width={20} height={20} />}
className="bg-[#791B80] w-full"
onClick={handleConnectToGitHub}
/>
)
}
/>
{connectToGitHubModalOpen && (

View File

@@ -56,14 +56,9 @@ export function InteractiveChatBox({
<div
className={cn(
"flex items-end gap-1",
"bg-neutral-700 border border-neutral-600 rounded-lg px-2 py-[10px]",
"bg-neutral-700 border border-neutral-600 rounded-lg px-2",
"transition-colors duration-200",
"hover:border-neutral-500 focus-within:border-neutral-500",
"group relative",
"before:pointer-events-none before:absolute before:inset-0 before:rounded-lg before:transition-colors",
"before:border-2 before:border-dashed before:border-transparent",
"[&:has(*:focus-within)]:before:border-neutral-500/50",
"[&:has(*[data-dragging-over='true'])]:before:border-neutral-500/50",
)}
>
<UploadImageInput onUpload={handleUpload} />
@@ -76,6 +71,8 @@ export function InteractiveChatBox({
onStop={onStop}
value={value}
onImagePaste={handleUpload}
className="py-[10px]"
buttonClassName="py-[10px]"
/>
</div>
</div>

View File

@@ -4,8 +4,8 @@ import { ExtraProps } from "react-markdown";
// Custom component to render <ul> in markdown
export function ul({
children,
}: React.ClassAttributes<HTMLElement> &
React.HTMLAttributes<HTMLElement> &
}: React.ClassAttributes<HTMLUListElement> &
React.HTMLAttributes<HTMLUListElement> &
ExtraProps) {
return <ul className="list-disc ml-5 pl-2 whitespace-normal">{children}</ul>;
}
@@ -13,10 +13,13 @@ export function ul({
// Custom component to render <ol> in markdown
export function ol({
children,
}: React.ClassAttributes<HTMLElement> &
React.HTMLAttributes<HTMLElement> &
start,
}: React.ClassAttributes<HTMLOListElement> &
React.OlHTMLAttributes<HTMLOListElement> &
ExtraProps) {
return (
<ol className="list-decimal ml-5 pl-2 whitespace-normal">{children}</ol>
<ol className="list-decimal ml-5 pl-2 whitespace-normal" start={start}>
{children}
</ol>
);
}

View File

@@ -1,16 +1,18 @@
import { useFetcher, useRouteLoaderData } from "@remix-run/react";
import React from "react";
import { useTranslation } from "react-i18next";
import { BaseModalTitle } from "./confirmation-modals/BaseModal";
import {
BaseModalDescription,
BaseModalTitle,
} from "./confirmation-modals/BaseModal";
import ModalBody from "./ModalBody";
import ModalButton from "../buttons/ModalButton";
import FormFieldset from "../form/FormFieldset";
import { CustomInput } from "../form/custom-input";
import { clientLoader } from "#/routes/_oh";
import { clientAction as settingsClientAction } from "#/routes/settings";
import { clientAction as loginClientAction } from "#/routes/login";
import { AvailableLanguages } from "#/i18n";
import { I18nKey } from "#/i18n/declaration";
import { useAuth } from "#/context/auth-context";
import { useUserPrefs } from "#/context/user-prefs-context";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
interface AccountSettingsModalProps {
onClose: () => void;
@@ -25,41 +27,33 @@ function AccountSettingsModal({
gitHubError,
analyticsConsent,
}: AccountSettingsModalProps) {
const { gitHubToken, setGitHubToken, logout } = useAuth();
const { saveSettings } = useUserPrefs();
const { t } = useTranslation();
const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
const settingsFetcher = useFetcher<typeof settingsClientAction>({
key: "settings",
});
const loginFetcher = useFetcher<typeof loginClientAction>({ key: "login" });
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const language = formData.get("language")?.toString();
const ghToken = formData.get("ghToken")?.toString();
const language = formData.get("language")?.toString();
const analytics = formData.get("analytics")?.toString() === "on";
const accountForm = new FormData();
const loginForm = new FormData();
if (ghToken) setGitHubToken(ghToken);
accountForm.append("intent", "account");
// The form returns the language label, so we need to find the corresponding
// language key to save it in the settings
if (language) {
const languageKey = AvailableLanguages.find(
({ label }) => label === language,
)?.value;
accountForm.append("language", languageKey ?? "en");
}
if (ghToken) loginForm.append("ghToken", ghToken);
accountForm.append("analytics", analytics.toString());
settingsFetcher.submit(accountForm, {
method: "POST",
action: "/settings",
});
loginFetcher.submit(loginForm, {
method: "POST",
action: "/login",
});
if (languageKey) saveSettings({ LANGUAGE: languageKey });
}
handleCaptureConsent(analytics);
const ANALYTICS = analytics.toString();
localStorage.setItem("analytics-consent", ANALYTICS);
onClose();
};
@@ -85,22 +79,30 @@ function AccountSettingsModal({
name="ghToken"
label="GitHub Token"
type="password"
defaultValue={data?.ghToken ?? ""}
defaultValue={gitHubToken ?? ""}
/>
<BaseModalDescription>
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN)}{" "}
<a
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
rel="noreferrer noopener"
className="text-[#791B80] underline"
>
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$HERE)}
</a>
</BaseModalDescription>
{gitHubError && (
<p className="text-danger text-xs">
{t(I18nKey.ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID)}
</p>
)}
{data?.ghToken && !gitHubError && (
{gitHubToken && !gitHubError && (
<ModalButton
variant="text-like"
text={t(I18nKey.ACCOUNT_SETTINGS_MODAL$DISCONNECT)}
onClick={() => {
settingsFetcher.submit(
{},
{ method: "POST", action: "/logout" },
);
logout();
onClose();
}}
className="text-danger self-start"
@@ -119,10 +121,6 @@ function AccountSettingsModal({
<div className="flex flex-col gap-2 w-full">
<ModalButton
disabled={
settingsFetcher.state === "submitting" ||
loginFetcher.state === "submitting"
}
type="submit"
intent="account"
text={t(I18nKey.ACCOUNT_SETTINGS_MODAL$SAVE)}

View File

@@ -1,54 +0,0 @@
import { Form, useNavigation } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import {
BaseModalDescription,
BaseModalTitle,
} from "./confirmation-modals/BaseModal";
import ModalButton from "../buttons/ModalButton";
import AllHandsLogo from "#/assets/branding/all-hands-logo-spark.svg?react";
import ModalBody from "./ModalBody";
import { CustomInput } from "../form/custom-input";
import { I18nKey } from "#/i18n/declaration";
function ConnectToGitHubByTokenModal() {
const navigation = useNavigation();
const { t } = useTranslation();
return (
<ModalBody testID="auth-modal">
<div className="flex flex-col gap-2">
<AllHandsLogo width={69} height={46} className="self-center" />
<BaseModalTitle title="Ready to experience the future?" />
<BaseModalDescription description="Connect All Hands to your GitHub account to start building." />
</div>
<Form className="w-full flex flex-col gap-6" method="post" action="/">
<CustomInput label="GitHub Token" name="token" />
<label htmlFor="tos" className="flex gap-2">
<input
data-testid="accept-terms"
id="tos"
name="tos"
type="checkbox"
required
/>
<p className="text-xs text-[#A3A3A3]">
{t(
I18nKey.CONNECT_TO_GITHUB_BY_TOKEN_MODAL$BY_CONNECTING_YOU_AGREE,
)}{" "}
<span className="text-hyperlink">
{t(I18nKey.CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE)}
</span>
.
</p>
</label>
<ModalButton
type="submit"
text={t(I18nKey.CONNECT_TO_GITHUB_BY_TOKEN_MODAL$CONTINUE)}
className="bg-[#791B80] w-full"
disabled={navigation.state === "loading"}
/>
</Form>
</ModalBody>
);
}
export default ConnectToGitHubByTokenModal;

View File

@@ -1,4 +1,3 @@
import { useFetcher, useRouteLoaderData } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import ModalBody from "./ModalBody";
import { CustomInput } from "../form/custom-input";
@@ -7,19 +6,26 @@ import {
BaseModalDescription,
BaseModalTitle,
} from "./confirmation-modals/BaseModal";
import { clientLoader } from "#/routes/_oh";
import { clientAction } from "#/routes/login";
import { I18nKey } from "#/i18n/declaration";
import { useAuth } from "#/context/auth-context";
interface ConnectToGitHubModalProps {
onClose: () => void;
}
export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
const fetcher = useFetcher<typeof clientAction>({ key: "login" });
const { gitHubToken, setGitHubToken } = useAuth();
const { t } = useTranslation();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const ghToken = formData.get("ghToken")?.toString();
if (ghToken) setGitHubToken(ghToken);
onClose();
};
return (
<ModalBody>
<div className="flex flex-col gap-2 self-start">
@@ -40,18 +46,13 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
}
/>
</div>
<fetcher.Form
method="POST"
action="/login"
className="w-full flex flex-col gap-6"
onSubmit={onClose}
>
<form onSubmit={handleSubmit} className="w-full flex flex-col gap-6">
<CustomInput
label="GitHub Token"
name="ghToken"
required
type="password"
defaultValue={data?.ghToken ?? ""}
defaultValue={gitHubToken ?? ""}
/>
<div className="flex flex-col gap-2 w-full">
@@ -59,7 +60,6 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
testId="connect-to-github"
type="submit"
text={t(I18nKey.CONNECT_TO_GITHUB_MODAL$CONNECT)}
disabled={fetcher.state === "submitting"}
className="bg-[#791B80] w-full"
/>
<ModalButton
@@ -68,7 +68,7 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
className="bg-[#737373] w-full"
/>
</div>
</fetcher.Form>
</form>
</ModalBody>
);
}

View File

@@ -17,7 +17,7 @@ import { useWsClient } from "#/context/ws-client-provider";
interface ProjectMenuCardProps {
isConnectedToGitHub: boolean;
githubData: {
avatar: string;
avatar: string | null;
repoName: string;
lastCommit: GitHubCommit;
} | null;

View File

@@ -5,7 +5,7 @@ import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuDetailsProps {
repoName: string;
avatar: string;
avatar: string | null;
lastCommit: GitHubCommit;
}
@@ -23,7 +23,7 @@ export function ProjectMenuDetails({
rel="noreferrer noopener"
className="flex items-center gap-2"
>
<img src={avatar} alt="" className="w-4 h-4 rounded-full" />
{avatar && <img src={avatar} alt="" className="w-4 h-4 rounded-full" />}
<span className="text-sm leading-6 font-semibold">{repoName}</span>
<ExternalLinkIcon width={16} height={16} />
</a>

View File

@@ -7,12 +7,12 @@ interface SuggestionItemProps {
export function SuggestionItem({ suggestion, onClick }: SuggestionItemProps) {
return (
<li className="border border-neutral-600 rounded-xl hover:bg-neutral-700">
<li className="list-none border border-neutral-600 rounded-xl hover:bg-neutral-700">
<button
type="button"
data-testid="suggestion"
onClick={() => onClick(suggestion.value)}
className="text-[16px] leading-6 -tracking-[0.01em] text-center w-full p-4 font-semibold"
className="text-[16px] leading-6 -tracking-[0.01em] text-center w-full p-3 font-semibold"
>
{suggestion.label}
</button>

View File

@@ -11,7 +11,7 @@ export function UploadImageInput({ onUpload, label }: UploadImageInputProps) {
};
return (
<label className="cursor-pointer">
<label className="cursor-pointer py-[10px]">
{label || <Clip data-testid="default-label" width={24} height={24} />}
<input
data-testid="upload-image-input"

View File

@@ -1,5 +1,4 @@
import React from "react";
import { useFetcher } from "@remix-run/react";
import { AccountSettingsContextMenu } from "./context-menu/account-settings-context-menu";
import { UserAvatar } from "./user-avatar";
@@ -14,8 +13,6 @@ export function UserActions({
onLogout,
user,
}: UserActionsProps) {
const loginFetcher = useFetcher({ key: "login" });
const [accountContextMenuIsVisible, setAccountContextMenuIsVisible] =
React.useState(false);
@@ -39,11 +36,7 @@ export function UserActions({
return (
<div data-testid="user-actions" className="w-8 h-8 relative">
<UserAvatar
isLoading={loginFetcher.state !== "idle"}
avatarUrl={user?.avatar_url}
onClick={toggleAccountMenu}
/>
<UserAvatar avatarUrl={user?.avatar_url} onClick={toggleAccountMenu} />
{accountContextMenuIsVisible && (
<AccountSettingsContextMenu

View File

@@ -0,0 +1,82 @@
import posthog from "posthog-js";
import React from "react";
interface AuthContextType {
token: string | null;
gitHubToken: string | null;
setToken: (token: string | null) => void;
setGitHubToken: (token: string | null) => void;
clearToken: () => void;
clearGitHubToken: () => void;
logout: () => void;
}
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
function AuthProvider({ children }: React.PropsWithChildren) {
const [tokenState, setTokenState] = React.useState<string | null>(() =>
localStorage.getItem("token"),
);
const [gitHubTokenState, setGitHubTokenState] = React.useState<string | null>(
() => localStorage.getItem("ghToken"),
);
React.useLayoutEffect(() => {
setTokenState(localStorage.getItem("token"));
setGitHubTokenState(localStorage.getItem("ghToken"));
});
const setToken = (token: string | null) => {
setTokenState(token);
if (token) localStorage.setItem("token", token);
else localStorage.removeItem("token");
};
const setGitHubToken = (token: string | null) => {
setGitHubTokenState(token);
if (token) localStorage.setItem("ghToken", token);
else localStorage.removeItem("ghToken");
};
const clearToken = () => {
setTokenState(null);
localStorage.removeItem("token");
};
const clearGitHubToken = () => {
setGitHubTokenState(null);
localStorage.removeItem("ghToken");
};
const logout = () => {
clearGitHubToken();
posthog.reset();
};
const value = React.useMemo(
() => ({
token: tokenState,
gitHubToken: gitHubTokenState,
setToken,
setGitHubToken,
clearToken,
clearGitHubToken,
logout,
}),
[tokenState, gitHubTokenState],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function useAuth() {
const context = React.useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within a AuthProvider");
}
return context;
}
export { AuthProvider, useAuth };

View File

@@ -0,0 +1,55 @@
import React from "react";
import {
getSettings,
Settings,
saveSettings as updateAndSaveSettingsToLocalStorage,
settingsAreUpToDate as checkIfSettingsAreUpToDate,
} from "#/services/settings";
interface UserPrefsContextType {
settings: Settings;
settingsAreUpToDate: boolean;
saveSettings: (settings: Partial<Settings>) => void;
}
const UserPrefsContext = React.createContext<UserPrefsContextType | undefined>(
undefined,
);
function UserPrefsProvider({ children }: React.PropsWithChildren) {
const [settings, setSettings] = React.useState(getSettings());
const [settingsAreUpToDate, setSettingsAreUpToDate] = React.useState(
checkIfSettingsAreUpToDate(),
);
const saveSettings = (newSettings: Partial<Settings>) => {
updateAndSaveSettingsToLocalStorage(newSettings);
setSettings(getSettings());
setSettingsAreUpToDate(checkIfSettingsAreUpToDate());
};
const value = React.useMemo(
() => ({
settings,
settingsAreUpToDate,
saveSettings,
}),
[settings, settingsAreUpToDate],
);
return (
<UserPrefsContext.Provider value={value}>
{children}
</UserPrefsContext.Provider>
);
}
function useUserPrefs() {
const context = React.useContext(UserPrefsContext);
if (context === undefined) {
throw new Error("useUserPrefs must be used within a UserPrefsProvider");
}
return context;
}
export { UserPrefsProvider, useUserPrefs };

View File

@@ -4,6 +4,13 @@ import { Settings } from "#/services/settings";
import ActionType from "#/types/ActionType";
import EventLogger from "#/utils/event-logger";
import AgentState from "#/types/AgentState";
import { handleAssistantMessage } from "#/services/actions";
import { useRate } from "#/utils/use-rate";
const isOpenHandsMessage = (event: Record<string, unknown>) =>
event.action === "message";
const RECONNECT_RETRIES = 5;
export enum WsClientProviderStatus {
STOPPED,
@@ -14,12 +21,14 @@ export enum WsClientProviderStatus {
interface UseWsClient {
status: WsClientProviderStatus;
isLoadingMessages: boolean;
events: Record<string, unknown>[];
send: (event: Record<string, unknown>) => void;
}
const WsClientContext = React.createContext<UseWsClient>({
status: WsClientProviderStatus.STOPPED,
isLoadingMessages: true,
events: [],
send: () => {
throw new Error("not connected");
@@ -46,6 +55,9 @@ export function WsClientProvider({
const closeRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const [status, setStatus] = React.useState(WsClientProviderStatus.STOPPED);
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [retryCount, setRetryCount] = React.useState(RECONNECT_RETRIES);
const messageRateHandler = useRate({ threshold: 500 });
function send(event: Record<string, unknown>) {
if (!wsRef.current) {
@@ -56,6 +68,7 @@ export function WsClientProvider({
}
function handleOpen() {
setRetryCount(RECONNECT_RETRIES);
setStatus(WsClientProviderStatus.OPENING);
const initEvent = {
action: ActionType.INIT,
@@ -66,6 +79,9 @@ export function WsClientProvider({
function handleMessage(messageEvent: MessageEvent) {
const event = JSON.parse(messageEvent.data);
if (isOpenHandsMessage(event)) {
messageRateHandler.record(new Date().getTime());
}
setEvents((prevEvents) => [...prevEvents, event]);
if (event.extras?.agent_state === AgentState.INIT) {
setStatus(WsClientProviderStatus.ACTIVE);
@@ -76,11 +92,19 @@ export function WsClientProvider({
) {
setStatus(WsClientProviderStatus.ERROR);
}
handleAssistantMessage(event);
}
function handleClose() {
setStatus(WsClientProviderStatus.STOPPED);
setEvents([]);
if (retryCount) {
setTimeout(() => {
setRetryCount(retryCount - 1);
}, 1000);
} else {
setStatus(WsClientProviderStatus.STOPPED);
setEvents([]);
}
wsRef.current = null;
}
@@ -95,7 +119,7 @@ export function WsClientProvider({
let ws = wsRef.current;
// If disabled close any existing websockets...
if (!enabled) {
if (!enabled || !retryCount) {
if (ws) {
ws.close();
}
@@ -116,7 +140,11 @@ export function WsClientProvider({
const baseUrl =
import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(`${protocol}//${baseUrl}/ws`, [
let wsUrl = `${protocol}//${baseUrl}/ws`;
if (events.length) {
wsUrl += `?latest_event_id=${events[events.length - 1].id}`;
}
ws = new WebSocket(wsUrl, [
"openhands",
token || "NO_JWT",
ghToken || "NO_GITHUB",
@@ -136,7 +164,7 @@ export function WsClientProvider({
ws.removeEventListener("error", handleError);
ws.removeEventListener("close", handleClose);
};
}, [enabled, token, ghToken]);
}, [enabled, token, ghToken, retryCount]);
// Strict mode mounts and unmounts each component twice, so we have to wait in the destructor
// before actually closing the socket and cancel the operation if the component gets remounted.
@@ -148,7 +176,11 @@ export function WsClientProvider({
return () => {
closeRef.current = setTimeout(() => {
wsRef.current?.close();
const ws = wsRef.current;
if (ws) {
ws.removeEventListener("close", handleClose);
ws.close();
}
}, 100);
};
}, []);
@@ -156,10 +188,11 @@ export function WsClientProvider({
const value = React.useMemo<UseWsClient>(
() => ({
status,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
send,
}),
[status, events],
[status, messageRateHandler.isUnderThreshold, events],
);
return (

View File

@@ -11,15 +11,23 @@ import { hydrateRoot } from "react-dom/client";
import { Provider } from "react-redux";
import posthog from "posthog-js";
import "./i18n";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import store from "./store";
import { useConfig } from "./hooks/query/use-config";
import { AuthProvider } from "./context/auth-context";
import { UserPrefsProvider } from "./context/user-prefs-context";
function PosthogInit() {
const { data: config } = useConfig();
React.useEffect(() => {
posthog.init("phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA", {
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
});
}, []);
if (config?.POSTHOG_CLIENT_KEY) {
posthog.init(config.POSTHOG_CLIENT_KEY, {
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
});
}
}, [config]);
return null;
}
@@ -37,14 +45,22 @@ async function prepareApp() {
}
}
const queryClient = new QueryClient();
prepareApp().then(() =>
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<Provider store={store}>
<RemixBrowser />
<PosthogInit />
<UserPrefsProvider>
<AuthProvider>
<QueryClientProvider client={queryClient}>
<RemixBrowser />
<PosthogInit />
</QueryClientProvider>
</AuthProvider>
</UserPrefsProvider>
</Provider>
</StrictMode>,
);

View File

@@ -0,0 +1,21 @@
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
type SaveFileArgs = {
path: string;
content: string;
};
export const useSaveFile = () => {
const { token } = useAuth();
return useMutation({
mutationFn: ({ path, content }: SaveFileArgs) =>
OpenHands.saveFile(token || "", path, content),
onError: (error) => {
toast.error(error.message);
},
});
};

View File

@@ -0,0 +1,21 @@
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
type SubmitFeedbackArgs = {
feedback: Feedback;
};
export const useSubmitFeedback = () => {
const { token } = useAuth();
return useMutation({
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
OpenHands.submitFeedback(token || "", feedback),
onError: (error) => {
toast.error(error.message);
},
});
};

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