mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
4 Commits
rb/github-
...
rb/od
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f57c2ce64 | ||
|
|
9f880f7690 | ||
|
|
d69228b20c | ||
|
|
4340da8973 |
@@ -1 +1 @@
|
||||
The files in this directory configure a development container for GitHub Codespaces.
|
||||
The files in this directory configure a development container for GitHub Codespaces.
|
||||
@@ -2,5 +2,7 @@
|
||||
sudo apt update
|
||||
sudo apt install -y netcat
|
||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||
sudo apt install -y python3.12
|
||||
curl -sSL https://install.python-poetry.org | python3.12 -
|
||||
sudo apt install -y python3.11
|
||||
curl -sSL https://install.python-poetry.org | python3.11 -
|
||||
# chromadb requires SQLite > 3.35 but SQLite in Python3.11.9 comes with 3.31.1
|
||||
sudo cp /opt/conda/lib/libsqlite3.so.0 /lib/x86_64-linux-gnu/libsqlite3.so.0
|
||||
|
||||
70
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
70
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@@ -5,57 +5,71 @@ labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thank you for taking the time to fill out this bug report. Please provide as much information as possible to help us understand and address the issue effectively.
|
||||
value: Thank you for taking the time to fill out this bug report. We greatly appreciate your effort to complete this template fully. Please provide as much information as possible to help us understand and address the issue effectively.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for the same bug?
|
||||
description: Please check if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have checked the troubleshooting document at https://docs.all-hands.dev/modules/usage/troubleshooting
|
||||
required: true
|
||||
- label: I have checked the existing issues.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug and reproduction steps
|
||||
description: Provide a description of the issue along with any reproduction steps.
|
||||
label: Describe the bug
|
||||
description: Provide a short description of the problem.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: installation
|
||||
- type: textarea
|
||||
id: current-version
|
||||
attributes:
|
||||
label: OpenHands Installation
|
||||
description: How are you running OpenHands?
|
||||
options:
|
||||
- Docker command in README
|
||||
- Development workflow
|
||||
- app.all-hands.dev
|
||||
- Other
|
||||
default: 0
|
||||
label: Current OpenHands version
|
||||
description: What version of OpenHands are you using? If you're running in docker, tell us the tag you're using (e.g. ghcr.io/all-hands-ai/openhands:0.3.1).
|
||||
render: bash
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: openhands-version
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: OpenHands Version
|
||||
description: What version of OpenHands are you using?
|
||||
placeholder: ex. 0.9.8, main, etc.
|
||||
label: Installation and Configuration
|
||||
description: Please provide any commands you ran and any configuration (redacting API keys)
|
||||
render: bash
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
- type: textarea
|
||||
id: model-agent
|
||||
attributes:
|
||||
label: Model and Agent
|
||||
description: What model and agent are you using? You can see these settings in the UI by clicking the settings wheel.
|
||||
placeholder: |
|
||||
- Model:
|
||||
- Agent:
|
||||
|
||||
- type: textarea
|
||||
id: os-version
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- MacOS
|
||||
- Linux
|
||||
- WSL on Windows
|
||||
description: What Operating System are you using? Linux, Mac OS, WSL on Windows
|
||||
|
||||
- type: textarea
|
||||
id: repro-steps
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: Please list the steps to reproduce the issue.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Logs, Errors, Screenshots, and Additional Context
|
||||
description: Please provide any additional information you think might help. If you want to share the chat history
|
||||
you can click the thumbs-down (👎) button above the input field and you will get a shareable link
|
||||
(you can also click thumbs up when things are going well of course!). LLM logs will be stored in the
|
||||
`logs/llm/default` folder. Please add any additional context about the problem here.
|
||||
description: If you want to share the chat history you can click the thumbs-down (👎) button above the input field and you will get a shareable link (you can also click thumbs up when things are going well of course!). LLM logs will be stored in the `logs/llm/default` folder. Please add any additional context about the problem here.
|
||||
|
||||
49
.github/dependabot.yml
vendored
49
.github/dependabot.yml
vendored
@@ -1,35 +1,21 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 1
|
||||
groups:
|
||||
# put packages in their own group if they have a history of breaking the build or needing to be reverted
|
||||
pre-commit:
|
||||
patterns:
|
||||
- "pre-commit"
|
||||
llama:
|
||||
patterns:
|
||||
- "llama*"
|
||||
chromadb:
|
||||
patterns:
|
||||
- "chromadb"
|
||||
security-all:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
version-all:
|
||||
applies-to: "version-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 1
|
||||
open-pull-requests-limit: 20
|
||||
groups:
|
||||
docusaurus:
|
||||
patterns:
|
||||
@@ -37,21 +23,12 @@ updates:
|
||||
eslint:
|
||||
patterns:
|
||||
- "*eslint*"
|
||||
security-all:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
version-all:
|
||||
applies-to: "version-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "wednesday"
|
||||
open-pull-requests-limit: 1
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 20
|
||||
groups:
|
||||
docusaurus:
|
||||
patterns:
|
||||
@@ -59,11 +36,3 @@ updates:
|
||||
eslint:
|
||||
patterns:
|
||||
- "*eslint*"
|
||||
security-all:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
version-all:
|
||||
applies-to: "version-updates"
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -1,6 +1,6 @@
|
||||
**End-user friendly description of the problem this fixes or functionality that this introduces**
|
||||
**What is the problem that this fixes or functionality that this introduces? Does it fix any open issues?**
|
||||
|
||||
|
||||
- [ ] Include this change in the Release Notes. If checked, you must provide an **end-user friendly** description for your change below
|
||||
|
||||
---
|
||||
**Give a summary of what the PR does, explaining any non-trivial design decisions**
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
|
||||
---
|
||||
**Link of any specific issues this addresses**
|
||||
**Other references**
|
||||
|
||||
7
.github/workflows/deploy-docs.yml
vendored
7
.github/workflows/deploy-docs.yml
vendored
@@ -14,11 +14,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Build the documentation website
|
||||
build:
|
||||
@@ -37,7 +32,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.11'
|
||||
- name: Generate Python Docs
|
||||
run: rm -rf docs/modules/python && pip install pydoc-markdown && pydoc-markdown
|
||||
- name: Install dependencies
|
||||
|
||||
39
.github/workflows/dummy-agent-test.yml
vendored
39
.github/workflows/dummy-agent-test.yml
vendored
@@ -9,48 +9,25 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
python-version: '3.11'
|
||||
- name: Set up environment
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry install --without evaluation,llama-index
|
||||
poetry run playwright install --with-deps chromium
|
||||
wget https://huggingface.co/BAAI/bge-small-en-v1.5/raw/main/1_Pooling/config.json -P /tmp/llama_index/models--BAAI--bge-small-en-v1.5/snapshots/5c38ec7c405ec4b44b94cc5a9bb96e735b38267a/1_Pooling/
|
||||
- name: Run tests
|
||||
run: |
|
||||
set -e
|
||||
SANDBOX_FORCE_REBUILD_RUNTIME=True poetry run python3 openhands/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
|
||||
poetry run python openhands/core/main.py -t "do a flip" -d ./workspace/ -c DummyAgent
|
||||
- name: Check exit code
|
||||
run: |
|
||||
if [ $? -ne 0 ]; then
|
||||
|
||||
160
.github/workflows/eval-runner.yml
vendored
160
.github/workflows/eval-runner.yml
vendored
@@ -1,160 +0,0 @@
|
||||
name: Run Evaluation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
schedule:
|
||||
- cron: "0 1 * * *" # Run daily at 1 AM UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: "Reason for manual trigger"
|
||||
required: true
|
||||
default: ""
|
||||
|
||||
env:
|
||||
N_PROCESSES: 32 # Global configuration for number of parallel processes for evaluation
|
||||
|
||||
jobs:
|
||||
run-evaluation:
|
||||
if: github.event.label.name == 'eval-this' || github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: "read"
|
||||
id-token: "write"
|
||||
pull-requests: "write"
|
||||
issues: "write"
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Comment on PR if 'eval-this' label is present
|
||||
if: github.event_name == 'pull_request' && github.event.label.name == 'eval-this'
|
||||
uses: KeisukeYamashita/create-comment@v1
|
||||
with:
|
||||
unique: false
|
||||
comment: |
|
||||
Hi! I started running the evaluation on your PR. You will receive a comment with the results shortly.
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install
|
||||
|
||||
- name: Configure config.toml for evaluation
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_LLM_API_KEY }}
|
||||
run: |
|
||||
echo "[llm.eval]" > config.toml
|
||||
echo "model = \"deepseek/deepseek-chat\"" >> config.toml
|
||||
echo "api_key = \"$DEEPSEEK_API_KEY\"" >> config.toml
|
||||
echo "temperature = 0.0" >> config.toml
|
||||
|
||||
- name: Run integration test evaluation
|
||||
env:
|
||||
ALLHANDS_API_KEY: ${{ secrets.ALLHANDS_EVAL_RUNTIME_API_KEY }}
|
||||
RUNTIME: remote
|
||||
SANDBOX_REMOTE_RUNTIME_API_URL: https://runtime.eval.all-hands.dev
|
||||
EVAL_DOCKER_IMAGE_PREFIX: us-central1-docker.pkg.dev/evaluation-092424/swe-bench-images
|
||||
|
||||
run: |
|
||||
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' $N_PROCESSES
|
||||
|
||||
# get evaluation report
|
||||
REPORT_FILE=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/deepseek-chat_maxiter_10_N* -name "report.md" -type f | head -n 1)
|
||||
echo "REPORT_FILE: $REPORT_FILE"
|
||||
echo "INTEGRATION_TEST_REPORT<<EOF" >> $GITHUB_ENV
|
||||
cat $REPORT_FILE >> $GITHUB_ENV
|
||||
echo >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Run SWE-Bench evaluation
|
||||
env:
|
||||
ALLHANDS_API_KEY: ${{ secrets.ALLHANDS_EVAL_RUNTIME_API_KEY }}
|
||||
RUNTIME: remote
|
||||
SANDBOX_REMOTE_RUNTIME_API_URL: https://runtime.eval.all-hands.dev
|
||||
EVAL_DOCKER_IMAGE_PREFIX: us-central1-docker.pkg.dev/evaluation-092424/swe-bench-images
|
||||
|
||||
run: |
|
||||
poetry run ./evaluation/swe_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 300 30 $N_PROCESSES "princeton-nlp/SWE-bench_Lite" test
|
||||
OUTPUT_FOLDER=$(find evaluation/evaluation_outputs/outputs/princeton-nlp__SWE-bench_Lite-test/CodeActAgent -name "deepseek-chat_maxiter_50_N_*-no-hint-run_1" -type d | head -n 1)
|
||||
echo "OUTPUT_FOLDER for SWE-bench evaluation: $OUTPUT_FOLDER"
|
||||
poetry run ./evaluation/swe_bench/scripts/eval_infer_remote.sh $OUTPUT_FOLDER/output.jsonl $N_PROCESSES "princeton-nlp/SWE-bench_Lite" test
|
||||
|
||||
poetry run ./evaluation/swe_bench/scripts/eval/summarize_outputs.py $OUTPUT_FOLDER/output.jsonl > summarize_outputs.log 2>&1
|
||||
echo "SWEBENCH_REPORT<<EOF" >> $GITHUB_ENV
|
||||
cat summarize_outputs.log >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create tar.gz of evaluation outputs
|
||||
run: |
|
||||
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
|
||||
tar -czvf evaluation_outputs_${TIMESTAMP}.tar.gz evaluation/evaluation_outputs/outputs
|
||||
|
||||
- name: Upload evaluation results as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
id: upload_results_artifact
|
||||
with:
|
||||
name: evaluation-outputs
|
||||
path: evaluation_outputs_*.tar.gz
|
||||
|
||||
- name: Get artifact URL
|
||||
run: echo "ARTIFACT_URL=${{ steps.upload_results_artifact.outputs.artifact-url }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_RESEARCH_OBJECT_CREATOR_SA_KEY }}
|
||||
|
||||
- name: Set timestamp and trigger reason
|
||||
run: |
|
||||
echo "TIMESTAMP=$(date +'%Y-%m-%d-%H-%M')" >> $GITHUB_ENV
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "TRIGGER_REASON=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
echo "TRIGGER_REASON=schedule" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TRIGGER_REASON=manual-${{ github.event.inputs.reason }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Upload evaluation results to Google Cloud Storage
|
||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||
with:
|
||||
path: 'evaluation/evaluation_outputs/outputs'
|
||||
destination: 'openhands-oss-eval-results/${{ env.TIMESTAMP }}-${{ env.TRIGGER_REASON }}'
|
||||
|
||||
- name: Comment with evaluation results and artifact link
|
||||
id: create_comment
|
||||
uses: KeisukeYamashita/create-comment@v1
|
||||
with:
|
||||
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || 4504 }}
|
||||
unique: false
|
||||
comment: |
|
||||
Trigger by: ${{ github.event_name == 'pull_request' && format('Pull Request (eval-this label on PR #{0})', github.event.pull_request.number) || github.event_name == 'schedule' && 'Daily Schedule' || format('Manual Trigger: {0}', github.event.inputs.reason) }}
|
||||
Commit: ${{ github.sha }}
|
||||
**SWE-Bench Evaluation Report**
|
||||
${{ env.SWEBENCH_REPORT }}
|
||||
---
|
||||
**Integration Tests Evaluation Report**
|
||||
${{ env.INTEGRATION_TEST_REPORT }}
|
||||
---
|
||||
You can download the full evaluation outputs [here](${{ env.ARTIFACT_URL }}).
|
||||
|
||||
- name: Post to a Slack channel
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
with:
|
||||
channel-id: 'C07SVQSCR6F'
|
||||
slack-message: "*Evaluation Trigger:* ${{ github.event_name == 'pull_request' && format('Pull Request (eval-this label on PR #{0})', github.event.pull_request.number) || github.event_name == 'schedule' && 'Daily Schedule' || format('Manual Trigger: {0}', github.event.inputs.reason) }}\n\nLink to summary: [here](https://github.com/${{ github.repository }}/issues/${{ github.event_name == 'pull_request' && github.event.pull_request.number || 4504 }}#issuecomment-${{ steps.create_comment.outputs.comment-id }})"
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.EVAL_NOTIF_SLACK_BOT_TOKEN }}
|
||||
5
.github/workflows/fe-unit-tests.yml
vendored
5
.github/workflows/fe-unit-tests.yml
vendored
@@ -12,11 +12,6 @@ on:
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/fe-unit-tests.yml'
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Run frontend unit tests
|
||||
fe-test:
|
||||
|
||||
447
.github/workflows/ghcr-build.yml
vendored
447
.github/workflows/ghcr-build.yml
vendored
@@ -1,447 +0,0 @@
|
||||
# Workflow that builds, tests and then pushes the OpenHands and runtime docker images to the ghcr.io repository
|
||||
name: Docker
|
||||
|
||||
# Always run on "main"
|
||||
# Always run on tags
|
||||
# Always run on PRs
|
||||
# Can also be triggered manually
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22
|
||||
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
jobs:
|
||||
# Builds the OpenHands Docker images
|
||||
ghcr_build_app:
|
||||
name: Build App Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
hash_from_app_image: ${{ steps.get_hash_in_app_image.outputs.hash_from_app_image }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push app image
|
||||
if: "!github.event.pull_request.head.repo.fork"
|
||||
run: |
|
||||
./containers/build.sh -i openhands -o ${{ github.repository_owner }} --push
|
||||
- name: Build app image
|
||||
if: "github.event.pull_request.head.repo.fork"
|
||||
run: |
|
||||
./containers/build.sh -i openhands -o ${{ github.repository_owner }} --load
|
||||
- name: Get hash in App Image
|
||||
id: get_hash_in_app_image
|
||||
run: |
|
||||
# Lowercase the repository owner
|
||||
export REPO_OWNER=${{ github.repository_owner }}
|
||||
REPO_OWNER=$(echo $REPO_OWNER | tr '[:upper:]' '[:lower:]')
|
||||
# Run the build script in the app image
|
||||
docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${REPO_OWNER}/openhands:${{ env.RELEVANT_SHA }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt
|
||||
# Get the hash from the build script
|
||||
hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
|
||||
echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT
|
||||
echo "Hash from app image: $hash_from_app_image"
|
||||
|
||||
# Builds the runtime Docker images
|
||||
ghcr_build_runtime:
|
||||
name: Build Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
base_image:
|
||||
- image: 'nikolaik/python-nodejs:python3.12-nodejs22'
|
||||
tag: nikolaik
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Create source distribution and Dockerfile
|
||||
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
|
||||
- name: Build and push runtime image ${{ matrix.base_image.image }}
|
||||
if: github.event.pull_request.head.repo.fork != true
|
||||
run: |
|
||||
./containers/build.sh -i runtime -o ${{ github.repository_owner }} --push -t ${{ matrix.base_image.tag }}
|
||||
# Forked repos can't push to GHCR, so we need to upload the image as an artifact
|
||||
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: ghcr.io/all-hands-ai/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
context: containers/runtime
|
||||
- name: Upload runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
path: /tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
|
||||
verify_hash_equivalence_in_runtime_and_app:
|
||||
name: Verify Hash Equivalence in Runtime and Docker images
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime, ghcr_build_app]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
base_image: ['nikolaik']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Get hash in App Image
|
||||
run: |
|
||||
echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
|
||||
echo "hash_from_app_image=${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Get hash using code (development mode)
|
||||
run: |
|
||||
mkdir -p containers/runtime
|
||||
poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild > output.txt 2>&1
|
||||
hash_from_code=$(cat output.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
|
||||
echo "hash_from_code=$hash_from_code" >> $GITHUB_ENV
|
||||
|
||||
- name: Compare hashes
|
||||
run: |
|
||||
echo "Hash from App Image: ${{ env.hash_from_app_image }}"
|
||||
echo "Hash from Code: ${{ env.hash_from_code }}"
|
||||
if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then
|
||||
echo "Hashes match!"
|
||||
else
|
||||
echo "Hashes do not match!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run unit tests with the EventStream runtime Docker images as root
|
||||
test_runtime_root:
|
||||
name: RT Unit Tests (Root)
|
||||
needs: [ghcr_build_runtime]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
base_image: ['nikolaik']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# Forked repos can't push to GHCR, so we need to download the image as an artifact
|
||||
- name: Download runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run runtime tests
|
||||
run: |
|
||||
# We install pytest-xdist in order to run tests across CPUs
|
||||
poetry run pip install pytest-xdist
|
||||
|
||||
# Install to be able to retry on failures for flaky tests
|
||||
poetry run pip install pytest-rerunfailures
|
||||
|
||||
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 \
|
||||
TEST_IN_CI=true \
|
||||
RUN_AS_OPENHANDS=false \
|
||||
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
# Run unit tests with the EventStream runtime Docker images as openhands user
|
||||
test_runtime_oh:
|
||||
name: RT Unit Tests (openhands)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime]
|
||||
strategy:
|
||||
matrix:
|
||||
base_image: ['nikolaik']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# Forked repos can't push to GHCR, so we need to download the image as an artifact
|
||||
- name: Download runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-${{ matrix.base_image }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run runtime tests
|
||||
run: |
|
||||
# We install pytest-xdist in order to run tests across CPUs
|
||||
poetry run pip install pytest-xdist
|
||||
|
||||
# Install to be able to retry on failures for flaky tests
|
||||
poetry run pip install pytest-rerunfailures
|
||||
|
||||
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 \
|
||||
TEST_IN_CI=true \
|
||||
RUN_AS_OPENHANDS=true \
|
||||
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
|
||||
# "All Runtime Tests Passed" is a required job for PRs to merge
|
||||
# Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
|
||||
# prerequisites have been cancelled or failed so merging is disallowed, otherwise Github considers "skipped" as "success"
|
||||
runtime_tests_check_success:
|
||||
name: All Runtime Tests Passed
|
||||
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
|
||||
steps:
|
||||
- name: All tests passed
|
||||
run: echo "All runtime tests have passed successfully!"
|
||||
|
||||
runtime_tests_check_fail:
|
||||
name: All Runtime Tests Passed
|
||||
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
|
||||
steps:
|
||||
- name: Some tests failed
|
||||
run: |
|
||||
echo "Some runtime tests failed or were cancelled"
|
||||
exit 1
|
||||
update_pr_description:
|
||||
name: Update PR Description
|
||||
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
|
||||
needs: [ghcr_build_runtime]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: short_sha
|
||||
run: echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update PR Description
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
|
||||
run: |
|
||||
echo "updating PR description"
|
||||
DOCKER_RUN_COMMAND="docker run -it --rm \
|
||||
-p 3000:3000 \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
|
||||
--name openhands-app-$SHORT_SHA \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA"
|
||||
|
||||
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body)
|
||||
|
||||
if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then
|
||||
UPDATED_PR_BODY=$(echo "${PR_BODY}" | sed -E "s|docker run -it --rm.*|$DOCKER_RUN_COMMAND|")
|
||||
else
|
||||
UPDATED_PR_BODY="${PR_BODY}
|
||||
|
||||
---
|
||||
|
||||
To run this PR locally, use the following command:
|
||||
\`\`\`
|
||||
$DOCKER_RUN_COMMAND
|
||||
\`\`\`"
|
||||
fi
|
||||
|
||||
echo "updated body: $UPDATED_PR_BODY"
|
||||
gh pr edit $PR_NUMBER --body "$UPDATED_PR_BODY"
|
||||
61
.github/workflows/ghcr_app.yml
vendored
Normal file
61
.github/workflows/ghcr_app.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Workflow that builds, tests and then pushes the app docker images to the ghcr.io repository
|
||||
name: Build and Publish App Image
|
||||
|
||||
|
||||
# Always run on "main"
|
||||
# Always run on tags
|
||||
# Always run on PRs
|
||||
# Can also be triggered manually
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
# Builds the OpenHands Docker images
|
||||
ghcr_build:
|
||||
name: Build App Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and export image
|
||||
id: build
|
||||
run: ./containers/build.sh openhands ${{ github.repository_owner }} --push
|
||||
168
.github/workflows/ghcr_runtime.yml
vendored
Normal file
168
.github/workflows/ghcr_runtime.yml
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
# Workflow that builds, tests and then pushes the runtime docker images to the ghcr.io repository
|
||||
name: Build, Test and Publish Runtime Image
|
||||
|
||||
# Only run one workflow of the same group at a time.
|
||||
# There can be at most one running and one pending job in a concurrency group at any time.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
# Builds the runtime Docker images
|
||||
ghcr_build_runtime:
|
||||
name: Build Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
base_image: ['nikolaik/python-nodejs:python3.11-nodejs22', 'python:3.11-bookworm', 'node:22-bookworm']
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: true
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Create source distribution and Dockerfile
|
||||
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image }} --build_folder containers/runtime --force_rebuild
|
||||
- name: Build and export image
|
||||
id: build
|
||||
run: |
|
||||
suffix=$(echo "${{ matrix.base_image }}" | cut -d ':' -f 1 | cut -d '/' -f 1)
|
||||
./containers/build.sh runtime ${{ github.repository_owner }} --push $suffix
|
||||
|
||||
# Run unit tests with the EventStream runtime Docker images
|
||||
test_runtime:
|
||||
name: Test Runtime
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime]
|
||||
strategy:
|
||||
matrix:
|
||||
base_image: ['nikolaik', 'python', 'node']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run runtime tests
|
||||
run: |
|
||||
git_hash=$(git rev-parse --short "$GITHUB_SHA")
|
||||
image_name=ghcr.io/${{ github.repository_owner }}/runtime:$git_hash-${{ matrix.base_image }}
|
||||
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
TEST_RUNTIME=eventstream \
|
||||
SANDBOX_USER_ID=$(id -u) \
|
||||
SANDBOX_BASE_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
poetry run pytest --cov=agenthub --cov=openhands --cov-report=xml -s ./tests/runtime
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
# Run integration tests with the eventstream runtime Docker image
|
||||
runtime_integration_tests_on_linux:
|
||||
name: Runtime Integration Tests on Linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
base_image: ['nikolaik', 'python', 'node']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
git_hash=$(git rev-parse --short "$GITHUB_SHA")
|
||||
image_name=ghcr.io/${{ github.repository_owner }}/runtime:$git_hash-${{ matrix.base_image }}
|
||||
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
TEST_RUNTIME=eventstream \
|
||||
SANDBOX_USER_ID=$(id -u) \
|
||||
SANDBOX_BASE_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
TEST_ONLY=true \
|
||||
./tests/integration/regenerate.sh
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
# Checks that all runtime tests have passed
|
||||
all_runtime_tests_passed:
|
||||
name: All Runtime Tests Passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime, runtime_integration_tests_on_linux]
|
||||
steps:
|
||||
- name: All tests passed
|
||||
run: echo "All runtime tests have passed successfully!"
|
||||
9
.github/workflows/lint.yml
vendored
9
.github/workflows/lint.yml
vendored
@@ -10,11 +10,6 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Run lint on the frontend code
|
||||
lint-frontend:
|
||||
@@ -46,9 +41,9 @@ jobs:
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit==3.7.0
|
||||
- name: Run pre-commit hooks
|
||||
run: pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
run: pre-commit run --files openhands/**/* agenthub/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
15
.github/workflows/openhands-resolver.yml
vendored
15
.github/workflows/openhands-resolver.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Resolve Issues with OpenHands
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
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
|
||||
96
.github/workflows/py-unit-tests-mac.yml
vendored
96
.github/workflows/py-unit-tests-mac.yml
vendored
@@ -1,96 +0,0 @@
|
||||
# Workflow that runs python unit tests on mac
|
||||
name: Run Python Unit Tests Mac
|
||||
|
||||
# This job is flaky so only run it nightly
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
# Run python unit tests on macOS
|
||||
test-on-macos:
|
||||
name: Python Unit Tests on macOS
|
||||
runs-on: macos-14
|
||||
env:
|
||||
INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
~/.virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
- name: Install & Start Docker
|
||||
if: env.INSTALL_DOCKER == '1'
|
||||
run: |
|
||||
INSTANCE_NAME="colima-${GITHUB_RUN_ID}"
|
||||
|
||||
# Uninstall colima to upgrade to the latest version
|
||||
if brew list colima &>/dev/null; then
|
||||
brew uninstall colima
|
||||
# unlinking colima dependency: go
|
||||
brew uninstall go@1.21
|
||||
fi
|
||||
rm -rf ~/.colima ~/.lima
|
||||
brew install --HEAD colima
|
||||
brew install docker
|
||||
|
||||
start_colima() {
|
||||
# Find a free port in the range 10000-20000
|
||||
RANDOM_PORT=$((RANDOM % 10001 + 10000))
|
||||
|
||||
# Original line:
|
||||
if ! colima start --network-address --arch x86_64 --cpu=1 --memory=1 --verbose --ssh-port $RANDOM_PORT; then
|
||||
echo "Failed to start Colima."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Attempt to start Colima for 5 total attempts:
|
||||
ATTEMPT_LIMIT=5
|
||||
for ((i=1; i<=ATTEMPT_LIMIT; i++)); do
|
||||
|
||||
if start_colima; then
|
||||
echo "Colima started successfully."
|
||||
break
|
||||
else
|
||||
colima stop -f
|
||||
sleep 10
|
||||
colima delete -f
|
||||
if [ $i -eq $ATTEMPT_LIMIT ]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
|
||||
# For testcontainers to find the Colima socket
|
||||
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
|
||||
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --forked --cov=openhands --cov-report=xml ./tests/unit --ignore=tests/unit/test_memory.py
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
89
.github/workflows/py-unit-tests.yml
vendored
89
.github/workflows/py-unit-tests.yml
vendored
@@ -10,12 +10,84 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Run python unit tests on macOS
|
||||
test-on-macos:
|
||||
name: Python Unit Tests on macOS
|
||||
runs-on: macos-12
|
||||
env:
|
||||
INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --without evaluation,llama-index
|
||||
- name: Install & Start Docker
|
||||
if: env.INSTALL_DOCKER == '1'
|
||||
run: |
|
||||
INSTANCE_NAME="colima-${GITHUB_RUN_ID}"
|
||||
|
||||
# Uninstall colima to upgrade to the latest version
|
||||
if brew list colima &>/dev/null; then
|
||||
brew uninstall colima
|
||||
# unlinking colima dependency: go
|
||||
brew uninstall go@1.21
|
||||
fi
|
||||
rm -rf ~/.colima ~/.lima
|
||||
brew install --HEAD colima
|
||||
brew install docker
|
||||
|
||||
start_colima() {
|
||||
# Find a free port in the range 10000-20000
|
||||
RANDOM_PORT=$((RANDOM % 10001 + 10000))
|
||||
|
||||
# Original line:
|
||||
if ! colima start --network-address --arch x86_64 --cpu=1 --memory=1 --verbose --ssh-port $RANDOM_PORT; then
|
||||
echo "Failed to start Colima."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Attempt to start Colima for 5 total attempts:
|
||||
ATTEMPT_LIMIT=5
|
||||
for ((i=1; i<=ATTEMPT_LIMIT; i++)); do
|
||||
|
||||
if start_colima; then
|
||||
echo "Colima started successfully."
|
||||
break
|
||||
else
|
||||
colima stop -f
|
||||
sleep 10
|
||||
colima delete -f
|
||||
if [ $i -eq $ATTEMPT_LIMIT ]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
|
||||
# For testcontainers to find the Colima socket
|
||||
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
|
||||
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --forked --cov=agenthub --cov=openhands --cov-report=xml ./tests/unit
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
# Run python unit tests on Linux
|
||||
test-on-linux:
|
||||
name: Python Unit Tests on Linux
|
||||
@@ -24,12 +96,9 @@ jobs:
|
||||
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
python-version: ['3.11']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
@@ -42,7 +111,7 @@ jobs:
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --forked --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
|
||||
run: poetry run pytest --forked --cov=agenthub --cov=openhands --cov-report=xml ./tests/unit
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
|
||||
31
.github/workflows/pypi-release.yml
vendored
31
.github/workflows/pypi-release.yml
vendored
@@ -1,31 +0,0 @@
|
||||
# Publishes the OpenHands PyPi package
|
||||
name: Publish PyPi Package
|
||||
|
||||
# Triggered manually
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1.4.1
|
||||
with:
|
||||
virtualenvs-in-project: true
|
||||
virtualenvs-path: ~/.virtualenvs
|
||||
- name: Install Poetry Dependencies
|
||||
run: poetry install --no-interaction --no-root
|
||||
- name: Build poetry project
|
||||
run: ./build.sh
|
||||
- name: publish
|
||||
run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
|
||||
5
.github/workflows/review-pr.yml
vendored
5
.github/workflows/review-pr.yml
vendored
@@ -15,13 +15,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.11'
|
||||
- name: install git, github cli
|
||||
run: |
|
||||
sudo apt-get install -y git gh
|
||||
|
||||
113
.github/workflows/solve-issue.yml
vendored
Normal file
113
.github/workflows/solve-issue.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
# Workflow that uses OpenHands to resolve a GitHub issue. Issue must be labeled 'solve-this'
|
||||
name: Use OpenHands to Resolve GitHub Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
dogfood:
|
||||
if: github.event.label.name == 'solve-this'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/all-hands-ai/openhands
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
steps:
|
||||
- name: install git, github cli
|
||||
run: apt-get install -y git gh
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Write Task File
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
echo "TITLE:" > task.txt
|
||||
echo "${ISSUE_TITLE}" >> task.txt
|
||||
echo "" >> task.txt
|
||||
echo "BODY:" >> task.txt
|
||||
echo "${ISSUE_BODY}" >> task.txt
|
||||
- name: Set up environment
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
export PATH="/github/home/.local/bin:$PATH"
|
||||
poetry install --without evaluation,llama-index
|
||||
poetry run playwright install --with-deps chromium
|
||||
- name: Run OpenHands
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
LLM_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
# Append path to launch poetry
|
||||
export PATH="/github/home/.local/bin:$PATH"
|
||||
# Append path to correctly import package, note: must set pwd at first
|
||||
export PYTHONPATH=$(pwd):$PYTHONPATH
|
||||
WORKSPACE_MOUNT_PATH=$GITHUB_WORKSPACE poetry run python ./openhands/core/main.py -i 50 -f task.txt -d $GITHUB_WORKSPACE
|
||||
rm task.txt
|
||||
- name: Setup Git, Create Branch, and Commit Changes
|
||||
run: |
|
||||
# Setup Git configuration
|
||||
git config --global --add safe.directory $PWD
|
||||
git config --global user.name 'OpenHands'
|
||||
git config --global user.email 'OpenHands@users.noreply.github.com'
|
||||
|
||||
# Create a unique branch name with a timestamp
|
||||
BRANCH_NAME="fix/${{ github.event.issue.number }}-$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
# Checkout new branch
|
||||
git checkout -b $BRANCH_NAME
|
||||
|
||||
# Add all changes to staging, except task.txt
|
||||
git add --all -- ':!task.txt'
|
||||
|
||||
# Commit the changes, if any
|
||||
git commit -m "OpenHands: Resolve Issue #${{ github.event.issue.number }}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "No changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Push changes
|
||||
git push --set-upstream origin $BRANCH_NAME
|
||||
- name: Fetch Default Branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fetch the default branch using gh cli
|
||||
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
|
||||
echo "Default branch is $DEFAULT_BRANCH"
|
||||
echo "DEFAULT_BRANCH=$DEFAULT_BRANCH" >> $GITHUB_ENV
|
||||
- name: Generate PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Create PR and capture URL
|
||||
PR_URL=$(gh pr create \
|
||||
--title "OpenHands: Resolve Issue #2" \
|
||||
--body "This PR was generated by OpenHands to resolve issue #2" \
|
||||
--repo "foragerr/OpenHands" \
|
||||
--head "${{ github.head_ref }}" \
|
||||
--base "${{ env.DEFAULT_BRANCH }}" \
|
||||
| grep -o 'https://github.com/[^ ]*')
|
||||
|
||||
# Extract PR number from URL
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -o '[0-9]\+$')
|
||||
|
||||
# Set environment vars
|
||||
echo "PR_URL=$PR_URL" >> $GITHUB_ENV
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
|
||||
- name: Post Comment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh issue comment ${{ github.event.issue.number }} \
|
||||
-b "OpenHands raised [PR #${{ env.PR_NUMBER }}](${{ env.PR_URL }}) to resolve this issue."
|
||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -15,7 +15,6 @@ jobs:
|
||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
days-before-stale: 30
|
||||
exempt-issue-labels: 'tracked'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
|
||||
days-before-close: 7
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -121,7 +121,6 @@ celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
frontend/.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@@ -174,11 +173,11 @@ evaluation/bird/data
|
||||
evaluation/gaia/data
|
||||
evaluation/gorilla/data
|
||||
evaluation/toolqa/data
|
||||
evaluation/scienceagentbench/benchmark
|
||||
|
||||
# frontend
|
||||
|
||||
# dependencies
|
||||
frontend/node_modules
|
||||
frontend/.pnp
|
||||
frontend/bun.lockb
|
||||
frontend/yarn.lock
|
||||
@@ -218,6 +217,8 @@ config.toml
|
||||
config.toml_
|
||||
config.toml.bak
|
||||
|
||||
containers/agnostic_sandbox
|
||||
|
||||
# swe-bench-eval
|
||||
image_build_logs
|
||||
run_instance_logs
|
||||
@@ -227,5 +228,3 @@ runtime_*.tar
|
||||
# docker build
|
||||
containers/runtime/Dockerfile
|
||||
containers/runtime/project.tar.gz
|
||||
containers/runtime/code
|
||||
**/node_modules/
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
OpenHands is an automated AI software engineer. It is a repo with a Python backend
|
||||
(in the `openhands` directory) and TypeScript frontend (in the `frontend` directory).
|
||||
|
||||
General Setup:
|
||||
- To set up the entire repo, including frontend and backend, run `make build`
|
||||
- To run linting and type-checking before finishing the job, run `poetry run pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml`
|
||||
|
||||
Backend:
|
||||
- Located in the `openhands` directory
|
||||
- Testing:
|
||||
- All tests are in `tests/unit/test_*.py`
|
||||
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
|
||||
- Write all tests with pytest
|
||||
|
||||
Frontend:
|
||||
- Located in the `frontend` directory
|
||||
- Prerequisites: A recent version of NodeJS / NPM
|
||||
- Setup: Run `npm install` in the frontend directory
|
||||
- Testing:
|
||||
- Run tests: `npm run test`
|
||||
- To run specific tests: `npm run test -- -t "TestName"`
|
||||
- Building:
|
||||
- Build for production: `npm run build`
|
||||
- Environment Variables:
|
||||
- Set in `frontend/.env` or as environment variables
|
||||
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
|
||||
- Internationalization:
|
||||
- Generate i18n declaration file: `npm run make-i18n`
|
||||
130
CONTRIBUTING.md
130
CONTRIBUTING.md
@@ -2,70 +2,95 @@
|
||||
|
||||
Thanks for your interest in contributing to OpenHands! We welcome and appreciate contributions.
|
||||
|
||||
## Understanding OpenHands's CodeBase
|
||||
|
||||
To understand the codebase, please refer to the README in each module:
|
||||
- [frontend](./frontend/README.md)
|
||||
- [evaluation](./evaluation/README.md)
|
||||
- [openhands](./openhands/README.md)
|
||||
- [agenthub](./openhands/agenthub/README.md)
|
||||
- [server](./openhands/server/README.md)
|
||||
|
||||
## Setting up your development environment
|
||||
|
||||
We have a separate doc [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) that tells you how to set up a development workflow.
|
||||
|
||||
## How can I contribute?
|
||||
## How Can I Contribute?
|
||||
|
||||
There are many ways that you can contribute:
|
||||
|
||||
1. **Download and use** OpenHands, and send [issues](https://github.com/All-Hands-AI/OpenHands/issues) when you encounter something that isn't working or a feature that you'd like to see.
|
||||
2. **Send feedback** after each session by [clicking the thumbs-up thumbs-down buttons](https://docs.all-hands.dev/modules/usage/feedback), so we can see where things are working and failing, and also build an open dataset for training code agents.
|
||||
3. **Improve the Codebase** by sending PRs (see details below). In particular, we have some [good first issues](https://github.com/All-Hands-AI/OpenHands/labels/good%20first%20issue) that may be ones to start on.
|
||||
3. **Improve the Codebase** by sending PRs (see details below). In particular, we have some [good first issue](https://github.com/All-Hands-AI/OpenHands/labels/good%20first%20issue) issues that may be ones to start on.
|
||||
|
||||
## What can I build?
|
||||
Here are a few ways you can help improve the codebase.
|
||||
## Understanding OpenHands's CodeBase
|
||||
|
||||
#### UI/UX
|
||||
We're always looking to improve the look and feel of the application. If you've got a small fix
|
||||
for something that's bugging you, feel free to open up a PR that changes the `./frontend` directory.
|
||||
To understand the codebase, please refer to the README in each module:
|
||||
- [frontend](./frontend/README.md)
|
||||
- [agenthub](./agenthub/README.md)
|
||||
- [evaluation](./evaluation/README.md)
|
||||
- [openhands](./openhands/README.md)
|
||||
- [server](./openhands/server/README.md)
|
||||
|
||||
If you're looking to make a bigger change, add a new UI element, or significantly alter the style
|
||||
of the application, please open an issue first, or better, join the #frontend channel in our Slack
|
||||
to gather consensus from our design team first.
|
||||
|
||||
#### Improving the agent
|
||||
Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub/codeact_agent)
|
||||
|
||||
Changes to these prompts, and to the underlying behavior in Python, can have a huge impact on user experience.
|
||||
You can try modifying the prompts to see how they change the behavior of the agent as you use the app
|
||||
locally, but we will need to do an end-to-end evaluation of any changes here to ensure that the agent
|
||||
is getting better over time.
|
||||
|
||||
We use the [SWE-bench](https://www.swebench.com/) benchmark to test our agent. You can join the #evaluation
|
||||
channel in Slack to learn more.
|
||||
|
||||
#### Adding a new agent
|
||||
You may want to experiment with building new types of agents. You can add an agent to `openhands/agenthub`
|
||||
to help expand the capabilities of OpenHands.
|
||||
|
||||
#### Adding a new runtime
|
||||
The agent needs a place to run code and commands. When you run OpenHands on your laptop, it uses a Docker container
|
||||
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).
|
||||
|
||||
#### Testing
|
||||
When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites.
|
||||
At the moment, we have two kinds of tests: `unit` and `integration`. Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
|
||||
|
||||
## Sending Pull Requests to OpenHands
|
||||
|
||||
You'll need to fork our repository to send us a Pull Request. You can learn more
|
||||
about how to fork a GitHub repo and open a PR with your changes in [this article](https://medium.com/swlh/forks-and-pull-requests-how-to-contribute-to-github-repos-8843fac34ce8)
|
||||
### 1. Fork the Official Repository
|
||||
Fork the [OpenHands repository](https://github.com/All-Hands-AI/OpenHands) into your own account.
|
||||
Clone your own forked repository into your local environment:
|
||||
|
||||
### Pull Request title
|
||||
```shell
|
||||
git clone git@github.com:<YOUR-USERNAME>/OpenHands.git
|
||||
```
|
||||
|
||||
### 2. Configure Git
|
||||
|
||||
Set the official repository as your [upstream](https://www.atlassian.com/git/tutorials/git-forks-and-upstreams) to synchronize with the latest update in the official repository.
|
||||
Add the original repository as upstream:
|
||||
|
||||
```shell
|
||||
cd OpenHands
|
||||
git remote add upstream git@github.com:All-Hands-AI/OpenHands.git
|
||||
```
|
||||
|
||||
Verify that the remote is set:
|
||||
|
||||
```shell
|
||||
git remote -v
|
||||
```
|
||||
|
||||
You should see both `origin` and `upstream` in the output.
|
||||
|
||||
### 3. Synchronize with Official Repository
|
||||
Synchronize latest commit with official repository before coding:
|
||||
|
||||
```shell
|
||||
git fetch upstream
|
||||
git checkout main
|
||||
git merge upstream/main
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 4. Set up the Development Environment
|
||||
|
||||
We have a separate doc [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) that tells you how to set up a development workflow.
|
||||
|
||||
### 5. Write Code and Commit It
|
||||
|
||||
Once you have done this, you can write code, test it, and commit it to a branch (replace `my_branch` with an appropriate name):
|
||||
|
||||
```shell
|
||||
git checkout -b my_branch
|
||||
git add .
|
||||
git commit
|
||||
git push origin my_branch
|
||||
```
|
||||
|
||||
### 6. Open a Pull Request
|
||||
|
||||
* On GitHub, go to the page of your forked repository, and create a Pull Request:
|
||||
- Click on `Branches`
|
||||
- Click on the `...` beside your branch and click on `New pull request`
|
||||
- Set `base repository` to `All-Hands-AI/OpenHands`
|
||||
- Set `base` to `main`
|
||||
- Click `Create pull request`
|
||||
|
||||
The PR should appear in [OpenHands PRs](https://github.com/All-Hands-AI/OpenHands/pulls).
|
||||
|
||||
Then the OpenHands team will review your code.
|
||||
|
||||
## PR Rules
|
||||
|
||||
### 1. Pull Request title
|
||||
As described [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes:
|
||||
|
||||
- `feat`: A new feature
|
||||
@@ -86,9 +111,6 @@ For example, a PR title could be:
|
||||
|
||||
You may also check out previous PRs in the [PR list](https://github.com/All-Hands-AI/OpenHands/pulls).
|
||||
|
||||
### Pull Request description
|
||||
### 2. Pull Request description
|
||||
- If your PR is small (such as a typo fix), you can go brief.
|
||||
- If it contains a lot of changes, it's better to write more details.
|
||||
|
||||
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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Contributors
|
||||
|
||||
We would like to thank all the [contributors](https://github.com/All-Hands-AI/OpenHands/graphs/contributors) who have helped make OpenHands possible. We greatly appreciate your dedication and hard work.
|
||||
We would like to thank all the [contributors](https://github.com/All-Hands-AI/OpenHands/graphs/contributors) who have helped make OpenHands possible. Your dedication and hard work are greatly appreciated.
|
||||
|
||||
## Open Source Projects
|
||||
|
||||
@@ -10,7 +10,7 @@ OpenHands includes and adapts the following open source projects. We are gratefu
|
||||
|
||||
#### [SWE Agent](https://github.com/princeton-nlp/swe-agent)
|
||||
- License: MIT License
|
||||
- Description: Adapted for use in OpenHands's agent hub
|
||||
- Description: Adapted for use in OpenHands's agenthub
|
||||
|
||||
#### [Aider](https://github.com/paul-gauthier/aider)
|
||||
- License: Apache License 2.0
|
||||
|
||||
@@ -5,14 +5,12 @@ Otherwise, you can clone the OpenHands project directly.
|
||||
|
||||
## Start the server for development
|
||||
### 1. Requirements
|
||||
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [Ubuntu <= 22.04]
|
||||
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [ Ubuntu <= 22.04]
|
||||
* [Docker](https://docs.docker.com/engine/install/) (For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
|
||||
* [Python](https://www.python.org/downloads/) = 3.12
|
||||
* [Python](https://www.python.org/downloads/) = 3.11
|
||||
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 18.17.1
|
||||
* [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
|
||||
* OS-specific dependencies:
|
||||
- Ubuntu: build-essential => `sudo apt-get install build-essential`
|
||||
- WSL: netcat => `sudo apt-get install netcat`
|
||||
* netcat => sudo apt-get install netcat
|
||||
|
||||
Make sure you have all these dependencies installed before moving on to `make build`.
|
||||
|
||||
@@ -24,8 +22,8 @@ If you want to develop without system admin/sudo access to upgrade/install `Pyth
|
||||
curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
|
||||
bash Miniforge3-$(uname)-$(uname -m).sh
|
||||
|
||||
# Install Python 3.12, nodejs, and poetry
|
||||
mamba install python=3.12
|
||||
# Install Python 3.11, nodejs, and poetry
|
||||
mamba install python=3.11
|
||||
mamba install conda-forge::nodejs
|
||||
mamba install conda-forge::poetry
|
||||
```
|
||||
@@ -93,36 +91,9 @@ To run tests, refer to the following:
|
||||
poetry run pytest ./tests/unit/test_*.py
|
||||
```
|
||||
|
||||
#### Integration tests
|
||||
Please refer to [this README](./tests/integration/README.md) for details.
|
||||
|
||||
### 9. Add or update dependency
|
||||
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`
|
||||
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
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
TL;DR
|
||||
|
||||
```bash
|
||||
make docker-dev
|
||||
```
|
||||
|
||||
See more details [here](./containers/dev/README.md)
|
||||
|
||||
If you are just interested in running `OpenHands` without installing all the required tools on your host.
|
||||
|
||||
```bash
|
||||
make docker-run
|
||||
```
|
||||
|
||||
If you do not have `make` on your host, run:
|
||||
|
||||
```bash
|
||||
cd ./containers/dev
|
||||
./dev.sh
|
||||
```
|
||||
|
||||
You do need [Docker](https://docs.docker.com/engine/install/) installed on your host though.
|
||||
|
||||
44
Makefile
44
Makefile
@@ -2,15 +2,14 @@ SHELL=/bin/bash
|
||||
# Makefile for OpenHands project
|
||||
|
||||
# Variables
|
||||
BACKEND_HOST ?= "127.0.0.1"
|
||||
BACKEND_PORT = 3000
|
||||
BACKEND_HOST_PORT = "$(BACKEND_HOST):$(BACKEND_PORT)"
|
||||
BACKEND_HOST = "127.0.0.1:$(BACKEND_PORT)"
|
||||
FRONTEND_PORT = 3001
|
||||
DEFAULT_WORKSPACE_DIR = "./workspace"
|
||||
DEFAULT_MODEL = "gpt-4o"
|
||||
CONFIG_FILE = config.toml
|
||||
PRE_COMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
|
||||
PYTHON_VERSION = 3.12
|
||||
PYTHON_VERSION = 3.11
|
||||
|
||||
# ANSI color codes
|
||||
GREEN=$(shell tput -Txterm setaf 2)
|
||||
@@ -190,12 +189,12 @@ build-frontend:
|
||||
# Start backend
|
||||
start-backend:
|
||||
@echo "$(YELLOW)Starting backend...$(RESET)"
|
||||
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "$(shell pwd)/workspace"
|
||||
@poetry run uvicorn openhands.server.listen:app --port $(BACKEND_PORT) --reload --reload-exclude "workspace/*"
|
||||
|
||||
# Start frontend
|
||||
start-frontend:
|
||||
@echo "$(YELLOW)Starting frontend...$(RESET)"
|
||||
@cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run dev -- --port $(FRONTEND_PORT) --host $(BACKEND_HOST)
|
||||
@cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start
|
||||
|
||||
# Common setup for running the app (non-callable)
|
||||
_run_setup:
|
||||
@@ -205,7 +204,7 @@ _run_setup:
|
||||
fi
|
||||
@mkdir -p logs
|
||||
@echo "$(YELLOW)Starting backend server...$(RESET)"
|
||||
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) &
|
||||
@poetry run uvicorn openhands.server.listen:app --port $(BACKEND_PORT) &
|
||||
@echo "$(YELLOW)Waiting for the backend to start...$(RESET)"
|
||||
@until nc -z localhost $(BACKEND_PORT); do sleep 0.1; done
|
||||
@echo "$(GREEN)Backend started successfully.$(RESET)"
|
||||
@@ -214,23 +213,9 @@ _run_setup:
|
||||
run:
|
||||
@echo "$(YELLOW)Running the app...$(RESET)"
|
||||
@$(MAKE) -s _run_setup
|
||||
@$(MAKE) -s start-frontend
|
||||
@cd frontend && echo "$(BLUE)Starting frontend with npm...$(RESET)" && npm run start -- --port $(FRONTEND_PORT)
|
||||
@echo "$(GREEN)Application started successfully.$(RESET)"
|
||||
|
||||
# Run the app (in docker)
|
||||
docker-run: WORKSPACE_BASE ?= $(PWD)/workspace
|
||||
docker-run:
|
||||
@if [ -f /.dockerenv ]; then \
|
||||
echo "Running inside a Docker container. Exiting..."; \
|
||||
exit 0; \
|
||||
else \
|
||||
echo "$(YELLOW)Running the app in Docker $(OPTIONS)...$(RESET)"; \
|
||||
export WORKSPACE_BASE=${WORKSPACE_BASE}; \
|
||||
export SANDBOX_USER_ID=$(shell id -u); \
|
||||
export DATE=$(shell date +%Y%m%d%H%M%S); \
|
||||
docker compose up $(OPTIONS); \
|
||||
fi
|
||||
|
||||
# Run the app (WSL mode)
|
||||
run-wsl:
|
||||
@echo "$(YELLOW)Running the app in WSL mode...$(RESET)"
|
||||
@@ -275,10 +260,6 @@ setup-config-prompts:
|
||||
echo " - nomic-embed-text"; \
|
||||
echo " - all-minilm"; \
|
||||
echo " - stable-code"; \
|
||||
echo " - bge-m3"; \
|
||||
echo " - bge-large"; \
|
||||
echo " - paraphrase-multilingual"; \
|
||||
echo " - snowflake-arctic-embed"; \
|
||||
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
|
||||
read -p "> " llm_embedding_model; \
|
||||
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
|
||||
@@ -295,16 +276,6 @@ setup-config-prompts:
|
||||
fi
|
||||
|
||||
|
||||
# Develop in container
|
||||
docker-dev:
|
||||
@if [ -f /.dockerenv ]; then \
|
||||
echo "Running inside a Docker container. Exiting..."; \
|
||||
exit 0; \
|
||||
else \
|
||||
echo "$(YELLOW)Build and run in Docker $(OPTIONS)...$(RESET)"; \
|
||||
./containers/dev/dev.sh $(OPTIONS); \
|
||||
fi
|
||||
|
||||
# Clean up all caches
|
||||
clean:
|
||||
@echo "$(YELLOW)Cleaning up caches...$(RESET)"
|
||||
@@ -323,10 +294,7 @@ help:
|
||||
@echo " $(GREEN)start-frontend$(RESET) - Start the frontend server for the OpenHands project."
|
||||
@echo " $(GREEN)run$(RESET) - Run the OpenHands application, starting both backend and frontend servers."
|
||||
@echo " Backend Log file will be stored in the 'logs' directory."
|
||||
@echo " $(GREEN)docker-dev$(RESET) - Build and run the OpenHands application in Docker."
|
||||
@echo " $(GREEN)docker-run$(RESET) - Run the OpenHands application, starting both backend and frontend servers in Docker."
|
||||
@echo " $(GREEN)help$(RESET) - Display this help message, providing information on available targets."
|
||||
|
||||
# Phony targets
|
||||
.PHONY: build check-dependencies check-python check-npm check-docker check-poetry install-python-dependencies install-frontend-dependencies install-pre-commit-hooks lint start-backend start-frontend run run-wsl setup-config setup-config-prompts help
|
||||
.PHONY: docker-dev docker-run
|
||||
|
||||
123
README.md
123
README.md
@@ -1,80 +1,98 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
|
||||
<h1 align="center">OpenHands: Code Less, Make More</h1>
|
||||
</div>
|
||||
<!--
|
||||
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
||||
*** that would make this better, please fork the repo and create a pull request
|
||||
*** or simply open an issue with the tag "enhancement".
|
||||
*** Don't forget to give the project a star!
|
||||
*** Thanks again! Now go create something AMAZING! :D
|
||||
-->
|
||||
|
||||
<!-- PROJECT SHIELDS -->
|
||||
<!--
|
||||
*** I'm using markdown "reference style" links for readability.
|
||||
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
||||
*** See the bottom of this document for the declaration of the reference variables
|
||||
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
||||
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
||||
-->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/network/members"><img src="https://img.shields.io/github/forks/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Forks"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/issues"><img src="https://img.shields.io/github/issues/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Issues"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=blue" alt="Credits"></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
|
||||
<br/>
|
||||
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
|
||||
<a href="https://huggingface.co/spaces/OpenHands/evaluation"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
|
||||
<hr>
|
||||
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge"></a>
|
||||
</div>
|
||||
|
||||
Welcome to OpenHands (formerly OpenDevin), a platform for software development agents powered by AI.
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200" height="200">
|
||||
<h1 align="center">OpenHands: Code Less, Make More</h1>
|
||||
<a href="https://docs.all-hands.dev/modules/usage/intro"><img src="https://img.shields.io/badge/Documentation-OpenHands-blue?logo=googledocs&logoColor=white&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper-%20on%20Arxiv-red?logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
|
||||
<br/>
|
||||
<a href="https://huggingface.co/spaces/OpenHands/evaluation"><img src="https://img.shields.io/badge/Evaluation-Benchmark%20on%20HF%20Space-green?logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark"></a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
|
||||
call APIs, and yes—even copy code snippets from StackOverflow.
|
||||
Welcome to OpenHands, a platform for autonomous software engineers, powered by AI and LLMs (previously called "OpenDevin").
|
||||
|
||||
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [Quick Start](#-quick-start).
|
||||
OpenHands agents collaborate with human developers to write code, fix bugs, and ship features.
|
||||
|
||||

|
||||
|
||||
## ⚡ Quick Start
|
||||
## ⚡ Getting Started
|
||||
OpenHands works best with Docker version 26.0.0+ (Docker Desktop 4.31.0+).
|
||||
You must be using Linux, Mac OS, or WSL on Windows.
|
||||
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
See the [Installation](https://docs.all-hands.dev/modules/usage/installation) guide for
|
||||
system requirements and more information.
|
||||
To start OpenHands in a docker container, run the following commands in your terminal:
|
||||
|
||||
> [!WARNING]
|
||||
> When you run the following command, files in `./workspace` may be modified or deleted.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
|
||||
|
||||
docker run -it --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
WORKSPACE_BASE=$(pwd)/workspace
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-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
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
ghcr.io/all-hands-ai/openhands:0.9
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
> [!NOTE]
|
||||
> This command pulls the `0.9` tag, which represents the most recent stable release of OpenHands. You have other options as well:
|
||||
> - For a specific release version, use `ghcr.io/all-hands-ai/openhands:<OpenHands_version>` (replace <OpenHands_version> with the desired version number).
|
||||
> - For the most up-to-date development version, use `ghcr.io/all-hands-ai/openhands:main`. This version may be **(unstable!)** and is recommended for testing or development purposes only.
|
||||
>
|
||||
> Choose the tag that best suits your needs based on stability requirements and desired features.
|
||||
|
||||
Finally, you'll need a model provider and API key.
|
||||
[Anthropic's Claude 3.5 Sonnet](https://www.anthropic.com/api) (`anthropic/claude-3-5-sonnet-20241022`)
|
||||
works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000) with access to `./workspace`. To have OpenHands operate on your code, place it in `./workspace`.
|
||||
OpenHands will only have access to this workspace folder. The rest of your system will not be affected as it runs in a secured docker sandbox.
|
||||
|
||||
---
|
||||
Upon opening OpenHands, you must select the appropriate `Model` and enter the `API Key` within the settings that should pop up automatically. These can be set at any time by selecting
|
||||
the `Settings` button (gear icon) in the UI. If the required `Model` does not exist in the list, you can manually enter it in the text box.
|
||||
|
||||
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).
|
||||
For the development workflow, see [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
|
||||
Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
|
||||
Are you having trouble? Check out our [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting).
|
||||
|
||||
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
|
||||
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting) can help.
|
||||
|
||||
## 📖 Documentation
|
||||
## 🚀 Documentation
|
||||
|
||||
To learn more about the project, and for tips on using OpenHands,
|
||||
**check out our [documentation](https://docs.all-hands.dev/modules/usage/getting-started)**.
|
||||
**check out our [documentation](https://docs.all-hands.dev/modules/usage/intro)**.
|
||||
|
||||
There you'll find resources on how to use different LLM providers,
|
||||
There you'll find resources on how to use different LLM providers (like ollama and Anthropic's Claude),
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
## 🤝 How to Contribute
|
||||
@@ -94,7 +112,7 @@ For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
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.
|
||||
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA) - 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.
|
||||
|
||||
## 📈 Progress
|
||||
@@ -109,6 +127,17 @@ Let's make software engineering better together!
|
||||
|
||||
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[contributors-url]: https://github.com/All-Hands-AI/OpenHands/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[forks-url]: https://github.com/All-Hands-AI/OpenHands/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[stars-url]: https://github.com/All-Hands-AI/OpenHands/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[issues-url]: https://github.com/All-Hands-AI/OpenHands/issues
|
||||
[license-shield]: https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge
|
||||
[license-url]: https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
OpenHands is built by a large number of contributors, and every contribution is greatly appreciated! We also build upon other open source projects, and we are deeply thankful for their work.
|
||||
@@ -118,8 +147,8 @@ For a list of open source projects and licenses used in OpenHands, please see ou
|
||||
## 📚 Cite
|
||||
|
||||
```
|
||||
@misc{openhands,
|
||||
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
|
||||
@misc{opendevin,
|
||||
title={{OpenDevin: An Open Platform for AI Software Developers as Generalist Agents}},
|
||||
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
|
||||
year={2024},
|
||||
eprint={2407.16741},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In this folder, there may exist multiple implementations of `Agent` that will be used by the framework.
|
||||
|
||||
For example, `openhands/agenthub/codeact_agent`, etc.
|
||||
For example, `agenthub/codeact_agent`, etc.
|
||||
Contributors from different backgrounds and interests can choose to contribute to any (or all!) of these directions.
|
||||
|
||||
## Constructing an Agent
|
||||
@@ -1,13 +1,13 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from openhands.agenthub.micro.agent import MicroAgent
|
||||
from openhands.agenthub.micro.registry import all_microagents
|
||||
from agenthub.micro.agent import MicroAgent
|
||||
from agenthub.micro.registry import all_microagents
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
from openhands.agenthub import ( # noqa: E402
|
||||
from agenthub import ( # noqa: E402
|
||||
browsing_agent,
|
||||
codeact_agent,
|
||||
codeact_swe_agent,
|
||||
@@ -12,5 +12,5 @@ poetry run python ./openhands/core/main.py \
|
||||
-i 10 \
|
||||
-t "tell me the usa's president using google search" \
|
||||
-c BrowsingAgent \
|
||||
-m claude-3-5-sonnet-20241022
|
||||
-m gpt-4o-2024-05-13
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.browsing_agent.browsing_agent import BrowsingAgent
|
||||
from agenthub.browsing_agent.browsing_agent import BrowsingAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('BrowsingAgent', BrowsingAgent)
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from browsergym.utils.obs import flatten_axtree_to_str
|
||||
|
||||
from openhands.agenthub.browsing_agent.response_parser import BrowsingResponseParser
|
||||
from agenthub.browsing_agent.response_parser import BrowsingResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
@@ -65,15 +65,10 @@ In order to accomplish my goal I need to send the information asked back to the
|
||||
"""
|
||||
|
||||
|
||||
def get_prompt(
|
||||
error_prefix: str, cur_url: str, cur_axtree_txt: str, prev_action_str: str
|
||||
) -> str:
|
||||
def get_prompt(error_prefix: str, cur_axtree_txt: str, prev_action_str: str) -> str:
|
||||
prompt = f"""\
|
||||
{error_prefix}
|
||||
|
||||
# Current Page URL:
|
||||
{cur_url}
|
||||
|
||||
# Current Accessibility Tree:
|
||||
{cur_axtree_txt}
|
||||
|
||||
@@ -144,19 +139,18 @@ class BrowsingAgent(Agent):
|
||||
"""
|
||||
messages: list[Message] = []
|
||||
prev_actions = []
|
||||
cur_url = ''
|
||||
cur_axtree_txt = ''
|
||||
error_prefix = ''
|
||||
last_obs = None
|
||||
last_action = None
|
||||
|
||||
if EVAL_MODE and len(state.history) == 1:
|
||||
if EVAL_MODE and len(state.history.get_events_as_list()) == 1:
|
||||
# for webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
|
||||
# initialize and retrieve the first observation by issuing an noop OP
|
||||
# For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
|
||||
return BrowseInteractiveAction(browser_actions='noop()')
|
||||
|
||||
for event in state.history:
|
||||
for event in state.history.get_events():
|
||||
if isinstance(event, BrowseInteractiveAction):
|
||||
prev_actions.append(event.browser_actions)
|
||||
last_action = event
|
||||
@@ -185,9 +179,6 @@ class BrowsingAgent(Agent):
|
||||
self.error_accumulator += 1
|
||||
if self.error_accumulator > 5:
|
||||
return MessageAction('Too many errors encountered. Task failed.')
|
||||
|
||||
cur_url = last_obs.url
|
||||
|
||||
try:
|
||||
cur_axtree_txt = flatten_axtree_to_str(
|
||||
last_obs.axtree_object,
|
||||
@@ -213,11 +204,12 @@ class BrowsingAgent(Agent):
|
||||
|
||||
messages.append(Message(role='system', content=[TextContent(text=system_msg)]))
|
||||
|
||||
prompt = get_prompt(error_prefix, cur_url, cur_axtree_txt, prev_action_str)
|
||||
prompt = get_prompt(error_prefix, cur_axtree_txt, prev_action_str)
|
||||
messages.append(Message(role='user', content=[TextContent(text=prompt)]))
|
||||
|
||||
logger.debug(prompt)
|
||||
response = self.llm.completion(
|
||||
messages=self.llm.format_messages_for_llm(messages),
|
||||
messages=[message.model_dump() for message in messages],
|
||||
temperature=0.0,
|
||||
stop=[')```', ')\n```'],
|
||||
)
|
||||
return self.response_parser.parse(response)
|
||||
@@ -12,7 +12,7 @@ from browsergym.core.action.base import AbstractActionSet
|
||||
from browsergym.core.action.highlevel import HighLevelActionSet
|
||||
from browsergym.core.action.python import PythonActionSet
|
||||
|
||||
from openhands.agenthub.browsing_agent.utils import (
|
||||
from agenthub.browsing_agent.utils import (
|
||||
ParseError,
|
||||
parse_html_tags_raise,
|
||||
)
|
||||
@@ -57,7 +57,7 @@ class Flags:
|
||||
|
||||
@classmethod
|
||||
def from_dict(self, flags_dict):
|
||||
"""Helper for JSON serializable requirement."""
|
||||
"""Helper for JSON serializble requirement."""
|
||||
if isinstance(flags_dict, Flags):
|
||||
return flags_dict
|
||||
|
||||
@@ -354,7 +354,7 @@ and executed by a program, make sure to follow the formatting instructions.
|
||||
self._prompt += '\n'.join(
|
||||
[
|
||||
f"""\
|
||||
- [{msg['role']}], {msg['message']}"""
|
||||
- [{msg['role']}] {msg['message']}"""
|
||||
for msg in chat_messages
|
||||
]
|
||||
)
|
||||
88
agenthub/browsing_agent/response_parser.py
Normal file
88
agenthub/browsing_agent/response_parser.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import ast
|
||||
|
||||
from openhands.controller.action_parser import ActionParser, ResponseParser
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
BrowseInteractiveAction,
|
||||
)
|
||||
|
||||
|
||||
class BrowsingResponseParser(ResponseParser):
|
||||
def __init__(self):
|
||||
# Need to pay attention to the item order in self.action_parsers
|
||||
super().__init__()
|
||||
self.action_parsers = [BrowsingActionParserMessage()]
|
||||
self.default_parser = BrowsingActionParserBrowseInteractive()
|
||||
|
||||
def parse(self, response: str) -> Action:
|
||||
action_str = self.parse_response(response)
|
||||
return self.parse_action(action_str)
|
||||
|
||||
def parse_response(self, response) -> str:
|
||||
action_str = response['choices'][0]['message']['content']
|
||||
if action_str is None:
|
||||
return ''
|
||||
action_str = action_str.strip()
|
||||
if not action_str.endswith('```'):
|
||||
action_str = action_str + ')```'
|
||||
logger.info(action_str)
|
||||
return action_str
|
||||
|
||||
def parse_action(self, action_str: str) -> Action:
|
||||
for action_parser in self.action_parsers:
|
||||
if action_parser.check_condition(action_str):
|
||||
return action_parser.parse(action_str)
|
||||
return self.default_parser.parse(action_str)
|
||||
|
||||
|
||||
class BrowsingActionParserMessage(ActionParser):
|
||||
"""Parser action:
|
||||
- BrowseInteractiveAction(browser_actions) - unexpected response format, message back to user
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
pass
|
||||
|
||||
def check_condition(self, action_str: str) -> bool:
|
||||
return '```' not in action_str
|
||||
|
||||
def parse(self, action_str: str) -> Action:
|
||||
msg = f'send_msg_to_user("""{action_str}""")'
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions=msg,
|
||||
thought=action_str,
|
||||
browsergym_send_msg_to_user=action_str,
|
||||
)
|
||||
|
||||
|
||||
class BrowsingActionParserBrowseInteractive(ActionParser):
|
||||
"""Parser action:
|
||||
- BrowseInteractiveAction(browser_actions) - handle send message to user function call in BrowserGym
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
pass
|
||||
|
||||
def check_condition(self, action_str: str) -> bool:
|
||||
return True
|
||||
|
||||
def parse(self, action_str: str) -> Action:
|
||||
thought = action_str.split('```')[0].strip()
|
||||
action_str = action_str.split('```')[1].strip()
|
||||
msg_content = ''
|
||||
for sub_action in action_str.split('\n'):
|
||||
if 'send_msg_to_user(' in sub_action:
|
||||
tree = ast.parse(sub_action)
|
||||
args = tree.body[0].value.args # type: ignore
|
||||
msg_content = args[0].value
|
||||
|
||||
return BrowseInteractiveAction(
|
||||
browser_actions=action_str,
|
||||
thought=thought,
|
||||
browsergym_send_msg_to_user=msg_content,
|
||||
)
|
||||
@@ -10,3 +10,20 @@ The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
|
||||
|
||||

|
||||
|
||||
## Plugin System
|
||||
|
||||
To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenHands's plugin system:
|
||||
- [Jupyter plugin](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/runtime/plugins/jupyter): for IPython execution via bash command
|
||||
- [SWE-agent tool plugin](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/All-Hands-AI/OpenHands/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
|
||||
|
||||
*Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
|
||||
|
||||
## Work-in-progress & Next step
|
||||
|
||||
[] Support web-browsing
|
||||
[] Complete the workflow for CodeAct agent to submit Github PRs
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.codeact_agent.codeact_agent import CodeActAgent
|
||||
from agenthub.codeact_agent.codeact_agent import CodeActAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('CodeActAgent', CodeActAgent)
|
||||
@@ -1,17 +1,11 @@
|
||||
import re
|
||||
|
||||
from openhands.controller.action_parser import (
|
||||
ActionParser,
|
||||
ResponseParser,
|
||||
)
|
||||
from openhands.core.exceptions import LLMMalformedActionError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.controller.action_parser import ActionParser, ResponseParser
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
@@ -20,7 +14,6 @@ from openhands.events.action import (
|
||||
class CodeActResponseParser(ResponseParser):
|
||||
"""Parser action:
|
||||
- CmdRunAction(command) - bash command to run
|
||||
- FileEditAction(path, content) - edit a file
|
||||
- IPythonRunCellAction(code) - IPython code to run
|
||||
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
@@ -32,7 +25,6 @@ class CodeActResponseParser(ResponseParser):
|
||||
super().__init__()
|
||||
self.action_parsers = [
|
||||
CodeActActionParserFinish(),
|
||||
CodeActActionParserFileEdit(),
|
||||
CodeActActionParserCmdRun(),
|
||||
CodeActActionParserIPythonRunCell(),
|
||||
CodeActActionParserAgentDelegate(),
|
||||
@@ -48,19 +40,8 @@ class CodeActResponseParser(ResponseParser):
|
||||
if action is None:
|
||||
return ''
|
||||
for lang in ['bash', 'ipython', 'browse']:
|
||||
# special handling for DeepSeek: it has stop-word bug and returns </execute_ipython instead of </execute_ipython>
|
||||
if f'</execute_{lang}' in action and f'</execute_{lang}>' not in action:
|
||||
action = action.replace(f'</execute_{lang}', f'</execute_{lang}>')
|
||||
|
||||
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
|
||||
action += f'</execute_{lang}>'
|
||||
|
||||
# special handling for DeepSeek: it has stop-word bug and returns </execute_ipython instead of </execute_ipython>
|
||||
if '</file_edit' in action and '</file_edit>' not in action:
|
||||
action = action.replace('</file_edit', '</file_edit>')
|
||||
|
||||
if '<file_edit' in action and '</file_edit>' not in action:
|
||||
action += '</file_edit>'
|
||||
return action
|
||||
|
||||
def parse_action(self, action_str: str) -> Action:
|
||||
@@ -69,23 +50,6 @@ class CodeActResponseParser(ResponseParser):
|
||||
return action_parser.parse(action_str)
|
||||
return self.default_parser.parse(action_str)
|
||||
|
||||
def action_to_str(self, action: Action) -> str:
|
||||
if isinstance(action, CmdRunAction):
|
||||
return (
|
||||
f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
|
||||
)
|
||||
elif isinstance(action, IPythonRunCellAction):
|
||||
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
|
||||
elif isinstance(action, AgentDelegateAction):
|
||||
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
|
||||
elif isinstance(action, FileEditAction):
|
||||
return f'{action.thought}\n<file_edit path={action.path}>\n{action.content}\n</file_edit>'
|
||||
elif isinstance(action, MessageAction):
|
||||
return action.content
|
||||
elif isinstance(action, AgentFinishAction) and action.source == 'agent':
|
||||
return action.thought
|
||||
return ''
|
||||
|
||||
|
||||
class CodeActActionParserFinish(ActionParser):
|
||||
"""Parser action:
|
||||
@@ -190,15 +154,8 @@ class CodeActActionParserAgentDelegate(ActionParser):
|
||||
), 'self.agent_delegate should not be None when parse is called'
|
||||
thought = action_str.replace(self.agent_delegate.group(0), '').strip()
|
||||
browse_actions = self.agent_delegate.group(1).strip()
|
||||
thought = (
|
||||
f'{thought}\nI should start with: {browse_actions}'
|
||||
if thought
|
||||
else f'I should start with: {browse_actions}'
|
||||
)
|
||||
|
||||
return AgentDelegateAction(
|
||||
agent='BrowsingAgent', thought=thought, inputs={'task': browse_actions}
|
||||
)
|
||||
task = f'{thought}. I should start with: {browse_actions}'
|
||||
return AgentDelegateAction(agent='BrowsingAgent', inputs={'task': task})
|
||||
|
||||
|
||||
class CodeActActionParserMessage(ActionParser):
|
||||
@@ -218,87 +175,3 @@ class CodeActActionParserMessage(ActionParser):
|
||||
|
||||
def parse(self, action_str: str) -> Action:
|
||||
return MessageAction(content=action_str, wait_for_response=True)
|
||||
|
||||
|
||||
class CodeActActionParserFileEdit(ActionParser):
|
||||
"""Parser action:
|
||||
- FileEditAction(path, content) - edit a file
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.file_edit_match: re.Match | None = None
|
||||
|
||||
def check_condition(self, action_str: str) -> bool:
|
||||
if '<file_edit' not in action_str:
|
||||
return False
|
||||
|
||||
# Updated regex to make start and end optional
|
||||
self.file_edit_match = re.search(
|
||||
r'<file_edit\s+path=(["\']?)(.*?)\1(?:\s+start=(["\']?)(.*?)\3)?(?:\s+end=(["\']?)(.*?)\5)?\s*>(.*?)</file_edit>',
|
||||
action_str,
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
if self.file_edit_match is None:
|
||||
logger.error(
|
||||
f'FileEditAction detected but the format is incorrect. Unable to match for <file_edit> in:\n{"-" * 80}\n{action_str}\n{"-" * 80}'
|
||||
)
|
||||
raise LLMMalformedActionError(
|
||||
'FileEditAction detected but the format is incorrect. Usage:\n'
|
||||
'<file_edit path="[path]" start=[start_line] end=[end_line]>\n'
|
||||
'[content_to_edit]\n'
|
||||
'</file_edit>\n'
|
||||
)
|
||||
|
||||
path = self.file_edit_match.group(2)
|
||||
start = self.file_edit_match.group(4)
|
||||
end = self.file_edit_match.group(6)
|
||||
|
||||
if not path:
|
||||
raise LLMMalformedActionError(
|
||||
'FileEditAction detected but no `path` specified. You should specify the path of the file to edit.'
|
||||
)
|
||||
|
||||
if start:
|
||||
try:
|
||||
int(start)
|
||||
except ValueError:
|
||||
raise LLMMalformedActionError(
|
||||
f'FileEditAction detected but `start` is not a valid integer: {start}'
|
||||
)
|
||||
|
||||
if end:
|
||||
try:
|
||||
int(end)
|
||||
except ValueError:
|
||||
raise LLMMalformedActionError(
|
||||
f'FileEditAction detected but `end` is not a valid integer: {end}'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def parse(self, action_str: str) -> Action:
|
||||
assert (
|
||||
self.file_edit_match is not None
|
||||
), 'self.file_edit_match should not be None when parse is called'
|
||||
|
||||
file_path = self.file_edit_match.group(2).strip()
|
||||
start_line = (
|
||||
int(self.file_edit_match.group(4))
|
||||
if self.file_edit_match.group(4)
|
||||
else None
|
||||
)
|
||||
end_line = (
|
||||
int(self.file_edit_match.group(6))
|
||||
if self.file_edit_match.group(6)
|
||||
else None
|
||||
)
|
||||
content = self.file_edit_match.group(7)
|
||||
thought = action_str.replace(self.file_edit_match.group(0), '').strip()
|
||||
|
||||
action = FileEditAction(path=file_path, content=content, thought=thought)
|
||||
if start_line is not None:
|
||||
action.start = start_line
|
||||
if end_line is not None:
|
||||
action.end = end_line
|
||||
return action
|
||||
249
agenthub/codeact_agent/codeact_agent.py
Normal file
249
agenthub/codeact_agent/codeact_agent.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import os
|
||||
|
||||
from agenthub.codeact_agent.action_parser import CodeActResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.observation import (
|
||||
AgentDelegateObservation,
|
||||
CmdOutputObservation,
|
||||
IPythonRunCellObservation,
|
||||
)
|
||||
from openhands.events.observation.error import ErrorObservation
|
||||
from openhands.events.observation.observation import Observation
|
||||
from openhands.events.serialization.event import truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.plugins import (
|
||||
AgentSkillsRequirement,
|
||||
JupyterRequirement,
|
||||
PluginRequirement,
|
||||
)
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
|
||||
class CodeActAgent(Agent):
|
||||
VERSION = '1.9'
|
||||
"""
|
||||
The Code Act Agent is a minimalist agent.
|
||||
The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
|
||||
|
||||
### Overview
|
||||
|
||||
This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.01030), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
|
||||
|
||||
The conceptual idea is illustrated below. At each turn, the agent can:
|
||||
|
||||
1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
|
||||
2. **CodeAct**: Choose to perform the task by executing code
|
||||
- Execute any valid Linux `bash` command
|
||||
- Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
|
||||
|
||||

|
||||
|
||||
"""
|
||||
|
||||
sandbox_plugins: list[PluginRequirement] = [
|
||||
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
|
||||
# AgentSkillsRequirement provides a lot of Python functions,
|
||||
# and it needs to be initialized before Jupyter for Jupyter to use those functions.
|
||||
AgentSkillsRequirement(),
|
||||
JupyterRequirement(),
|
||||
]
|
||||
|
||||
action_parser = CodeActResponseParser()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: LLM,
|
||||
config: AgentConfig,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the CodeActAgent class.
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
self.reset()
|
||||
self.prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__)),
|
||||
agent_skills_docs=AgentSkillsRequirement.documentation,
|
||||
micro_agent_name=None, # TODO: implement micro-agent
|
||||
)
|
||||
|
||||
def action_to_str(self, action: Action) -> str:
|
||||
if isinstance(action, CmdRunAction):
|
||||
return (
|
||||
f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
|
||||
)
|
||||
elif isinstance(action, IPythonRunCellAction):
|
||||
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
|
||||
elif isinstance(action, AgentDelegateAction):
|
||||
return f'{action.thought}\n<execute_browse>\n{action.inputs["task"]}\n</execute_browse>'
|
||||
elif isinstance(action, MessageAction):
|
||||
return action.content
|
||||
elif isinstance(action, AgentFinishAction) and action.source == 'agent':
|
||||
return action.thought
|
||||
return ''
|
||||
|
||||
def get_action_message(self, action: Action) -> Message | None:
|
||||
if (
|
||||
isinstance(action, AgentDelegateAction)
|
||||
or isinstance(action, CmdRunAction)
|
||||
or isinstance(action, IPythonRunCellAction)
|
||||
or isinstance(action, MessageAction)
|
||||
or (isinstance(action, AgentFinishAction) and action.source == 'agent')
|
||||
):
|
||||
content = [TextContent(text=self.action_to_str(action))]
|
||||
|
||||
if isinstance(action, MessageAction) and action.images_urls:
|
||||
content.append(ImageContent(image_urls=action.images_urls))
|
||||
|
||||
return Message(
|
||||
role='user' if action.source == 'user' else 'assistant', content=content
|
||||
)
|
||||
return None
|
||||
|
||||
def get_observation_message(self, obs: Observation) -> Message | None:
|
||||
max_message_chars = self.llm.config.max_message_chars
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
|
||||
text += (
|
||||
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
|
||||
)
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, IPythonRunCellObservation):
|
||||
text = 'OBSERVATION:\n' + obs.content
|
||||
# replace base64 images with a placeholder
|
||||
splitted = text.split('\n')
|
||||
for i, line in enumerate(splitted):
|
||||
if ' already displayed to user'
|
||||
)
|
||||
text = '\n'.join(splitted)
|
||||
text = truncate_content(text, max_message_chars)
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, AgentDelegateObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(
|
||||
str(obs.outputs), max_message_chars
|
||||
)
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
elif isinstance(obs, ErrorObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
|
||||
text += '\n[Error occurred in processing last action]'
|
||||
return Message(role='user', content=[TextContent(text=text)])
|
||||
else:
|
||||
# If an observation message is not returned, it will cause an error
|
||||
# when the LLM tries to return the next message
|
||||
raise ValueError(f'Unknown observation type: {type(obs)}')
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Resets the CodeAct Agent."""
|
||||
super().reset()
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""Performs one step using the CodeAct Agent.
|
||||
This includes gathering info on previous steps and prompting the model to make a command to execute.
|
||||
|
||||
Parameters:
|
||||
- state (State): used to get updated info
|
||||
|
||||
Returns:
|
||||
- CmdRunAction(command) - bash command to run
|
||||
- IPythonRunCellAction(code) - IPython code to run
|
||||
- AgentDelegateAction(agent, inputs) - delegate action for (sub)task
|
||||
- MessageAction(content) - Message action to run (e.g. ask for clarification)
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
# if we're done, go back
|
||||
latest_user_message = state.history.get_last_user_message()
|
||||
if latest_user_message and latest_user_message.strip() == '/exit':
|
||||
return AgentFinishAction()
|
||||
|
||||
# prepare what we want to send to the LLM
|
||||
messages = self._get_messages(state)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=[message.model_dump() for message in messages],
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
'</execute_browse>',
|
||||
],
|
||||
temperature=0.0,
|
||||
)
|
||||
return self.action_parser.parse(response)
|
||||
|
||||
def _get_messages(self, state: State) -> list[Message]:
|
||||
messages: list[Message] = [
|
||||
Message(
|
||||
role='system',
|
||||
content=[TextContent(text=self.prompt_manager.system_message)],
|
||||
),
|
||||
Message(
|
||||
role='user',
|
||||
content=[TextContent(text=self.prompt_manager.initial_user_message)],
|
||||
),
|
||||
]
|
||||
|
||||
for event in state.history.get_events():
|
||||
# create a regular message from an event
|
||||
if isinstance(event, Action):
|
||||
message = self.get_action_message(event)
|
||||
elif isinstance(event, Observation):
|
||||
message = self.get_observation_message(event)
|
||||
else:
|
||||
raise ValueError(f'Unknown event type: {type(event)}')
|
||||
|
||||
# add regular message
|
||||
if message:
|
||||
# handle error if the message is the SAME role as the previous message
|
||||
# litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
|
||||
# there should not have two consecutive messages from the same role
|
||||
if messages and messages[-1].role == message.role:
|
||||
messages[-1].content.extend(message.content)
|
||||
else:
|
||||
messages.append(message)
|
||||
|
||||
# the latest user message is important:
|
||||
# we want to remind the agent of the environment constraints
|
||||
latest_user_message = next(
|
||||
(
|
||||
m
|
||||
for m in reversed(messages)
|
||||
if m.role == 'user'
|
||||
and any(isinstance(c, TextContent) for c in m.content)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
# Get the last user text inside content
|
||||
if latest_user_message:
|
||||
latest_user_message_text = next(
|
||||
(
|
||||
t
|
||||
for t in reversed(latest_user_message.content)
|
||||
if isinstance(t, TextContent)
|
||||
)
|
||||
)
|
||||
# add a reminder to the prompt
|
||||
reminder_text = f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with <finish></finish>.'
|
||||
|
||||
if latest_user_message_text:
|
||||
latest_user_message_text.text = (
|
||||
latest_user_message_text.text + reminder_text
|
||||
)
|
||||
else:
|
||||
latest_user_message_text = TextContent(text=reminder_text)
|
||||
latest_user_message.content.append(latest_user_message_text)
|
||||
|
||||
return messages
|
||||
41
agenthub/codeact_agent/system_prompt.j2
Normal file
41
agenthub/codeact_agent/system_prompt.j2
Normal file
@@ -0,0 +1,41 @@
|
||||
{% set MINIMAL_SYSTEM_PREFIX %}
|
||||
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||
The assistant can use an interactive Python (Jupyter Notebook) environment, executing code with <execute_ipython>.
|
||||
<execute_ipython>
|
||||
print("Hello World!")
|
||||
</execute_ipython>
|
||||
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
|
||||
|
||||
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
|
||||
Important, however: do not run interactive commands. You do not have access to stdin.
|
||||
Also, you need to handle commands that may run indefinitely and not return a result. For such cases, you should redirect the output to a file and run the command in the background to avoid blocking the execution.
|
||||
For example, to run a Python script that might run indefinitely without returning immediately, you can use the following format: <execute_bash> python3 app.py > server.log 2>&1 & </execute_bash>
|
||||
Also, if a command execution result saying like: Command: "npm start" timed out. Sending SIGINT to the process, you should also retry with running the command in the background.
|
||||
{% endset %}
|
||||
{% set BROWSING_PREFIX %}
|
||||
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
|
||||
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
|
||||
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
|
||||
{% endset %}
|
||||
{% set PIP_INSTALL_PREFIX %}
|
||||
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
|
||||
{% endset %}
|
||||
{% set SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX %}
|
||||
{% set COMMAND_DOCS %}
|
||||
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
|
||||
{{ agent_skills_docs }}
|
||||
Please note that THE `edit_file_by_replace`, `append_file` and `insert_content_at_line` FUNCTIONS REQUIRE PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
|
||||
{% endset %}
|
||||
{% set SYSTEM_SUFFIX %}
|
||||
Responses should be concise.
|
||||
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
|
||||
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
|
||||
If the assistant is finished with the task you MUST include <finish></finish> in your response.
|
||||
IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_browse> whenever possible.
|
||||
The assistant should utilize full file paths and the 'pwd' command to prevent path-related errors. The assistant should refrain from excessive apologies in its responses.
|
||||
|
||||
{% endset %}
|
||||
{# Combine all parts without newlines between them #}
|
||||
{{ SYSTEM_PREFIX -}}
|
||||
{{- COMMAND_DOCS -}}
|
||||
{{- SYSTEM_SUFFIX }}
|
||||
@@ -4,19 +4,23 @@
|
||||
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
|
||||
|
||||
ASSISTANT:
|
||||
Sure! Let me first check the current directory:
|
||||
<execute_bash>
|
||||
ls
|
||||
</execute_bash>
|
||||
Sure! Let me create a Python file `app.py`:
|
||||
<execute_ipython>
|
||||
create_file('app.py')
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
openhands@runtime:~/workspace$
|
||||
[File: /workspace/app.py (1 lines total)]
|
||||
(this is the beginning of the file)
|
||||
1|
|
||||
(this is the end of the file)
|
||||
[File app.py created.]
|
||||
|
||||
ASSISTANT:
|
||||
There is no `app.py` file in the current directory. Let me create a Python file `app.py`:
|
||||
<file_edit path="/workspace/app.py" start=-1 end=-1>
|
||||
from flask import Flask
|
||||
Now I will write the Python code for starting a web server and save it to the file `app.py`:
|
||||
<execute_ipython>
|
||||
EDITED_CODE="""from flask import Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
@@ -25,27 +29,30 @@ def index():
|
||||
return str(numbers)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=5000)
|
||||
</file_edit>
|
||||
app.run(port=5000)"""
|
||||
|
||||
insert_content_at_line(
|
||||
'app.py',
|
||||
1,
|
||||
EDITED_CODE,
|
||||
)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
OBSERVATION:
|
||||
[New file /workspace/app.py is created.]
|
||||
(begin of changes)
|
||||
--- /workspace/app.py
|
||||
+++ /workspace/app.py
|
||||
@@ -1 +1,10 @@
|
||||
+from flask import Flask
|
||||
+app = Flask(__name__)
|
||||
|
||||
+@app.route('/')
|
||||
+def index():
|
||||
+ numbers = list(range(1, 11))
|
||||
+ return str(numbers)
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ app.run(port=5000)
|
||||
(end of changes)
|
||||
(this is the beginning of the file)
|
||||
1|from flask import Flask
|
||||
2|app = Flask(__name__)
|
||||
3|
|
||||
4|@app.route('/')
|
||||
5|def index():
|
||||
6| numbers = list(range(1, 11))
|
||||
7| return str(numbers)
|
||||
8|
|
||||
9|if __name__ == '__main__':
|
||||
10| app.run(port=5000)
|
||||
(this is the end of the file)
|
||||
[File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
|
||||
|
||||
ASSISTANT:
|
||||
I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
|
||||
@@ -170,17 +177,16 @@ USER:
|
||||
|
||||
ASSISTANT:
|
||||
I should edit the file to display the numbers in a table format. I should include correct indentation. Let me update the file:
|
||||
<file_edit path="/workspace/app.py" start=4 end=7>
|
||||
@app.route('/')
|
||||
def index():
|
||||
numbers = list(range(1, 11))
|
||||
ret = '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
return ret
|
||||
</file_edit>
|
||||
<execute_ipython>
|
||||
edit_file_by_replace(
|
||||
'app.py',
|
||||
to_replace=" return str(numbers)",
|
||||
new_content=" return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'",
|
||||
)
|
||||
</execute_ipython>
|
||||
|
||||
USER:
|
||||
Observation:
|
||||
[Edited existing file /workspace/app.py]
|
||||
[File: /workspace/app.py (10 lines total after edit)]
|
||||
(this is the beginning of the file)
|
||||
1|from flask import Flask
|
||||
@@ -189,11 +195,10 @@ Observation:
|
||||
4|@app.route('/')
|
||||
5|def index():
|
||||
6| numbers = list(range(1, 11))
|
||||
7| ret = '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
8| return ret
|
||||
9|
|
||||
10|if __name__ == '__main__':
|
||||
11| app.run(port=5000)
|
||||
7| return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
8|
|
||||
9|if __name__ == '__main__':
|
||||
10| app.run(port=5000)
|
||||
(this is the end of the file)
|
||||
[File updated (edited at line 7). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
|
||||
|
||||
@@ -215,5 +220,12 @@ The server is running on port 5000 with PID 126. You can access the list of numb
|
||||
{% endset %}
|
||||
Here is an example of how you can interact with the environment for task solving:
|
||||
{{ DEFAULT_EXAMPLE }}
|
||||
{% if micro_agent %}
|
||||
--- BEGIN OF GUIDELINE ---
|
||||
The following information may assist you in completing your task:
|
||||
|
||||
{{ micro_agent }}
|
||||
--- END OF GUIDELINE ---
|
||||
{% endif %}
|
||||
|
||||
NOW, LET'S START!
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.codeact_swe_agent.codeact_swe_agent import CodeActSWEAgent
|
||||
from agenthub.codeact_swe_agent.codeact_swe_agent import CodeActSWEAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('CodeActSWEAgent', CodeActSWEAgent)
|
||||
@@ -1,12 +1,10 @@
|
||||
from openhands.agenthub.codeact_swe_agent.prompt import (
|
||||
from agenthub.codeact_swe_agent.prompt import (
|
||||
COMMAND_DOCS,
|
||||
SWE_EXAMPLE,
|
||||
SYSTEM_PREFIX,
|
||||
SYSTEM_SUFFIX,
|
||||
)
|
||||
from openhands.agenthub.codeact_swe_agent.response_parser import (
|
||||
CodeActSWEResponseParser,
|
||||
)
|
||||
from agenthub.codeact_swe_agent.response_parser import CodeActSWEResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
@@ -89,14 +87,14 @@ class CodeActSWEAgent(Agent):
|
||||
return ''
|
||||
|
||||
def get_action_message(self, action: Action) -> Message | None:
|
||||
if isinstance(action, (CmdRunAction, IPythonRunCellAction, MessageAction)):
|
||||
if (
|
||||
isinstance(action, CmdRunAction)
|
||||
or isinstance(action, IPythonRunCellAction)
|
||||
or isinstance(action, MessageAction)
|
||||
):
|
||||
content = [TextContent(text=self.action_to_str(action))]
|
||||
|
||||
if (
|
||||
self.llm.vision_is_active()
|
||||
and isinstance(action, MessageAction)
|
||||
and action.images_urls
|
||||
):
|
||||
if isinstance(action, MessageAction) and action.images_urls:
|
||||
content.append(ImageContent(image_urls=action.images_urls))
|
||||
|
||||
return Message(
|
||||
@@ -108,9 +106,7 @@ class CodeActSWEAgent(Agent):
|
||||
def get_observation_message(self, obs: Observation) -> Message | None:
|
||||
max_message_chars = self.llm.config.max_message_chars
|
||||
if isinstance(obs, CmdOutputObservation):
|
||||
text = 'OBSERVATION:\n' + truncate_content(
|
||||
obs.content + obs.interpreter_details, max_message_chars
|
||||
)
|
||||
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
|
||||
text += (
|
||||
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
|
||||
)
|
||||
@@ -154,18 +150,20 @@ class CodeActSWEAgent(Agent):
|
||||
- AgentFinishAction() - end the interaction
|
||||
"""
|
||||
# if we're done, go back
|
||||
last_user_message = state.get_last_user_message()
|
||||
if last_user_message and last_user_message.content.strip() == '/exit':
|
||||
latest_user_message = state.history.get_last_user_message()
|
||||
if latest_user_message and latest_user_message.strip() == '/exit':
|
||||
return AgentFinishAction()
|
||||
|
||||
# prepare what we want to send to the LLM
|
||||
messages: list[Message] = self._get_messages(state)
|
||||
|
||||
response = self.llm.completion(
|
||||
messages=self.llm.format_messages_for_llm(messages),
|
||||
messages=[message.model_dump() for message in messages],
|
||||
stop=[
|
||||
'</execute_ipython>',
|
||||
'</execute_bash>',
|
||||
],
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
return self.response_parser.parse(response)
|
||||
@@ -176,7 +174,7 @@ class CodeActSWEAgent(Agent):
|
||||
Message(role='user', content=[TextContent(text=self.in_context_example)]),
|
||||
]
|
||||
|
||||
for event in state.history:
|
||||
for event in state.history.get_events():
|
||||
# create a regular message from an event
|
||||
if isinstance(event, Action):
|
||||
message = self.get_action_message(event)
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.codeact_swe_agent.action_parser import (
|
||||
from agenthub.codeact_swe_agent.action_parser import (
|
||||
CodeActSWEActionParserCmdRun,
|
||||
CodeActSWEActionParserFinish,
|
||||
CodeActSWEActionParserIPythonRunCell,
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.delegator_agent.agent import DelegatorAgent
|
||||
from agenthub.delegator_agent.agent import DelegatorAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('DelegatorAgent', DelegatorAgent)
|
||||
@@ -2,7 +2,7 @@ from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.events.action import Action, AgentDelegateAction, AgentFinishAction
|
||||
from openhands.events.observation import AgentDelegateObservation, Observation
|
||||
from openhands.events.observation import AgentDelegateObservation
|
||||
from openhands.llm.llm import LLM
|
||||
|
||||
|
||||
@@ -41,11 +41,7 @@ class DelegatorAgent(Agent):
|
||||
)
|
||||
|
||||
# last observation in history should be from the delegate
|
||||
last_observation = None
|
||||
for event in reversed(state.history):
|
||||
if isinstance(event, Observation):
|
||||
last_observation = event
|
||||
break
|
||||
last_observation = state.history.get_last_observation()
|
||||
|
||||
if not isinstance(last_observation, AgentDelegateObservation):
|
||||
raise Exception('Last observation is not an AgentDelegateObservation')
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.dummy_agent.agent import DummyAgent
|
||||
from agenthub.dummy_agent.agent import DummyAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('DummyAgent', DummyAgent)
|
||||
@@ -164,7 +164,7 @@ class DummyAgent(Agent):
|
||||
|
||||
if 'observations' in prev_step and prev_step['observations']:
|
||||
expected_observations = prev_step['observations']
|
||||
hist_events = state.history[-len(expected_observations) :]
|
||||
hist_events = state.history.get_last_events(len(expected_observations))
|
||||
|
||||
if len(hist_events) < len(expected_observations):
|
||||
print(
|
||||
@@ -1,17 +1,17 @@
|
||||
from jinja2 import BaseLoader, Environment
|
||||
|
||||
from openhands.agenthub.micro.instructions import instructions
|
||||
from openhands.agenthub.micro.registry import all_microagents
|
||||
from agenthub.micro.instructions import instructions
|
||||
from agenthub.micro.registry import all_microagents
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import Action
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.serialization.event import event_to_memory
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.memory.history import ShortTermHistory
|
||||
|
||||
|
||||
def parse_response(orig_response: str) -> Action:
|
||||
@@ -32,14 +32,16 @@ class MicroAgent(Agent):
|
||||
prompt = ''
|
||||
agent_definition: dict = {}
|
||||
|
||||
def history_to_json(self, history: list[Event], max_events: int = 20, **kwargs):
|
||||
def history_to_json(
|
||||
self, history: ShortTermHistory, max_events: int = 20, **kwargs
|
||||
):
|
||||
"""
|
||||
Serialize and simplify history to str format
|
||||
"""
|
||||
processed_history = []
|
||||
event_count = 0
|
||||
|
||||
for event in reversed(history):
|
||||
for event in history.get_events(reverse=True):
|
||||
if event_count >= max_events:
|
||||
break
|
||||
processed_history.append(
|
||||
@@ -71,12 +73,10 @@ class MicroAgent(Agent):
|
||||
latest_user_message=last_user_message,
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if self.llm.vision_is_active() and last_image_urls:
|
||||
if last_image_urls:
|
||||
content.append(ImageContent(image_urls=last_image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(
|
||||
messages=self.llm.format_messages_for_llm(message),
|
||||
)
|
||||
resp = self.llm.completion(messages=[message.model_dump()])
|
||||
action_resp = resp['choices'][0]['message']['content']
|
||||
action = parse_response(action_resp)
|
||||
return action
|
||||
@@ -1,7 +1,6 @@
|
||||
# Task
|
||||
You are in charge of accomplishing the following task:
|
||||
{% set goal = latest_user_message if latest_user_message is not none else state.inputs.task %}
|
||||
{{ goal }}
|
||||
{{ latest_user_message }}
|
||||
|
||||
In order to accomplish this goal, you must delegate tasks to one or more agents, who
|
||||
can do the actual work. A description of each agent is provided below. You MUST
|
||||
@@ -1,4 +1,4 @@
|
||||
from openhands.agenthub.planner_agent.agent import PlannerAgent
|
||||
from agenthub.planner_agent.agent import PlannerAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('PlannerAgent', PlannerAgent)
|
||||
@@ -1,5 +1,5 @@
|
||||
from openhands.agenthub.planner_agent.prompt import get_prompt_and_images
|
||||
from openhands.agenthub.planner_agent.response_parser import PlannerResponseParser
|
||||
from agenthub.planner_agent.prompt import get_prompt_and_images
|
||||
from agenthub.planner_agent.response_parser import PlannerResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
@@ -46,8 +46,8 @@ class PlannerAgent(Agent):
|
||||
state, self.llm.config.max_message_chars
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if self.llm.vision_is_active() and image_urls:
|
||||
if image_urls:
|
||||
content.append(ImageContent(image_urls=image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(messages=self.llm.format_messages_for_llm(message))
|
||||
resp = self.llm.completion(messages=[message.model_dump()])
|
||||
return self.response_parser.parse(resp)
|
||||
@@ -117,7 +117,7 @@ def get_hint(latest_action_id: str) -> str:
|
||||
|
||||
def get_prompt_and_images(
|
||||
state: State, max_message_chars: int
|
||||
) -> tuple[str, list[str] | None]:
|
||||
) -> tuple[str, list[str]]:
|
||||
"""Gets the prompt for the planner agent.
|
||||
|
||||
Formatted with the most recent action-observation pairs, current task, and hint based on last action
|
||||
@@ -136,7 +136,7 @@ def get_prompt_and_images(
|
||||
latest_action: Action = NullAction()
|
||||
|
||||
# retrieve the latest HISTORY_SIZE events
|
||||
for event_count, event in enumerate(reversed(state.history)):
|
||||
for event_count, event in enumerate(state.history.get_events(reverse=True)):
|
||||
if event_count >= HISTORY_SIZE:
|
||||
break
|
||||
if latest_action == NullAction() and isinstance(event, Action):
|
||||
@@ -160,7 +160,7 @@ def get_prompt_and_images(
|
||||
|
||||
# the hint, based on the last action
|
||||
hint = get_hint(event_to_memory(latest_action, max_message_chars).get('action', ''))
|
||||
logger.debug('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
|
||||
logger.info('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
|
||||
|
||||
# the last relevant user message (the task)
|
||||
message, image_urls = state.get_current_user_intent()
|
||||
5
build.sh
5
build.sh
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cp pyproject.toml poetry.lock openhands
|
||||
poetry build -v
|
||||
22
compose.yml
22
compose.yml
@@ -1,22 +0,0 @@
|
||||
#
|
||||
services:
|
||||
openhands:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./containers/app/Dockerfile
|
||||
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_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${WORKSPACE_BASE:-$PWD/workspace}:/opt/workspace_base
|
||||
pull_policy: build
|
||||
stdin_open: true
|
||||
tty: true
|
||||
@@ -13,10 +13,6 @@
|
||||
# API key for E2B
|
||||
#e2b_api_key = ""
|
||||
|
||||
# API key for Modal
|
||||
#modal_api_token_id = ""
|
||||
#modal_api_token_secret = ""
|
||||
|
||||
# Base path for the workspace
|
||||
workspace_base = "./workspace"
|
||||
|
||||
@@ -32,10 +28,6 @@ workspace_base = "./workspace"
|
||||
# Enable saving and restoring the session when run from CLI
|
||||
#enable_cli_session = false
|
||||
|
||||
# Path to store trajectories, can be a folder or a file
|
||||
# If it's a folder, the session id will be used as the file name
|
||||
#trajectories_path="./trajectories"
|
||||
|
||||
# File store path
|
||||
#file_store_path = "/tmp/file_store"
|
||||
|
||||
@@ -72,15 +64,6 @@ workspace_base = "./workspace"
|
||||
# Name of the default agent
|
||||
#default_agent = "CodeActAgent"
|
||||
|
||||
# JWT secret for authentication
|
||||
#jwt_secret = ""
|
||||
|
||||
# Restrict file types for file uploads
|
||||
#file_uploads_restrict_file_types = false
|
||||
|
||||
# List of allowed file extensions for uploads
|
||||
#file_uploads_allowed_extensions = [".*"]
|
||||
|
||||
#################################### LLM #####################################
|
||||
# Configuration for LLM models (group name starts with 'llm')
|
||||
# use 'llm' for the default LLM config
|
||||
@@ -120,7 +103,7 @@ api_key = "your-api-key"
|
||||
#embedding_deployment_name = ""
|
||||
|
||||
# Embedding model to use
|
||||
embedding_model = "local"
|
||||
embedding_model = ""
|
||||
|
||||
# Maximum number of characters in an observation's content
|
||||
#max_message_chars = 10000
|
||||
@@ -134,31 +117,14 @@ embedding_model = "local"
|
||||
# Model to use
|
||||
model = "gpt-4o"
|
||||
|
||||
# Number of retries to attempt when an operation fails with the LLM.
|
||||
# Increase this value to allow more attempts before giving up
|
||||
#num_retries = 8
|
||||
# Number of retries to attempt
|
||||
#num_retries = 5
|
||||
|
||||
# Maximum wait time (in seconds) between retry attempts
|
||||
# This caps the exponential backoff to prevent excessively long
|
||||
#retry_max_wait = 120
|
||||
# Retry maximum wait time
|
||||
#retry_max_wait = 60
|
||||
|
||||
# Minimum wait time (in seconds) between retry attempts
|
||||
# This sets the initial delay before the first retry
|
||||
#retry_min_wait = 15
|
||||
|
||||
# Multiplier for exponential backoff calculation
|
||||
# The wait time increases by this factor after each failed attempt
|
||||
# A value of 2.0 means each retry waits twice as long as the previous one
|
||||
#retry_multiplier = 2.0
|
||||
|
||||
# Drop any unmapped (unsupported) params without causing an exception
|
||||
#drop_params = false
|
||||
|
||||
# Using the prompt caching feature if provided by the LLM and supported
|
||||
#caching_prompt = true
|
||||
|
||||
# Base URL for the OLLAMA API
|
||||
#ollama_base_url = ""
|
||||
# Retry minimum wait time
|
||||
#retry_min_wait = 3
|
||||
|
||||
# Temperature for the API
|
||||
#temperature = 0.0
|
||||
@@ -167,15 +133,14 @@ model = "gpt-4o"
|
||||
#timeout = 0
|
||||
|
||||
# Top p for the API
|
||||
#top_p = 1.0
|
||||
#top_p = 0.5
|
||||
|
||||
# If model is vision capable, this option allows to disable image processing (useful for cost reduction).
|
||||
#disable_vision = true
|
||||
|
||||
[llm.gpt4o-mini]
|
||||
[llm.gpt3]
|
||||
# API key to use
|
||||
api_key = "your-api-key"
|
||||
model = "gpt-4o"
|
||||
|
||||
# Model to use
|
||||
model = "gpt-3.5"
|
||||
|
||||
#################################### Agent ###################################
|
||||
# Configuration for agents (group name starts with 'agent')
|
||||
@@ -184,17 +149,14 @@ model = "gpt-4o"
|
||||
# agent.CodeActAgent
|
||||
##############################################################################
|
||||
[agent]
|
||||
# Name of the micro agent to use for this agent
|
||||
#micro_agent_name = ""
|
||||
|
||||
# Memory enabled
|
||||
#memory_enabled = false
|
||||
|
||||
# Memory maximum threads
|
||||
#memory_max_threads = 3
|
||||
#memory_max_threads = 2
|
||||
|
||||
# LLM config group to use
|
||||
#llm_config = 'your-llm-config-group'
|
||||
#llm_config = 'llm'
|
||||
|
||||
[agent.RepoExplorerAgent]
|
||||
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
|
||||
@@ -212,7 +174,7 @@ llm_config = 'gpt3'
|
||||
#user_id = 1000
|
||||
|
||||
# Container image to use for the sandbox
|
||||
#base_container_image = "nikolaik/python-nodejs:python3.12-nodejs22"
|
||||
#base_container_image = "nikolaik/python-nodejs:python3.11-nodejs22"
|
||||
|
||||
# Use host network
|
||||
#use_host_network = false
|
||||
@@ -220,25 +182,13 @@ llm_config = 'gpt3'
|
||||
# Enable auto linting after editing
|
||||
#enable_auto_lint = false
|
||||
|
||||
# Whether to initialize plugins
|
||||
#initialize_plugins = true
|
||||
|
||||
# Extra dependencies to install in the runtime image
|
||||
#runtime_extra_deps = ""
|
||||
|
||||
# Environment variables to set at the launch of the runtime
|
||||
#runtime_startup_env_vars = {}
|
||||
|
||||
# BrowserGym environment to use for evaluation
|
||||
#browsergym_eval_env = ""
|
||||
|
||||
#################################### Security ###################################
|
||||
# Configuration for security features
|
||||
##############################################################################
|
||||
[security]
|
||||
|
||||
# Enable confirmation mode
|
||||
#confirmation_mode = false
|
||||
#confirmation_mode = true
|
||||
|
||||
# The security analyzer to use
|
||||
#security_analyzer = ""
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN npm install -g npm@10.5.1
|
||||
RUN npm ci
|
||||
|
||||
COPY ./frontend ./
|
||||
RUN npm run build
|
||||
RUN npm run make-i18n && npm run build
|
||||
|
||||
FROM python:3.12.3-slim AS backend-builder
|
||||
|
||||
@@ -28,7 +28,7 @@ COPY ./pyproject.toml ./poetry.lock ./
|
||||
RUN touch README.md
|
||||
RUN export POETRY_CACHE_DIR && poetry install --without evaluation,llama-index --no-root && rm -rf $POETRY_CACHE_DIR
|
||||
|
||||
FROM python:3.12.3-slim AS openhands-app
|
||||
FROM python:3.12.3-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -37,11 +37,10 @@ ARG OPENHANDS_BUILD_VERSION #re-declare for this section
|
||||
ENV RUN_AS_OPENHANDS=true
|
||||
# A random number--we need this to be different from the user's UID on the host machine
|
||||
ENV OPENHANDS_USER_ID=42420
|
||||
ENV SANDBOX_LOCAL_RUNTIME_URL=http://host.docker.internal
|
||||
ENV SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
ENV USE_HOST_NETWORK=false
|
||||
ENV WORKSPACE_BASE=/opt/workspace_base
|
||||
ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
|
||||
ENV SANDBOX_USER_ID=0
|
||||
RUN mkdir -p $WORKSPACE_BASE
|
||||
|
||||
RUN apt-get update -y \
|
||||
@@ -70,12 +69,11 @@ RUN playwright install --with-deps chromium
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
|
||||
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands/agenthub ./openhands/agenthub
|
||||
COPY --chown=openhands:app ./pyproject.toml ./pyproject.toml
|
||||
COPY --chown=openhands:app ./poetry.lock ./poetry.lock
|
||||
COPY --chown=openhands:app ./README.md ./README.md
|
||||
COPY --chown=openhands:app ./MANIFEST.in ./MANIFEST.in
|
||||
COPY --chown=openhands:app ./LICENSE ./LICENSE
|
||||
COPY --chown=openhands:app --chmod=770 ./agenthub ./agenthub
|
||||
COPY --chown=openhands:app --chmod=770 ./pyproject.toml ./pyproject.toml
|
||||
COPY --chown=openhands:app --chmod=770 ./poetry.lock ./poetry.lock
|
||||
COPY --chown=openhands:app --chmod=770 ./README.md ./README.md
|
||||
COPY --chown=openhands:app --chmod=770 ./MANIFEST.in ./MANIFEST.in
|
||||
|
||||
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
|
||||
RUN python openhands/core/download.py # No-op to download assets
|
||||
@@ -83,7 +81,7 @@ RUN python openhands/core/download.py # No-op to download assets
|
||||
# openhands:openhands -> openhands:app
|
||||
RUN find /app \! -group app -exec chgrp app {} +
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build
|
||||
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/dist ./frontend/dist
|
||||
COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
USER root
|
||||
|
||||
@@ -18,11 +18,6 @@ if [ -z "$SANDBOX_USER_ID" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$WORKSPACE_MOUNT_PATH" ]; then
|
||||
# This is set to /opt/workspace in the Dockerfile. But if the user isn't mounting, we want to unset it so that OpenHands doesn't mount at all
|
||||
unset WORKSPACE_BASE
|
||||
fi
|
||||
|
||||
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
|
||||
echo "Running OpenHands as root"
|
||||
export RUN_AS_OPENHANDS=false
|
||||
|
||||
@@ -1,40 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
# Initialize variables with default values
|
||||
image_name=""
|
||||
org_name=""
|
||||
image_name=$1
|
||||
org_name=$2
|
||||
push=0
|
||||
load=0
|
||||
tag_suffix=""
|
||||
|
||||
# Function to display usage information
|
||||
usage() {
|
||||
echo "Usage: $0 -i <image_name> [-o <org_name>] [--push] [--load] [-t <tag_suffix>]"
|
||||
echo " -i: Image name (required)"
|
||||
echo " -o: Organization name"
|
||||
echo " --push: Push the image"
|
||||
echo " --load: Load the image"
|
||||
echo " -t: Tag suffix"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse command-line options
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i) image_name="$2"; shift 2 ;;
|
||||
-o) org_name="$2"; shift 2 ;;
|
||||
--push) push=1; shift ;;
|
||||
--load) load=1; shift ;;
|
||||
-t) tag_suffix="$2"; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
# Check if required arguments are provided
|
||||
if [[ -z "$image_name" ]]; then
|
||||
echo "Error: Image name is required."
|
||||
usage
|
||||
if [[ $3 == "--push" ]]; then
|
||||
push=1
|
||||
fi
|
||||
tag_suffix=$4
|
||||
|
||||
echo "Building: $image_name"
|
||||
tags=()
|
||||
@@ -44,10 +17,9 @@ OPENHANDS_BUILD_VERSION="dev"
|
||||
cache_tag_base="buildcache"
|
||||
cache_tag="$cache_tag_base"
|
||||
|
||||
if [[ -n $RELEVANT_SHA ]]; then
|
||||
git_hash=$(git rev-parse --short "$RELEVANT_SHA")
|
||||
if [[ -n $GITHUB_SHA ]]; then
|
||||
git_hash=$(git rev-parse --short "$GITHUB_SHA")
|
||||
tags+=("$git_hash")
|
||||
tags+=("$RELEVANT_SHA")
|
||||
fi
|
||||
|
||||
if [[ -n $GITHUB_REF_NAME ]]; then
|
||||
@@ -98,9 +70,9 @@ if [[ -n "$org_name" ]]; then
|
||||
DOCKER_ORG="$org_name"
|
||||
fi
|
||||
|
||||
# If $DOCKER_IMAGE_SOURCE_TAG is set, add it to the tags
|
||||
if [[ -n "$DOCKER_IMAGE_SOURCE_TAG" ]]; then
|
||||
tags+=("$DOCKER_IMAGE_SOURCE_TAG")
|
||||
# If $DOCKER_IMAGE_HASH_TAG is set, add it to the tags
|
||||
if [[ -n "$DOCKER_IMAGE_HASH_TAG" ]]; then
|
||||
tags+=("$DOCKER_IMAGE_HASH_TAG")
|
||||
fi
|
||||
# If $DOCKER_IMAGE_TAG is set, add it to the tags
|
||||
if [[ -n "$DOCKER_IMAGE_TAG" ]]; then
|
||||
@@ -122,35 +94,14 @@ if [[ $push -eq 1 ]]; then
|
||||
args+=" --cache-to=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag,mode=max"
|
||||
fi
|
||||
|
||||
if [[ $load -eq 1 ]]; then
|
||||
args+=" --load"
|
||||
fi
|
||||
|
||||
echo "Args: $args"
|
||||
|
||||
# Modify the platform selection based on --load flag
|
||||
if [[ $load -eq 1 ]]; then
|
||||
# When loading, build only for the current platform
|
||||
platform=$(docker version -f '{{.Server.Os}}/{{.Server.Arch}}')
|
||||
else
|
||||
# For push or without load, build for multiple platforms
|
||||
platform="linux/amd64,linux/arm64"
|
||||
fi
|
||||
|
||||
echo "Building for platform(s): $platform"
|
||||
|
||||
docker buildx build \
|
||||
$args \
|
||||
--build-arg OPENHANDS_BUILD_VERSION="$OPENHANDS_BUILD_VERSION" \
|
||||
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag \
|
||||
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag_base-main \
|
||||
--platform $platform \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--provenance=false \
|
||||
-f "$dir/Dockerfile" \
|
||||
"$DOCKER_BASE_DIR"
|
||||
|
||||
# If load was requested, print the loaded images
|
||||
if [[ $load -eq 1 ]]; then
|
||||
echo "Local images built:"
|
||||
docker images "$DOCKER_REPOSITORY" --format "{{.Repository}}:{{.Tag}}"
|
||||
fi
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
###
|
||||
FROM ubuntu:22.04 AS dind
|
||||
|
||||
# https://docs.docker.com/engine/install/ubuntu/
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& install -m 0755 -d /etc/apt/keyrings \
|
||||
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
|
||||
&& chmod a+r /etc/apt/keyrings/docker.asc \
|
||||
&& echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
docker-ce \
|
||||
docker-ce-cli \
|
||||
containerd.io \
|
||||
docker-buildx-plugin \
|
||||
docker-compose-plugin \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean \
|
||||
&& apt-get autoremove -y
|
||||
|
||||
###
|
||||
FROM dind AS openhands
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
#
|
||||
RUN apt-get update && apt-get install -y \
|
||||
bash \
|
||||
build-essential \
|
||||
curl \
|
||||
git \
|
||||
git-lfs \
|
||||
software-properties-common \
|
||||
make \
|
||||
netcat \
|
||||
sudo \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean \
|
||||
&& apt-get autoremove -y
|
||||
|
||||
# https://github.com/cli/cli/blob/trunk/docs/install_linux.md
|
||||
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& apt-get update && apt-get -y install \
|
||||
gh \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean \
|
||||
&& apt-get autoremove -y
|
||||
|
||||
# Python 3.12
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y python3.12 python3.12-venv python3.12-dev python3-pip \
|
||||
&& ln -s /usr/bin/python3.12 /usr/bin/python
|
||||
|
||||
# NodeJS >= 18.17.1
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Poetry >= 1.8
|
||||
RUN curl -fsSL https://install.python-poetry.org | python3.12 - \
|
||||
&& ln -s ~/.local/bin/poetry /usr/local/bin/poetry
|
||||
|
||||
#
|
||||
RUN <<EOF
|
||||
#!/bin/bash
|
||||
printf "#!/bin/bash
|
||||
set +x
|
||||
uname -a
|
||||
docker --version
|
||||
gh --version | head -n 1
|
||||
git --version
|
||||
#
|
||||
python --version
|
||||
echo node `node --version`
|
||||
echo npm `npm --version`
|
||||
poetry --version
|
||||
netcat -h 2>&1 | head -n 1
|
||||
" > /version.sh
|
||||
chmod a+x /version.sh
|
||||
EOF
|
||||
|
||||
###
|
||||
FROM openhands AS dev
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
dnsutils \
|
||||
file \
|
||||
iproute2 \
|
||||
jq \
|
||||
lsof \
|
||||
ripgrep \
|
||||
silversearcher-ag \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean \
|
||||
&& apt-get autoremove -y
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# cache build dependencies
|
||||
RUN \
|
||||
--mount=type=bind,source=./,target=/app/ \
|
||||
<<EOF
|
||||
#!/bin/bash
|
||||
make -s clean
|
||||
make -s check-dependencies
|
||||
make -s install-python-dependencies
|
||||
|
||||
# NOTE
|
||||
# node_modules are .dockerignore-d therefore not mountable
|
||||
# make -s install-frontend-dependencies
|
||||
EOF
|
||||
|
||||
#
|
||||
CMD ["bash"]
|
||||
@@ -1,54 +0,0 @@
|
||||
# Develop in Docker
|
||||
|
||||
Install [Docker](https://docs.docker.com/engine/install/) on your host machine and run:
|
||||
|
||||
```bash
|
||||
make docker-dev
|
||||
# same as:
|
||||
cd ./containers/dev
|
||||
./dev.sh
|
||||
```
|
||||
|
||||
It could take some time if you are running for the first time as Docker will pull all the tools required for building OpenHands. The next time you run again, it should be instant.
|
||||
|
||||
## Build and run
|
||||
|
||||
If everything goes well, you should be inside a container after Docker finishes building the `openhands:dev` image similar to the following:
|
||||
|
||||
```bash
|
||||
Build and run in Docker ...
|
||||
root@93fc0005fcd2:/app#
|
||||
```
|
||||
|
||||
You may now proceed with the normal [build and run](../../Development.md) workflow as if you were on the host.
|
||||
|
||||
## Make changes
|
||||
|
||||
The source code on the host is mounted as `/app` inside docker. You may edit the files as usual either inside the Docker container or on your host with your favorite IDE/editors.
|
||||
|
||||
The following are also mapped as readonly from your host:
|
||||
|
||||
```yaml
|
||||
# host credentials
|
||||
- $HOME/.git-credentials:/root/.git-credentials:ro
|
||||
- $HOME/.gitconfig:/root/.gitconfig:ro
|
||||
- $HOME/.npmrc:/root/.npmrc:ro
|
||||
```
|
||||
|
||||
## VSCode
|
||||
|
||||
Alternatively, if you use VSCode, you could also [attach to the running container](https://code.visualstudio.com/docs/devcontainers/attach-container).
|
||||
|
||||
See details for [developing in docker](https://code.visualstudio.com/docs/devcontainers/containers) or simply ask `OpenHands` ;-)
|
||||
|
||||
## Rebuild dev image
|
||||
|
||||
You could optionally pass additional options to the build script.
|
||||
|
||||
```bash
|
||||
make docker-dev OPTIONS="--build"
|
||||
# or
|
||||
./containers/dev/dev.sh --build
|
||||
```
|
||||
|
||||
See [docker compose run](https://docs.docker.com/reference/cli/docker/compose/run/) for more options.
|
||||
@@ -1,38 +0,0 @@
|
||||
#
|
||||
services:
|
||||
dev:
|
||||
privileged: true
|
||||
build:
|
||||
context: ${OPENHANDS_WORKSPACE:-../../}
|
||||
dockerfile: ./containers/dev/Dockerfile
|
||||
image: openhands:dev
|
||||
container_name: openhands-dev
|
||||
environment:
|
||||
- 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_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${WORKSPACE_BASE:-$PWD/workspace}:/opt/workspace_base
|
||||
# source code
|
||||
- ${OPENHANDS_WORKSPACE:-../../}:/app
|
||||
# host credentials
|
||||
- $HOME/.git-credentials:/root/.git-credentials:ro
|
||||
- $HOME/.gitconfig:/root/.gitconfig:ro
|
||||
- $HOME/.npmrc:/root/.npmrc:ro
|
||||
# cache
|
||||
- cache-data:/root/.cache
|
||||
pull_policy: never
|
||||
stdin_open: true
|
||||
tty: true
|
||||
|
||||
##
|
||||
volumes:
|
||||
cache-data:
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
|
||||
function get_docker() {
|
||||
echo "Docker is required to build and run OpenHands."
|
||||
echo "https://docs.docker.com/get-started/get-docker/"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function check_tools() {
|
||||
command -v docker &>/dev/null || get_docker
|
||||
}
|
||||
|
||||
function exit_if_indocker() {
|
||||
if [ -f /.dockerenv ]; then
|
||||
echo "Running inside a Docker container. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
exit_if_indocker
|
||||
|
||||
check_tools
|
||||
|
||||
##
|
||||
OPENHANDS_WORKSPACE=$(git rev-parse --show-toplevel)
|
||||
|
||||
cd "$OPENHANDS_WORKSPACE/containers/dev/" || exit 1
|
||||
|
||||
##
|
||||
export BACKEND_HOST="0.0.0.0"
|
||||
#
|
||||
export SANDBOX_USER_ID=$(id -u)
|
||||
export WORKSPACE_BASE=${WORKSPACE_BASE:-$OPENHANDS_WORKSPACE/workspace}
|
||||
|
||||
docker compose run --rm --service-ports "$@" dev
|
||||
|
||||
##
|
||||
@@ -1,12 +1,11 @@
|
||||
# Dynamically constructed Dockerfile
|
||||
# Dynamic constructed Dockerfile
|
||||
|
||||
This folder builds a runtime image (sandbox), which will use a dynamically generated `Dockerfile`
|
||||
that depends on the `base_image` **AND** a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that is based on the current commit of `openhands`.
|
||||
This folder builds runtime image (sandbox), which will use a `Dockerfile` that is dynamically generated depends on the `base_image` AND a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that's based on the current commit of `openhands`.
|
||||
|
||||
The following command will generate a `Dockerfile` file for `nikolaik/python-nodejs:python3.12-nodejs22` (the default base image), an updated `config.sh` and the runtime source distribution files/folders into `containers/runtime`:
|
||||
The following command will generate Dockerfile for `ubuntu:22.04` and the source distribution `.tar` into `containers/runtime`.
|
||||
|
||||
```bash
|
||||
poetry run python3 openhands/runtime/utils/runtime_build.py \
|
||||
--base_image nikolaik/python-nodejs:python3.12-nodejs22 \
|
||||
--base_image ubuntu:22.04 \
|
||||
--build_folder containers/runtime
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user