mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc27eae841 | |||
| f0e5c81272 | |||
| 4984bf6ee7 | |||
| 92ddc1b46c | |||
| 367c8a9f83 | |||
| 09335d67be | |||
| eb36426d33 | |||
| ace9e6e724 | |||
| 1290a2599d | |||
| 513dd9791d | |||
| fd53378d06 | |||
| 4471002c79 | |||
| 326e75e829 | |||
| d8ad8babf6 | |||
| 8782e3ae65 | |||
| eef0ed3410 | |||
| e7a8daf3ec | |||
| 64abd4a95e | |||
| c7d575b4e1 | |||
| f781bc8343 | |||
| 9f9a65c787 | |||
| 3355baea4c | |||
| 8848e60c6d | |||
| d1e84093cc | |||
| 3f0f13d335 | |||
| 219a134bb0 | |||
| efb525a463 | |||
| 1ded123116 | |||
| 31b6967a87 | |||
| 90422e5bfd | |||
| b47da9e894 | |||
| 3401bd610d | |||
| 77a153e42f | |||
| fb9bc87e35 | |||
| b685c67263 | |||
| 2cd64bc636 | |||
| 7c81deb132 | |||
| b19f735808 | |||
| 3af6025303 | |||
| 585dba9917 | |||
| bd66d09a33 | |||
| 791b7f9f60 | |||
| 30197e616b | |||
| f7f25319e3 | |||
| 75fba59588 | |||
| 280baa24e2 | |||
| c6206f5cf2 | |||
| 7a4729c034 |
@@ -46,7 +46,7 @@ on:
|
||||
|
||||
jobs:
|
||||
del_runs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
@@ -24,20 +24,22 @@ jobs:
|
||||
build:
|
||||
if: github.repository == 'All-Hands-AI/OpenHands'
|
||||
name: Build Docusaurus
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: useblacksmith/setup-node@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
cache-dependency-path: docs/package-lock.json
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Generate Python Docs
|
||||
run: rm -rf docs/modules/python && pip install pydoc-markdown && pydoc-markdown
|
||||
- name: Install dependencies
|
||||
run: cd docs && npm ci
|
||||
- name: Build website
|
||||
@@ -52,7 +54,7 @@ jobs:
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/main' && github.repository == 'All-Hands-AI/OpenHands'
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
# This job only runs on "main" so only run one of these jobs at a time
|
||||
# otherwise it will fail if one is already running
|
||||
concurrency:
|
||||
|
||||
@@ -16,7 +16,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -25,13 +25,13 @@ jobs:
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Setup Node.js
|
||||
uses: useblacksmith/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'poetry'
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
name: Run SWE-Bench Evaluation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
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 tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- 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 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/benchmarks/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/benchmarks/swe_bench/scripts/eval_infer_remote.sh $OUTPUT_FOLDER/output.jsonl $N_PROCESSES "princeton-nlp/SWE-bench_Lite" test
|
||||
|
||||
poetry run ./evaluation/benchmarks/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 }}
|
||||
---
|
||||
You can download the full evaluation outputs [here](${{ env.ARTIFACT_URL }}).
|
||||
|
||||
- name: Post to a Slack channel
|
||||
id: slack
|
||||
uses: slackapi/slack-github-action@v2.0.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 }}
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
# Run frontend unit tests
|
||||
fe-test:
|
||||
name: FE Unit Tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: useblacksmith/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
# Builds the OpenHands Docker images
|
||||
ghcr_build_app:
|
||||
name: Build App Image
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
# Builds the runtime Docker images
|
||||
ghcr_build_runtime:
|
||||
name: Build Image
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -108,11 +108,11 @@ jobs:
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
|
||||
verify_hash_equivalence_in_runtime_and_app:
|
||||
name: Verify Hash Equivalence in Runtime and Docker images
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime, ghcr_build_app]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install poetry via pipx
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
test_runtime_root:
|
||||
name: RT Unit Tests (Root)
|
||||
needs: [ghcr_build_runtime]
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install poetry via pipx
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
# Run unit tests with the Docker runtime Docker images as openhands user
|
||||
test_runtime_oh:
|
||||
name: RT Unit Tests (openhands)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ghcr_build_runtime]
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pypoetry
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install poetry via pipx
|
||||
@@ -338,7 +338,7 @@ jobs:
|
||||
runtime_tests_check_success:
|
||||
name: All Runtime Tests Passed
|
||||
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
|
||||
steps:
|
||||
- name: All tests passed
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
runtime_tests_check_fail:
|
||||
name: All Runtime Tests Passed
|
||||
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
|
||||
steps:
|
||||
- name: Some tests failed
|
||||
@@ -358,7 +358,7 @@ jobs:
|
||||
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: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -18,7 +18,7 @@ env:
|
||||
jobs:
|
||||
run-integration-tests:
|
||||
if: github.event.label.name == 'integration-test' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: "read"
|
||||
id-token: "write"
|
||||
@@ -35,13 +35,13 @@ jobs:
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: useblacksmith/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
lint-fix-frontend:
|
||||
if: github.event.label.name == 'lint-fix'
|
||||
name: Fix frontend linting issues
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Node.js 20
|
||||
uses: useblacksmith/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install frontend dependencies
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
lint-fix-python:
|
||||
if: github.event.label.name == 'lint-fix'
|
||||
name: Fix Python linting issues
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: 'pip'
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
# Run lint on the frontend code
|
||||
lint-frontend:
|
||||
name: Lint frontend
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js 20
|
||||
uses: useblacksmith/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
# Run lint on the python code
|
||||
lint-python:
|
||||
name: Lint python
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: 'pip'
|
||||
@@ -57,11 +57,11 @@ jobs:
|
||||
# Check version consistency across documentation
|
||||
check-version-consistency:
|
||||
name: Check version consistency
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Run version consistency check
|
||||
|
||||
@@ -295,12 +295,11 @@ jobs:
|
||||
if: always()
|
||||
env:
|
||||
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
|
||||
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
|
||||
with:
|
||||
github-token: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const issueNumber = process.env.ISSUE_NUMBER;
|
||||
const issueNumber = ${{ env.ISSUE_NUMBER }};
|
||||
let logContent = '';
|
||||
|
||||
try {
|
||||
@@ -331,15 +330,13 @@ jobs:
|
||||
if: always() # Comment on issue even if the previous steps fail
|
||||
env:
|
||||
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
|
||||
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
|
||||
RESOLUTION_SUCCESS: ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}
|
||||
with:
|
||||
github-token: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const issueNumber = process.env.ISSUE_NUMBER;
|
||||
const success = process.env.RESOLUTION_SUCCESS === 'true';
|
||||
const issueNumber = ${{ env.ISSUE_NUMBER }};
|
||||
const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }};
|
||||
|
||||
let prNumber = '';
|
||||
let branchName = '';
|
||||
@@ -404,12 +401,10 @@ jobs:
|
||||
- name: Fallback Error Comment
|
||||
uses: actions/github-script@v7
|
||||
if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
|
||||
with:
|
||||
github-token: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
script: |
|
||||
const issueNumber = process.env.ISSUE_NUMBER;
|
||||
const issueNumber = ${{ env.ISSUE_NUMBER }};
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
# Run python unit tests on Linux
|
||||
test-on-linux:
|
||||
name: Python Unit Tests on Linux
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
@@ -33,13 +33,13 @@ jobs:
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Setup Node.js
|
||||
uses: useblacksmith/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'poetry'
|
||||
|
||||
@@ -12,10 +12,10 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: useblacksmith/setup-python@v6
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Install Poetry
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
trigger-job:
|
||||
name: Trigger remote eval job
|
||||
if: ${{ github.event.label.name == 'run-eval-xs' || github.event.label.name == 'run-eval-s' || github.event.label.name == 'run-eval-m' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
|
||||
@@ -33,7 +33,6 @@ Frontend:
|
||||
- Testing:
|
||||
- Run tests: `npm run test`
|
||||
- To run specific tests: `npm run test -- -t "TestName"`
|
||||
- Our test framework is vitest
|
||||
- Building:
|
||||
- Build for production: `npm run build`
|
||||
- Environment Variables:
|
||||
@@ -41,8 +40,3 @@ Frontend:
|
||||
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
|
||||
- Internationalization:
|
||||
- Generate i18n declaration file: `npm run make-i18n`
|
||||
|
||||
|
||||
## Template for Github Pull Request
|
||||
|
||||
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#! /bin/bash
|
||||
|
||||
echo "Setting up the environment..."
|
||||
|
||||
python -m pip install pre-commit
|
||||
+1
-17
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.29-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.28-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
@@ -126,19 +126,3 @@ cd ./containers/dev
|
||||
```
|
||||
|
||||
You do need [Docker](https://docs.docker.com/engine/install/) installed on your host though.
|
||||
|
||||
## Key Documentation Resources
|
||||
Here's a guide to the important documentation files in the repository:
|
||||
|
||||
- `/README.md`: Main project overview, features, and basic setup instructions
|
||||
- `/Development.md` (this file): Comprehensive guide for developers working on OpenHands
|
||||
- `/CONTRIBUTING.md`: Guidelines for contributing to the project, including code style and PR process
|
||||
- `/docs/DOC_STYLE_GUIDE.md`: Standards for writing and maintaining project documentation
|
||||
- `/openhands/README.md`: Details about the backend Python implementation
|
||||
- `/frontend/README.md`: Frontend React application setup and development guide
|
||||
- `/containers/README.md`: Information about Docker containers and deployment
|
||||
- `/tests/unit/README.md`: Guide to writing and running unit tests
|
||||
- `/evaluation/README.md`: Documentation for the evaluation framework and benchmarks
|
||||
- `/microagents/README.md`: Information about the microagents architecture and implementation
|
||||
- `/openhands/server/README.md`: Server implementation details and API documentation
|
||||
- `/openhands/runtime/README.md`: Documentation for the runtime environment and execution model
|
||||
|
||||
@@ -43,23 +43,19 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> On a public network? See our [Hardened Docker Installation](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation) guide
|
||||
> to secure your deployment by restricting network binding and implementing additional security measures.
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
Finally, you'll need a model provider and API key.
|
||||
|
||||
+16
-46
@@ -38,14 +38,13 @@ workspace_base = "./workspace"
|
||||
# Disable color in terminal output
|
||||
#disable_color = false
|
||||
|
||||
# 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
|
||||
#save_trajectory_path="./trajectories"
|
||||
|
||||
# Whether to save screenshots in the trajectory
|
||||
# The screenshots are encoded and can make trajectory json files very large
|
||||
#save_screenshots_in_trajectory = false
|
||||
|
||||
# Path to replay a trajectory, must be a file path
|
||||
# If provided, trajectory will be loaded and replayed before the
|
||||
# agent responds to any user instruction
|
||||
@@ -57,6 +56,9 @@ workspace_base = "./workspace"
|
||||
# File store type
|
||||
#file_store = "memory"
|
||||
|
||||
# List of allowed file extensions for uploads
|
||||
#file_uploads_allowed_extensions = [".*"]
|
||||
|
||||
# Maximum file size for uploads, in megabytes
|
||||
#file_uploads_max_file_size_mb = 0
|
||||
|
||||
@@ -98,12 +100,6 @@ workspace_base = "./workspace"
|
||||
# When false, a NoOpCondenserConfig (no summarization) will be used
|
||||
#enable_default_condenser = true
|
||||
|
||||
# Maximum number of concurrent conversations per user
|
||||
#max_concurrent_conversations = 3
|
||||
|
||||
# Maximum age of conversations in seconds before they are automatically closed
|
||||
#conversation_max_age_seconds = 864000 # 10 days
|
||||
|
||||
#################################### LLM #####################################
|
||||
# Configuration for LLM models (group name starts with 'llm')
|
||||
# use 'llm' for the default LLM config
|
||||
@@ -200,8 +196,6 @@ model = "gpt-4o"
|
||||
# https://github.com/All-Hands-AI/OpenHands/pull/4711
|
||||
#native_tool_calling = None
|
||||
|
||||
|
||||
|
||||
[llm.gpt4o-mini]
|
||||
api_key = ""
|
||||
model = "gpt-4o"
|
||||
@@ -215,15 +209,21 @@ model = "gpt-4o"
|
||||
##############################################################################
|
||||
[agent]
|
||||
|
||||
# Whether the browsing tool is enabled
|
||||
# whether the browsing tool is enabled
|
||||
codeact_enable_browsing = true
|
||||
|
||||
# Whether the LLM draft editor is enabled
|
||||
# whether the LLM draft editor is enabled
|
||||
codeact_enable_llm_editor = false
|
||||
|
||||
# Whether the IPython tool is enabled
|
||||
# whether the IPython tool is enabled
|
||||
codeact_enable_jupyter = true
|
||||
|
||||
# Memory enabled
|
||||
#memory_enabled = false
|
||||
|
||||
# Memory maximum threads
|
||||
#memory_max_threads = 3
|
||||
|
||||
# LLM config group to use
|
||||
#llm_config = 'your-llm-config-group'
|
||||
|
||||
@@ -258,7 +258,7 @@ llm_config = 'gpt3'
|
||||
# Use host network
|
||||
#use_host_network = false
|
||||
|
||||
# Runtime extra build args
|
||||
# runtime extra build args
|
||||
#runtime_extra_build_args = ["--network=host", "--add-host=host.docker.internal:host-gateway"]
|
||||
|
||||
# Enable auto linting after editing
|
||||
@@ -276,33 +276,6 @@ llm_config = 'gpt3'
|
||||
# BrowserGym environment to use for evaluation
|
||||
#browsergym_eval_env = ""
|
||||
|
||||
# Platform to use for building the runtime image (e.g., "linux/amd64")
|
||||
#platform = ""
|
||||
|
||||
# Force rebuild of runtime image even if it exists
|
||||
#force_rebuild_runtime = false
|
||||
|
||||
# Runtime container image to use (if not provided, will be built from base_container_image)
|
||||
#runtime_container_image = ""
|
||||
|
||||
# Keep runtime alive after session ends
|
||||
#keep_runtime_alive = false
|
||||
|
||||
# Pause closed runtimes instead of stopping them
|
||||
#pause_closed_runtimes = false
|
||||
|
||||
# Delay in seconds before closing idle runtimes
|
||||
#close_delay = 300
|
||||
|
||||
# Remove all containers when stopping the runtime
|
||||
#rm_all_containers = false
|
||||
|
||||
# Enable GPU support in the runtime
|
||||
#enable_gpu = false
|
||||
|
||||
# Additional Docker runtime kwargs
|
||||
#docker_runtime_kwargs = {}
|
||||
|
||||
#################################### Security ###################################
|
||||
# Configuration for security features
|
||||
##############################################################################
|
||||
@@ -314,9 +287,6 @@ llm_config = 'gpt3'
|
||||
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
|
||||
#security_analyzer = ""
|
||||
|
||||
# Whether to enable security analyzer
|
||||
#enable_security_analyzer = false
|
||||
|
||||
#################################### Condenser #################################
|
||||
# Condensers control how conversation history is managed and compressed when
|
||||
# the context grows too large. Each agent uses one condenser configuration.
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.29-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.28-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -75,6 +75,12 @@ const config: Config = {
|
||||
position: "left",
|
||||
label: "User Guides",
|
||||
},
|
||||
{
|
||||
type: "docSidebar",
|
||||
sidebarId: "apiSidebar",
|
||||
position: "left",
|
||||
label: "Python API",
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'left',
|
||||
|
||||
@@ -402,26 +402,5 @@
|
||||
"theme.unlistedContent.message": {
|
||||
"message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder.",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web, call APIs, and yes-even copy code snippets from StackOverflow.": {
|
||||
"message": "Utilisez l'IA pour gérer les tâches répétitives de votre backlog. Nos agents disposent des mêmes outils qu'un développeur humain : ils peuvent modifier du code, exécuter des commandes, naviguer sur le web, appeler des API et même copier des extraits de code depuis StackOverflow."
|
||||
},
|
||||
"Get started with OpenHands.": {
|
||||
"message": "Commencer avec OpenHands"
|
||||
},
|
||||
"Most Popular Links": {
|
||||
"message": "Liens Populaires"
|
||||
},
|
||||
"Customizing OpenHands to a repository": {
|
||||
"message": "Personnaliser OpenHands pour un dépôt"
|
||||
},
|
||||
"Integrating OpenHands with Github": {
|
||||
"message": "Intégrer OpenHands avec Github"
|
||||
},
|
||||
"Recommended models to use": {
|
||||
"message": "Modèles recommandés"
|
||||
},
|
||||
"Connecting OpenHands to your filesystem": {
|
||||
"message": "Connecter OpenHands à votre système de fichiers"
|
||||
}
|
||||
}
|
||||
|
||||
+34
-32
@@ -11,39 +11,41 @@ la priorité.
|
||||
|
||||
# Table des matières
|
||||
|
||||
1. [Configuration de base](#core-configuration)
|
||||
- [Clés API](#api-keys)
|
||||
- [Espace de travail](#workspace)
|
||||
- [Débogage et journalisation](#debugging-and-logging)
|
||||
- [Trajectoires](#trajectories)
|
||||
- [Stockage de fichiers](#file-store)
|
||||
- [Gestion des tâches](#task-management)
|
||||
- [Configuration du bac à sable](#sandbox-configuration)
|
||||
- [Divers](#miscellaneous)
|
||||
2. [Configuration LLM](#llm-configuration)
|
||||
- [Informations d'identification AWS](#aws-credentials)
|
||||
- [Configuration de l'API](#api-configuration)
|
||||
- [Fournisseur LLM personnalisé](#custom-llm-provider)
|
||||
1. [Configuration de base](#configuration-de-base)
|
||||
- [Clés API](#clés-api)
|
||||
- [Espace de travail](#espace-de-travail)
|
||||
- [Débogage et journalisation](#débogage-et-journalisation)
|
||||
- [Gestion des sessions](#gestion-des-sessions)
|
||||
- [Trajectoires](#trajectoires)
|
||||
- [Stockage de fichiers](#stockage-de-fichiers)
|
||||
- [Gestion des tâches](#gestion-des-tâches)
|
||||
- [Configuration du bac à sable](#configuration-du-bac-à-sable)
|
||||
- [Divers](#divers)
|
||||
2. [Configuration LLM](#configuration-llm)
|
||||
- [Informations d'identification AWS](#informations-didentification-aws)
|
||||
- [Configuration de l'API](#configuration-de-lapi)
|
||||
- [Fournisseur LLM personnalisé](#fournisseur-llm-personnalisé)
|
||||
- [Embeddings](#embeddings)
|
||||
- [Gestion des messages](#message-handling)
|
||||
- [Sélection du modèle](#model-selection)
|
||||
- [Nouvelles tentatives](#retrying)
|
||||
- [Options avancées](#advanced-options)
|
||||
3. [Configuration de l'agent](#agent-configuration)
|
||||
- [Configuration de la mémoire](#memory-configuration)
|
||||
- [Configuration LLM](#llm-configuration-1)
|
||||
- [Configuration de l'espace d'action](#actionspace-configuration)
|
||||
- [Utilisation du micro-agent](#microagent-usage)
|
||||
4. [Configuration du bac à sable](#sandbox-configuration-1)
|
||||
- [Exécution](#execution)
|
||||
- [Image de conteneur](#container-image)
|
||||
- [Mise en réseau](#networking)
|
||||
- [Linting et plugins](#linting-and-plugins)
|
||||
- [Dépendances et environnement](#dependencies-and-environment)
|
||||
- [Évaluation](#evaluation)
|
||||
5. [Configuration de sécurité](#security-configuration)
|
||||
- [Mode de confirmation](#confirmation-mode)
|
||||
- [Analyseur de sécurité](#security-analyzer)
|
||||
- [Gestion des messages](#gestion-des-messages)
|
||||
- [Sélection du modèle](#sélection-du-modèle)
|
||||
- [Nouvelles tentatives](#nouvelles-tentatives)
|
||||
- [Options avancées](#options-avancées)
|
||||
3. [Configuration de l'agent](#configuration-de-lagent)
|
||||
- [Configuration du micro-agent](#configuration-du-micro-agent)
|
||||
- [Configuration de la mémoire](#configuration-de-la-mémoire)
|
||||
- [Configuration LLM](#configuration-llm-2)
|
||||
- [Configuration de l'espace d'action](#configuration-de-lespace-daction)
|
||||
- [Utilisation du micro-agent](#utilisation-du-micro-agent)
|
||||
4. [Configuration du bac à sable](#configuration-du-bac-à-sable-2)
|
||||
- [Exécution](#exécution)
|
||||
- [Image de conteneur](#image-de-conteneur)
|
||||
- [Mise en réseau](#mise-en-réseau)
|
||||
- [Linting et plugins](#linting-et-plugins)
|
||||
- [Dépendances et environnement](#dépendances-et-environnement)
|
||||
- [Évaluation](#évaluation)
|
||||
5. [Configuration de sécurité](#configuration-de-sécurité)
|
||||
- [Mode de confirmation](#mode-de-confirmation)
|
||||
- [Analyseur de sécurité](#analyseur-de-sécurité)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -402,26 +402,5 @@
|
||||
"theme.unlistedContent.message": {
|
||||
"message": "此页面未列出。搜索引擎不会对其索引,只有拥有直接链接的用户才能访问。",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web, call APIs, and yes-even copy code snippets from StackOverflow.": {
|
||||
"message": "使用AI处理您积压的工作。我们的代理拥有与人类开发者相同的工具:它们可以修改代码、运行命令、浏览网页、调用API,甚至从StackOverflow复制代码片段。"
|
||||
},
|
||||
"Get started with OpenHands.": {
|
||||
"message": "开始使用OpenHands"
|
||||
},
|
||||
"Most Popular Links": {
|
||||
"message": "热门链接"
|
||||
},
|
||||
"Customizing OpenHands to a repository": {
|
||||
"message": "为仓库定制OpenHands"
|
||||
},
|
||||
"Integrating OpenHands with Github": {
|
||||
"message": "将OpenHands与Github集成"
|
||||
},
|
||||
"Recommended models to use": {
|
||||
"message": "推荐使用的模型"
|
||||
},
|
||||
"Connecting OpenHands to your filesystem": {
|
||||
"message": "将OpenHands连接到您的文件系统"
|
||||
}
|
||||
}
|
||||
|
||||
+33
-31
@@ -10,39 +10,41 @@
|
||||
|
||||
# 目录
|
||||
|
||||
1. [核心配置](#core-configuration)
|
||||
1. [核心配置](#核心配置)
|
||||
- [API Keys](#api-keys)
|
||||
- [工作区](#workspace)
|
||||
- [调试和日志记录](#debugging-and-logging)
|
||||
- [轨迹](#trajectories)
|
||||
- [文件存储](#file-store)
|
||||
- [任务管理](#task-management)
|
||||
- [沙箱配置](#sandbox-configuration)
|
||||
- [其他](#miscellaneous)
|
||||
2. [LLM 配置](#llm-configuration)
|
||||
- [AWS 凭证](#aws-credentials)
|
||||
- [API 配置](#api-configuration)
|
||||
- [自定义 LLM Provider](#custom-llm-provider)
|
||||
- [工作区](#工作区)
|
||||
- [调试和日志记录](#调试和日志记录)
|
||||
- [会话管理](#会话管理)
|
||||
- [轨迹](#轨迹)
|
||||
- [文件存储](#文件存储)
|
||||
- [任务管理](#任务管理)
|
||||
- [沙箱配置](#沙箱配置)
|
||||
- [其他](#其他)
|
||||
2. [LLM 配置](#llm-配置)
|
||||
- [AWS 凭证](#aws-凭证)
|
||||
- [API 配置](#api-配置)
|
||||
- [自定义 LLM Provider](#自定义-llm-provider)
|
||||
- [Embeddings](#embeddings)
|
||||
- [消息处理](#message-handling)
|
||||
- [模型选择](#model-selection)
|
||||
- [重试](#retrying)
|
||||
- [高级选项](#advanced-options)
|
||||
3. [Agent 配置](#agent-configuration)
|
||||
- [内存配置](#memory-configuration)
|
||||
- [LLM 配置](#llm-configuration-1)
|
||||
- [ActionSpace 配置](#actionspace-configuration)
|
||||
- [Microagent 使用](#microagent-usage)
|
||||
4. [沙箱配置](#sandbox-configuration-1)
|
||||
- [执行](#execution)
|
||||
- [容器镜像](#container-image)
|
||||
- [网络](#networking)
|
||||
- [Linting 和插件](#linting-and-plugins)
|
||||
- [依赖和环境](#dependencies-and-environment)
|
||||
- [评估](#evaluation)
|
||||
5. [安全配置](#security-configuration)
|
||||
- [确认模式](#confirmation-mode)
|
||||
- [安全分析器](#security-analyzer)
|
||||
- [消息处理](#消息处理)
|
||||
- [模型选择](#模型选择)
|
||||
- [重试](#重试)
|
||||
- [高级选项](#高级选项)
|
||||
3. [Agent 配置](#agent-配置)
|
||||
- [Microagent 配置](#microagent-配置)
|
||||
- [内存配置](#内存配置)
|
||||
- [LLM 配置](#llm-配置-2)
|
||||
- [ActionSpace 配置](#actionspace-配置)
|
||||
- [Microagent 使用](#microagent-使用)
|
||||
4. [沙箱配置](#沙箱配置-2)
|
||||
- [执行](#执行)
|
||||
- [容器镜像](#容器镜像)
|
||||
- [网络](#网络)
|
||||
- [Linting 和插件](#linting-和插件)
|
||||
- [依赖和环境](#依赖和环境)
|
||||
- [评估](#评估)
|
||||
5. [安全配置](#安全配置)
|
||||
- [确认模式](#确认模式)
|
||||
- [安全分析器](#安全分析器)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ The GitHub Resolver automates code fixes and provides intelligent assistance for
|
||||
## Setup
|
||||
|
||||
The Cloud GitHub Resolver is available automatically when you
|
||||
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repository-access).
|
||||
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repositories).
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Repository Customization
|
||||
|
||||
You can customize how OpenHands works with your repository by creating a
|
||||
`.openhands` directory at the root level.
|
||||
|
||||
## Microagents
|
||||
You can use microagents to extend the OpenHands prompts with information
|
||||
about your project and how you want OpenHands to work. See
|
||||
[Repository Microagents](../prompting/microagents-repo) for more information.
|
||||
|
||||
|
||||
## Setup Script
|
||||
You can add `.openhands/setup.sh`, which will be run every time OpenHands begins
|
||||
working with your repository. This is a good place to install dependencies, set
|
||||
environment variables, etc.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export MY_ENV_VAR="my value"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y lsof
|
||||
cd frontend && npm install ; cd ..
|
||||
```
|
||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Development Overview
|
||||
|
||||
This guide provides an overview of the key documentation resources available in the OpenHands repository. Whether you're looking to contribute, understand the architecture, or work on specific components, these resources will help you navigate the codebase effectively.
|
||||
|
||||
## Core Documentation
|
||||
|
||||
### Project Fundamentals
|
||||
- **Main Project Overview** (`/README.md`)
|
||||
The primary entry point for understanding OpenHands, including features and basic setup instructions.
|
||||
|
||||
- **Development Guide** (`/Development.md`)
|
||||
Comprehensive guide for developers working on OpenHands, including setup, requirements, and development workflows.
|
||||
|
||||
- **Contributing Guidelines** (`/CONTRIBUTING.md`)
|
||||
Essential information for contributors, covering code style, PR process, and contribution workflows.
|
||||
|
||||
### Component Documentation
|
||||
|
||||
#### Frontend
|
||||
- **Frontend Application** (`/frontend/README.md`)
|
||||
Complete guide for setting up and developing the React-based frontend application.
|
||||
|
||||
#### Backend
|
||||
- **Backend Implementation** (`/openhands/README.md`)
|
||||
Detailed documentation of the Python backend implementation and architecture.
|
||||
|
||||
- **Server Documentation** (`/openhands/server/README.md`)
|
||||
Server implementation details, API documentation, and service architecture.
|
||||
|
||||
- **Runtime Environment** (`/openhands/runtime/README.md`)
|
||||
Documentation covering the runtime environment, execution model, and runtime configurations.
|
||||
|
||||
#### Infrastructure
|
||||
- **Container Documentation** (`/containers/README.md`)
|
||||
Comprehensive information about Docker containers, deployment strategies, and container management.
|
||||
|
||||
### Testing and Evaluation
|
||||
- **Unit Testing Guide** (`/tests/unit/README.md`)
|
||||
Instructions for writing, running, and maintaining unit tests.
|
||||
|
||||
- **Evaluation Framework** (`/evaluation/README.md`)
|
||||
Documentation for the evaluation framework, benchmarks, and performance testing.
|
||||
|
||||
### Advanced Features
|
||||
- **Microagents Architecture** (`/microagents/README.md`)
|
||||
Detailed information about the microagents architecture, implementation, and usage.
|
||||
|
||||
### Documentation Standards
|
||||
- **Documentation Style Guide** (`/docs/DOC_STYLE_GUIDE.md`)
|
||||
Standards and guidelines for writing and maintaining project documentation.
|
||||
|
||||
## Getting Started with Development
|
||||
|
||||
If you're new to developing with OpenHands, we recommend following this sequence:
|
||||
|
||||
1. Start with the main `README.md` to understand the project's purpose and features
|
||||
2. Review the `CONTRIBUTING.md` guidelines if you plan to contribute
|
||||
3. Follow the setup instructions in `Development.md`
|
||||
4. Dive into specific component documentation based on your area of interest:
|
||||
- Frontend developers should focus on `/frontend/README.md`
|
||||
- Backend developers should start with `/openhands/README.md`
|
||||
- Infrastructure work should begin with `/containers/README.md`
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
When making changes to the codebase, please ensure that:
|
||||
1. Relevant documentation is updated to reflect your changes
|
||||
2. New features are documented in the appropriate README files
|
||||
3. Any API changes are reflected in the server documentation
|
||||
4. Documentation follows the style guide in `/docs/DOC_STYLE_GUIDE.md`
|
||||
@@ -4,7 +4,7 @@ OpenHands provides a Graphical User Interface (GUI) mode for interacting with th
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
1. Follow the installation instructions to install OpenHands.
|
||||
1. Follow the instructions in the [Installation](../installation) guide to install OpenHands.
|
||||
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Interacting with the GUI
|
||||
@@ -21,18 +21,14 @@ OpenHands provides a Graphical User Interface (GUI) mode for interacting with th
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
|
||||
|
||||
**Local Installation**: The user directly inputs their GitHub token.
|
||||
- **Local Installation**: The user directly inputs their GitHub token.
|
||||
<details>
|
||||
<summary>Setting Up a GitHub Token</summary>
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
- **New token (classic)**
|
||||
- Required scopes:
|
||||
- Click `Generate new token (classic)`.
|
||||
- Required scopes:
|
||||
- `repo` (Full control of private repositories)
|
||||
- **Fine-Grained Tokens**
|
||||
- All Repositories (You can select specific repositories, but this will impact what returns in repo search)
|
||||
- Minimal Permissions ( Select **Meta Data = Read-only** read for search, **Pull Requests = Read and Write**, **Content = Read and Write** for branch creation)
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `GitHub Settings` section.
|
||||
@@ -78,7 +74,7 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it
|
||||
- Check the browser console for any error messages.
|
||||
</details>
|
||||
|
||||
**OpenHands Cloud**: The token is obtained through GitHub OAuth authentication.
|
||||
- **OpenHands Cloud**: The token is obtained through GitHub OAuth authentication.
|
||||
|
||||
<details>
|
||||
<summary>OAuth Authentication</summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.29
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.28
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
# OpenHands Feature Overview
|
||||
|
||||

|
||||
|
||||
## 1. Workspace
|
||||
The Workspace feature provides a comprehensive development environment with the following key capabilities:
|
||||
- File Explorer: Browse, view, and manage project files and directories
|
||||
- Project Management: Import, create, and navigate between different projects
|
||||
- Integrated Development Tools: Seamless integration with various development workflows
|
||||
- File Operations:
|
||||
* View file contents
|
||||
* Create new files and folders
|
||||
* Upload and download files
|
||||
* Basic file manipulation
|
||||
|
||||
## 2. Jupyter Notebook
|
||||
The Jupyter Notebook feature offers an interactive coding and data analysis environment:
|
||||
- Interactive Code Cells: Execute Python code in a cell-based interface
|
||||
- Input and Output Tracking: Maintain a history of code inputs and their corresponding outputs
|
||||
- Persistent Session: Preserve code execution context between cells
|
||||
- Supports various Python operations and data analysis tasks
|
||||
- Real-time code execution and result visualization
|
||||
|
||||
## 3. Browser (Beta)
|
||||
The Browser feature provides web interaction capabilities:
|
||||
- Web Page Navigation: Open and browse websites within the application
|
||||
- Screenshot Capture: Automatically generate screenshots of web pages
|
||||
- Interaction Tools:
|
||||
* Click elements
|
||||
* Fill out forms
|
||||
* Scroll pages
|
||||
* Navigate through web content
|
||||
- Supports 15 different browser interaction functions
|
||||
|
||||
## 4. Terminal
|
||||
The Terminal feature offers a command-line interface within the application:
|
||||
- Execute Shell Commands: Run bash and system commands
|
||||
- Command History: Track and recall previous commands
|
||||
- Environment Interaction: Interact directly with the system's command line
|
||||
- Support for various programming and system administration tasks
|
||||
|
||||
## 5. Chat / AI Conversation
|
||||
The Chat interface provides an AI-powered conversational experience:
|
||||
- Interactive AI Assistant: Engage in natural language conversations
|
||||
- Context-Aware Responses: AI understands and responds to development-related queries
|
||||
- Action Suggestions: Provides actionable recommendations for tasks
|
||||
- Conversation Management: Create, delete, and manage different conversation threads
|
||||
|
||||
## 6. App (Beta)
|
||||
The main application interface combines all these features:
|
||||
- Integrated Workspace: Seamless integration of workspace, browser, terminal, and AI chat
|
||||
- Configurable Layout: Customize the arrangement of different feature panels
|
||||
- State Management: Maintain context and state across different features
|
||||
- Security and Privacy Controls: Manage application settings and permissions
|
||||
|
||||
### Additional Notes
|
||||
- The application is currently in beta, with ongoing improvements and feature additions
|
||||
- Supports various development workflows and AI-assisted coding
|
||||
- Designed to enhance developer productivity through integrated tools and AI assistance
|
||||
@@ -5,7 +5,7 @@ OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their
|
||||
## Azure OpenAI Configuration
|
||||
|
||||
When running OpenHands, you'll need to set the following environment variable using `-e` in the
|
||||
[docker run command](../installation#running-openhands):
|
||||
[docker run command](/modules/usage/installation#start-the-app):
|
||||
|
||||
```
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2023-05-15"
|
||||
@@ -34,7 +34,7 @@ You will need your ChatGPT deployment name which can be found on the deployments
|
||||
### Azure OpenAI Configuration
|
||||
|
||||
When running OpenHands, set the following environment variable using `-e` in the
|
||||
[docker run command](../installation#running-openhands):
|
||||
[docker run command](/modules/usage/installation#start-the-app):
|
||||
|
||||
```
|
||||
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
|
||||
|
||||
@@ -10,13 +10,13 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `Gemini`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-2.0-flash`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`).
|
||||
- `API Key` to your Gemini API key
|
||||
|
||||
## VertexAI - Google Cloud Platform Configs
|
||||
|
||||
To use Vertex AI through Google Cloud Platform when running OpenHands, you'll need to set the following environment
|
||||
variables using `-e` in the [docker run command](../installation#running-openhands):
|
||||
variables using `-e` in the [docker run command](/modules/usage/installation#start-the-app):
|
||||
|
||||
```
|
||||
GOOGLE_APPLICATION_CREDENTIALS="<json-dump-of-gcp-service-account-json>"
|
||||
|
||||
@@ -41,7 +41,7 @@ The following can be set in the OpenHands UI through the Settings:
|
||||
- `Base URL` (through `Advanced` settings)
|
||||
|
||||
There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these
|
||||
can be set through environment variables passed to the docker run command when starting the app
|
||||
can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app)
|
||||
using `-e`:
|
||||
|
||||
- `LLM_API_VERSION`
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Runtime Configuration
|
||||
|
||||
A Runtime is an environment where the OpenHands agent can edit files and run
|
||||
commands.
|
||||
|
||||
By default, OpenHands uses a Docker-based runtime, running on your local computer.
|
||||
This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
|
||||
|
||||
We also support "remote" runtimes, which are typically managed by third-parties.
|
||||
They can make setup a bit simpler and more scalable, especially
|
||||
if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
|
||||
|
||||
Additionally, we provide a "local" runtime that runs directly on your machine without Docker,
|
||||
which can be useful in controlled environments like CI pipelines.
|
||||
|
||||
## Available Runtimes
|
||||
|
||||
OpenHands supports several different runtime environments:
|
||||
|
||||
- [Docker Runtime](./runtimes/docker.md) - The default runtime that uses Docker containers for isolation (recommended for most users)
|
||||
- [OpenHands Remote Runtime](./runtimes/remote.md) - Cloud-based runtime for parallel execution (beta)
|
||||
- [Modal Runtime](./runtimes/modal.md) - Runtime provided by our partners at Modal
|
||||
- [Daytona Runtime](./runtimes/daytona.md) - Runtime provided by Daytona
|
||||
- [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker
|
||||
@@ -1,8 +1,186 @@
|
||||
---
|
||||
title: Runtime Configuration
|
||||
slug: /usage/runtimes
|
||||
---
|
||||
# Runtime Configuration
|
||||
|
||||
import { Redirect } from '@docusaurus/router';
|
||||
A Runtime is an environment where the OpenHands agent can edit files and run
|
||||
commands.
|
||||
|
||||
<Redirect to="/modules/usage/runtimes-index" />
|
||||
By default, OpenHands uses a Docker-based runtime, running on your local computer.
|
||||
This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
|
||||
|
||||
We also support "remote" runtimes, which are typically managed by third-parties.
|
||||
They can make setup a bit simpler and more scalable, especially
|
||||
if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
|
||||
|
||||
Additionally, we provide a "local" runtime that runs directly on your machine without Docker,
|
||||
which can be useful in controlled environments like CI pipelines.
|
||||
|
||||
## Docker Runtime
|
||||
This is the default Runtime that's used when you start OpenHands. You might notice
|
||||
some flags being passed to `docker run` that make this possible:
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.28-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
The `SANDBOX_RUNTIME_CONTAINER_IMAGE` from nikolaik is a pre-built runtime image
|
||||
that contains our Runtime server, as well as some basic utilities for Python and NodeJS.
|
||||
You can also [build your own runtime image](how-to/custom-sandbox-guide).
|
||||
|
||||
### Connecting to Your filesystem
|
||||
One useful feature here is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
|
||||
1. Set `WORKSPACE_BASE`:
|
||||
|
||||
```bash
|
||||
export WORKSPACE_BASE=/path/to/your/code
|
||||
|
||||
# Linux and Mac Example
|
||||
# export WORKSPACE_BASE=$HOME/OpenHands
|
||||
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
|
||||
#
|
||||
# WSL on Windows Example
|
||||
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
|
||||
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
|
||||
```
|
||||
2. Add the following options to the `docker run` command:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
# ...
|
||||
```
|
||||
|
||||
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
|
||||
any files that are mounted into its workspace.
|
||||
|
||||
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
|
||||
but seems to work well on most systems.
|
||||
|
||||
## OpenHands Remote Runtime
|
||||
|
||||
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes in parallel in the cloud.
|
||||
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
|
||||
|
||||
To use the OpenHands Remote Runtime, set the following environment variables when
|
||||
starting OpenHands:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e RUNTIME=remote \
|
||||
-e SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.app.all-hands.dev" \
|
||||
-e SANDBOX_API_KEY="your-all-hands-api-key" \
|
||||
-e SANDBOX_KEEP_RUNTIME_ALIVE="true" \
|
||||
# ...
|
||||
```
|
||||
|
||||
## Modal Runtime
|
||||
Our partners at [Modal](https://modal.com/) have also provided a runtime for OpenHands.
|
||||
|
||||
To use the Modal Runtime, create an account, and then [create an API key.](https://modal.com/settings)
|
||||
|
||||
You'll then need to set the following environment variables when starting OpenHands:
|
||||
```bash
|
||||
docker run # ...
|
||||
-e RUNTIME=modal \
|
||||
-e MODAL_API_TOKEN_ID="your-id" \
|
||||
-e MODAL_API_TOKEN_SECRET="your-secret" \
|
||||
```
|
||||
|
||||
## Daytona Runtime
|
||||
|
||||
Another option is using [Daytona](https://www.daytona.io/) as a runtime provider:
|
||||
|
||||
### Step 1: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
3. Enter a name for your key and confirm the creation.
|
||||
4. Once the key is generated, copy it.
|
||||
|
||||
### Step 2: Set Your API Key as an Environment Variable
|
||||
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
|
||||
```bash
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
|
||||
|
||||
### Step 3: Run OpenHands Locally Using Docker
|
||||
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
|
||||
```bash
|
||||
bash -i <(curl -sL https://get.daytona.io/openhands)
|
||||
```
|
||||
|
||||
#### What This Command Does:
|
||||
- Downloads the latest OpenHands release script.
|
||||
- Runs the script in an interactive Bash session.
|
||||
- Automatically pulls and runs the OpenHands container using Docker.
|
||||
|
||||
Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md)
|
||||
|
||||
## Local Runtime
|
||||
|
||||
The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
|
||||
|
||||
:::caution
|
||||
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
|
||||
:::
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before using the Local Runtime, ensure you have the following dependencies installed:
|
||||
|
||||
1. You have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
2. tmux is available on your system.
|
||||
|
||||
### Configuration
|
||||
|
||||
To use the Local Runtime, besides required configurations like the model, API key, you'll need to set the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
|
||||
|
||||
- Via environment variables:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export RUNTIME=local
|
||||
|
||||
# Optional but recommended
|
||||
export WORKSPACE_BASE=/path/to/your/workspace
|
||||
```
|
||||
|
||||
- Via `config.toml`:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
runtime = "local"
|
||||
workspace_base = "/path/to/your/workspace"
|
||||
```
|
||||
|
||||
If `WORKSPACE_BASE` is not set, the runtime will create a temporary directory for the agent to work in.
|
||||
|
||||
### Example Usage
|
||||
|
||||
Here's an example of how to start OpenHands with the Local Runtime in Headless Mode:
|
||||
|
||||
```bash
|
||||
# Set the runtime type to local
|
||||
export RUNTIME=local
|
||||
|
||||
# Optionally set a workspace directory
|
||||
export WORKSPACE_BASE=/path/to/your/project
|
||||
|
||||
# Start OpenHands
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
The Local Runtime is particularly useful for:
|
||||
|
||||
- CI/CD pipelines where Docker is not available.
|
||||
- Testing and development of OpenHands itself.
|
||||
- Environments where container usage is restricted.
|
||||
- Scenarios where direct file system access is required.
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Daytona Runtime
|
||||
|
||||
You can use [Daytona](https://www.daytona.io/) as a runtime provider:
|
||||
|
||||
## Step 1: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
3. Enter a name for your key and confirm the creation.
|
||||
4. Once the key is generated, copy it.
|
||||
|
||||
## Step 2: Set Your API Key as an Environment Variable
|
||||
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
|
||||
```bash
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
|
||||
|
||||
## Step 3: Run OpenHands Locally Using Docker
|
||||
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
|
||||
```bash
|
||||
bash -i <(curl -sL https://get.daytona.io/openhands)
|
||||
```
|
||||
|
||||
### What This Command Does:
|
||||
- Downloads the latest OpenHands release script.
|
||||
- Runs the script in an interactive Bash session.
|
||||
- Automatically pulls and runs the OpenHands container using Docker.
|
||||
|
||||
Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md)
|
||||
@@ -1,88 +0,0 @@
|
||||
# Docker Runtime
|
||||
|
||||
This is the default Runtime that's used when you start OpenHands.
|
||||
|
||||
## Image
|
||||
The `SANDBOX_RUNTIME_CONTAINER_IMAGE` from nikolaik is a pre-built runtime image
|
||||
that contains our Runtime server, as well as some basic utilities for Python and NodeJS.
|
||||
You can also [build your own runtime image](../how-to/custom-sandbox-guide).
|
||||
|
||||
## Connecting to Your filesystem
|
||||
One useful feature here is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
|
||||
1. Set `WORKSPACE_BASE`:
|
||||
|
||||
```bash
|
||||
export WORKSPACE_BASE=/path/to/your/code
|
||||
|
||||
# Linux and Mac Example
|
||||
# export WORKSPACE_BASE=$HOME/OpenHands
|
||||
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
|
||||
#
|
||||
# WSL on Windows Example
|
||||
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
|
||||
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
|
||||
```
|
||||
2. Add the following options to the `docker run` command:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
# ...
|
||||
```
|
||||
|
||||
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
|
||||
any files that are mounted into its workspace.
|
||||
|
||||
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
|
||||
but seems to work well on most systems.
|
||||
|
||||
## Hardened Docker Installation
|
||||
|
||||
When deploying OpenHands in environments where security is a priority, you should consider implementing a hardened Docker configuration. This section provides recommendations for securing your OpenHands Docker deployment beyond the default configuration.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
The default Docker configuration in the README is designed for ease of use on a local development machine. If you're running on a public network (e.g. airport WiFi),
|
||||
you should implement additional security measures.
|
||||
|
||||
### Network Binding Security
|
||||
|
||||
By default, OpenHands binds to all network interfaces (`0.0.0.0`), which can expose your instance to all networks the host is connected to. For a more secure setup:
|
||||
|
||||
1. **Restrict Network Binding**:
|
||||
|
||||
Use the `runtime_binding_address` configuration to restrict which network interfaces OpenHands listens on:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_BINDING_ADDRESS=127.0.0.1 \
|
||||
# ...
|
||||
```
|
||||
|
||||
This configuration ensures OpenHands only listens on the loopback interface (`127.0.0.1`), making it accessible only from the local machine.
|
||||
|
||||
2. **Secure Port Binding**:
|
||||
|
||||
Modify the `-p` flag to bind only to localhost instead of all interfaces:
|
||||
|
||||
```bash
|
||||
docker run # ... \
|
||||
-p 127.0.0.1:3000:3000 \
|
||||
```
|
||||
|
||||
This ensures that the OpenHands web interface is only accessible from the local machine, not from other machines on the network.
|
||||
|
||||
### Network Isolation
|
||||
|
||||
Use Docker's network features to isolate OpenHands:
|
||||
|
||||
```bash
|
||||
# Create an isolated network
|
||||
docker network create openhands-network
|
||||
|
||||
# Run OpenHands in the isolated network
|
||||
docker run # ... \
|
||||
--network openhands-network \
|
||||
```
|
||||
@@ -1,62 +0,0 @@
|
||||
# Local Runtime
|
||||
|
||||
The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
|
||||
|
||||
:::caution
|
||||
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the Local Runtime, ensure that:
|
||||
|
||||
1. You have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
2. tmux is available on your system.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the Local Runtime, besides required configurations like the model, API key, you'll need to set the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
|
||||
|
||||
- Via environment variables:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export RUNTIME=local
|
||||
|
||||
# Optional but recommended
|
||||
export WORKSPACE_BASE=/path/to/your/workspace
|
||||
```
|
||||
|
||||
- Via `config.toml`:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
runtime = "local"
|
||||
workspace_base = "/path/to/your/workspace"
|
||||
```
|
||||
|
||||
If `WORKSPACE_BASE` is not set, the runtime will create a temporary directory for the agent to work in.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's an example of how to start OpenHands with the Local Runtime in Headless Mode:
|
||||
|
||||
```bash
|
||||
# Set the runtime type to local
|
||||
export RUNTIME=local
|
||||
|
||||
# Optionally set a workspace directory
|
||||
export WORKSPACE_BASE=/path/to/your/project
|
||||
|
||||
# Start OpenHands
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
The Local Runtime is particularly useful for:
|
||||
|
||||
- CI/CD pipelines where Docker is not available.
|
||||
- Testing and development of OpenHands itself.
|
||||
- Environments where container usage is restricted.
|
||||
- Scenarios where direct file system access is required.
|
||||
@@ -1,13 +0,0 @@
|
||||
# Modal Runtime
|
||||
|
||||
Our partners at [Modal](https://modal.com/) have provided a runtime for OpenHands.
|
||||
|
||||
To use the Modal Runtime, create an account, and then [create an API key.](https://modal.com/settings)
|
||||
|
||||
You'll then need to set the following environment variables when starting OpenHands:
|
||||
```bash
|
||||
docker run # ...
|
||||
-e RUNTIME=modal \
|
||||
-e MODAL_API_TOKEN_ID="your-id" \
|
||||
-e MODAL_API_TOKEN_SECRET="your-secret" \
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
# OpenHands Remote Runtime
|
||||
|
||||
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes in parallel in the cloud.
|
||||
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
|
||||
|
||||
NOTE: This runtime is specifically designed for agent evaluation purposes only through [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications.
|
||||
+2
-54
@@ -13,11 +13,6 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Getting Started',
|
||||
id: 'usage/getting-started',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Key Features',
|
||||
id: 'usage/key-features',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Prompting',
|
||||
@@ -50,17 +45,6 @@ const sidebars: SidebarsConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Customization',
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Repository Customization',
|
||||
id: 'usage/customization/repository',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Usage Methods',
|
||||
@@ -156,40 +140,9 @@ const sidebars: SidebarsConfig = {
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
type: 'doc',
|
||||
label: 'Runtime Configuration',
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Overview',
|
||||
id: 'usage/runtimes-index',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Docker Runtime',
|
||||
id: 'usage/runtimes/docker',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Remote Runtime',
|
||||
id: 'usage/runtimes/remote',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Modal Runtime',
|
||||
id: 'usage/runtimes/modal',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Daytona Runtime',
|
||||
id: 'usage/runtimes/daytona',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Local Runtime',
|
||||
id: 'usage/runtimes/local',
|
||||
},
|
||||
],
|
||||
id: 'usage/runtimes',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
@@ -217,11 +170,6 @@ const sidebars: SidebarsConfig = {
|
||||
type: 'category',
|
||||
label: 'For OpenHands Developers',
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Development Overview',
|
||||
id: 'usage/how-to/development-overview',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Architecture',
|
||||
|
||||
@@ -25,19 +25,17 @@ export function HomepageHeader() {
|
||||
padding: '0rem 0rem 1rem'
|
||||
}}>
|
||||
<p style={{ margin: '0' }}>
|
||||
<Translate>
|
||||
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web,
|
||||
call APIs, and yes-even copy code snippets from StackOverflow.
|
||||
</Translate>
|
||||
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web,
|
||||
call APIs, and yes-even copy code snippets from StackOverflow.
|
||||
<br/>
|
||||
<Link to="/modules/usage/installation"
|
||||
<Link to="https://docs.all-hands.dev/modules/usage/installation"
|
||||
style={{
|
||||
textDecoration: 'underline',
|
||||
display: 'inline-block',
|
||||
marginTop: '0.5rem'
|
||||
}}
|
||||
>
|
||||
<Translate>Get started with OpenHands.</Translate>
|
||||
Get started with OpenHands.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Layout from '@theme/Layout';
|
||||
import { HomepageHeader } from '../components/HomepageHeader/HomepageHeader';
|
||||
import { translate } from '@docusaurus/Translate';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Link from '@docusaurus/Link';
|
||||
import { Demo } from "../components/Demo/Demo";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
@@ -23,28 +21,12 @@ export default function Home(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center', padding: '0.5rem 2rem 1.5rem' }}>
|
||||
<h2><Translate>Most Popular Links</Translate></h2>
|
||||
<h2>Most Popular Links</h2>
|
||||
<ul style={{ listStyleType: 'none'}}>
|
||||
<li>
|
||||
<Link to="/modules/usage/prompting/microagents-repo">
|
||||
<Translate>Customizing OpenHands to a repository</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/modules/usage/how-to/github-action">
|
||||
<Translate>Integrating OpenHands with Github</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/modules/usage/llms#model-recommendations">
|
||||
<Translate>Recommended models to use</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/modules/usage/runtimes#connecting-to-your-filesystem">
|
||||
<Translate>Connecting OpenHands to your filesystem</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li><a href="/modules/usage/prompting/microagents-repo">Customizing OpenHands to a repository</a></li>
|
||||
<li><a href="/modules/usage/how-to/github-action">Integrating OpenHands with Github</a></li>
|
||||
<li><a href="/modules/usage/llms#model-recommendations">Recommended models to use</a></li>
|
||||
<li><a href="/modules/usage/runtimes#connecting-to-your-filesystem">Connecting OpenHands to your filesystem</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -53,7 +53,6 @@ fi
|
||||
if [ -n "$AGENT_CONFIG" ]; then
|
||||
echo "AGENT_CONFIG: $AGENT_CONFIG"
|
||||
COMMAND="$COMMAND --agent-config $AGENT_CONFIG"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
|
||||
@@ -18,20 +18,6 @@ Please follow instruction [here](../../README.md#setup) to setup your local deve
|
||||
|
||||
## Run Inference (Rollout) on SWE-Bench Instances: Generate Patch from Problem Statement
|
||||
|
||||
> [!NOTE]
|
||||
> **Iterative Evaluation Protocol**
|
||||
>
|
||||
> We have an iterative approach for more stable and reproducible results:
|
||||
> - For each instance, we attempt to generate a solution up to 3 times
|
||||
> - Each attempt continues until either:
|
||||
> 1. The agent successfully produces a patch with `AgentFinishAction`, or
|
||||
> 2. The attempt reaches the maximum iteration limit
|
||||
> - If an attempt fails, we retry with a fresh attempt (up to the 3-attempt maximum)
|
||||
> - If your LLM config has temperature=0, we will automatically use temperature=0.1 for the 2nd and 3rd attempts
|
||||
>
|
||||
> To enable this iterative protocol, set `export ITERATIVE_EVAL_MODE=true`
|
||||
|
||||
|
||||
### Running Locally with Docker
|
||||
|
||||
Make sure your Docker daemon is running, and you have ample disk space (at least 200-500GB, depends on the SWE-Bench set you are running on) for the instance-level docker image.
|
||||
@@ -59,7 +45,7 @@ to `CodeActAgent`.
|
||||
default, the script evaluates the entire SWE-bench_Lite test set (300 issues). Note:
|
||||
in order to use `eval_limit`, you must also set `agent`.
|
||||
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
|
||||
default, it is set to 60.
|
||||
default, it is set to 30.
|
||||
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
|
||||
default, it is set to 1.
|
||||
- `dataset`, a huggingface dataset name. e.g. `princeton-nlp/SWE-bench`, `princeton-nlp/SWE-bench_Lite`, or `princeton-nlp/SWE-bench_Verified`, specifies which dataset to evaluate on.
|
||||
|
||||
@@ -37,10 +37,9 @@ from openhands.core.config import (
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.critic import AgentFinishedCritic
|
||||
from openhands.events.action import CmdRunAction, MessageAction
|
||||
from openhands.events.observation import CmdOutputObservation, ErrorObservation
|
||||
from openhands.events.serialization.event import event_from_dict, event_to_dict
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
from openhands.utils.shutdown_listener import sleep_if_should_continue
|
||||
@@ -123,9 +122,7 @@ You SHOULD NEVER attempt to browse the web.
|
||||
|
||||
|
||||
# TODO: migrate all swe-bench docker to ghcr.io/openhands
|
||||
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
|
||||
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
|
||||
)
|
||||
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/')
|
||||
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
|
||||
|
||||
|
||||
@@ -640,132 +637,20 @@ if __name__ == '__main__':
|
||||
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
print(f'### OUTPUT FILE: {output_file} ###')
|
||||
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
|
||||
|
||||
# Run evaluation in iterative mode:
|
||||
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
|
||||
ITERATIVE_EVAL_MODE = (
|
||||
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8 * 60 * 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
|
||||
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
|
||||
)
|
||||
|
||||
if not ITERATIVE_EVAL_MODE:
|
||||
# load the dataset
|
||||
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8
|
||||
* 60
|
||||
* 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
else:
|
||||
critic = AgentFinishedCritic()
|
||||
|
||||
def get_cur_output_file_path(attempt: int) -> str:
|
||||
return (
|
||||
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
|
||||
)
|
||||
|
||||
eval_ids = None
|
||||
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
|
||||
cur_output_file = get_cur_output_file_path(attempt)
|
||||
logger.info(
|
||||
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
|
||||
)
|
||||
|
||||
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
|
||||
# so hopefully we get slightly different results
|
||||
if attempt > 1 and metadata.llm_config.temperature == 0:
|
||||
logger.info(
|
||||
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
|
||||
)
|
||||
metadata.llm_config.temperature = 0.1
|
||||
|
||||
# Load instances - at first attempt, we evaluate all instances
|
||||
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
|
||||
instances = prepare_dataset(
|
||||
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
|
||||
)
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
# Run evaluation - but save them to cur_output_file
|
||||
logger.info(
|
||||
f'Evaluating {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
cur_output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8
|
||||
* 60
|
||||
* 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
|
||||
# When eval is done, we update eval_ids to the instances that failed the current attempt
|
||||
instances_failed = []
|
||||
logger.info(
|
||||
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
history = [event_from_dict(event) for event in instance['history']]
|
||||
critic_result = critic.evaluate(history)
|
||||
if not critic_result.success:
|
||||
instances_failed.append(instance['instance_id'])
|
||||
logger.info(
|
||||
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
|
||||
)
|
||||
eval_ids = instances_failed
|
||||
|
||||
# If no instances failed, we break
|
||||
if len(instances_failed) == 0:
|
||||
break
|
||||
|
||||
# Then we should aggregate the results from all attempts into the original output file
|
||||
# and remove the intermediate files
|
||||
logger.info(
|
||||
'Aggregating results from all attempts into the original output file...'
|
||||
)
|
||||
fout = open(output_file, 'w')
|
||||
added_instance_ids = set()
|
||||
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
|
||||
cur_output_file = get_cur_output_file_path(attempt)
|
||||
if not os.path.exists(cur_output_file):
|
||||
logger.warning(
|
||||
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
|
||||
)
|
||||
continue
|
||||
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
if instance['instance_id'] not in added_instance_ids:
|
||||
fout.write(line)
|
||||
added_instance_ids.add(instance['instance_id'])
|
||||
logger.info(
|
||||
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
|
||||
)
|
||||
fout.close()
|
||||
logger.info(
|
||||
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
|
||||
)
|
||||
|
||||
@@ -25,8 +25,8 @@ if [ -z "$AGENT" ]; then
|
||||
fi
|
||||
|
||||
if [ -z "$MAX_ITER" ]; then
|
||||
echo "MAX_ITER not specified, use default 60"
|
||||
MAX_ITER=60
|
||||
echo "MAX_ITER not specified, use default 100"
|
||||
MAX_ITER=100
|
||||
fi
|
||||
|
||||
if [ -z "$RUN_WITH_BROWSING" ]; then
|
||||
|
||||
@@ -42,7 +42,7 @@ To evaluate the generated tests, use the `eval_infer.sh` script:
|
||||
./evaluation/benchmarks/testgeneval/scripts/eval_infer.sh $YOUR_OUTPUT_JSONL [instance_id] [dataset_name] [split] [num_workers] [skip_mutation]
|
||||
|
||||
# Example
|
||||
./evaluation/benchmarks/testgeneval/scripts/eval_infer.sh evaluation/evaluation_outputs/outputs/kjain14__testgenevallite-test/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/output.jsonl
|
||||
./evaluation/benchmarks/testgeneval/scripts/eval_infer.sh evaluation/evaluation_outputs/outputs/testgeneval/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/output.jsonl
|
||||
```
|
||||
|
||||
Optional arguments:
|
||||
@@ -52,7 +52,7 @@ Optional arguments:
|
||||
- `num_workers`: Number of workers for running docker (default: 1)
|
||||
- `skip_mutation`: Skip mutation testing (enter `true` if desired)
|
||||
|
||||
The evaluation results will be saved to `evaluation/evaluation_outputs/outputs/kjain14__testgenevallite-test/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/` with `output.testgeneval.jsonl` containing the metrics.
|
||||
The evaluation results will be saved to `evaluation/evaluation_outputs/outputs/testgeneval/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/` with `output.testgeneval.jsonl` containing the metrics.
|
||||
|
||||
## Metrics
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from tree_sitter import Language, Parser
|
||||
|
||||
|
||||
def total_byte_entropy_stats(python_code):
|
||||
@@ -318,11 +322,16 @@ def compute_regression(results):
|
||||
|
||||
|
||||
def compute_readability(python_code):
|
||||
# Create parser and set up language
|
||||
import tree_sitter_python
|
||||
from tree_sitter import Language, Parser
|
||||
|
||||
parser = Parser(Language(tree_sitter_python.language()))
|
||||
parser = Parser()
|
||||
this_dir = Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
parser.set_language(Language.build_library(
|
||||
# Store the library in the `build` directory
|
||||
this_dir / "build" / "my-languages.so",
|
||||
# Include one or more languages
|
||||
[
|
||||
this_dir / "tree-sitter-python"
|
||||
]
|
||||
).get_language('python'))
|
||||
|
||||
results = code_stats(python_code)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from evaluation.benchmarks.testgeneval.constants import (
|
||||
)
|
||||
from evaluation.benchmarks.testgeneval.metrics import (
|
||||
bleu,
|
||||
code_bleu,
|
||||
edit_sim,
|
||||
exact_match,
|
||||
rouge_l,
|
||||
@@ -93,6 +94,7 @@ def compute_lexical_metrics(pred_suite, gold_suite):
|
||||
'gold_readability': readability_gold,
|
||||
'pred_methods': pred_methods,
|
||||
'gold_methods': gold_methods,
|
||||
'code_bleu': code_bleu(preds, golds, 'Python3'),
|
||||
'bleu': bleu(preds, golds),
|
||||
'xmatch': exact_match(preds, golds),
|
||||
'edit_sim': edit_sim(preds, golds),
|
||||
@@ -332,6 +334,7 @@ def process_instance(
|
||||
'gold_readability': -1,
|
||||
'pred_methods': -1,
|
||||
'gold_methods': -1,
|
||||
'code_bleu': -1,
|
||||
'bleu': -1,
|
||||
'xmatch': -1,
|
||||
'edit_sim': -1,
|
||||
@@ -616,6 +619,7 @@ if __name__ == '__main__':
|
||||
'gold_loc',
|
||||
'pred_methods',
|
||||
'gold_methods',
|
||||
'code_bleu',
|
||||
'bleu',
|
||||
'xmatch',
|
||||
'edit_sim',
|
||||
|
||||
@@ -6,11 +6,12 @@ import numpy as np
|
||||
from fuzzywuzzy import fuzz
|
||||
from rouge import Rouge
|
||||
|
||||
|
||||
|
||||
# increase recursion depth to ensure ROUGE can be calculated for long sentences
|
||||
if sys.getrecursionlimit() < 10_000:
|
||||
sys.setrecursionlimit(10_000)
|
||||
|
||||
|
||||
def bleu(gold: List[str], pred: List[str]) -> float:
|
||||
"""
|
||||
Calculate BLEU score, using smoothing method 2 with auto reweighting, in the range of 0~100.
|
||||
@@ -38,7 +39,7 @@ def batch_bleu(golds: List[List[str]], preds: List[List[str]]) -> List[float]:
|
||||
:return: list of BLEU scores
|
||||
"""
|
||||
if len(golds) != len(preds):
|
||||
raise ValueError('golds and preds must have the same length')
|
||||
raise ValueError("golds and preds must have the same length")
|
||||
return [bleu(gold, pred) for gold, pred in zip(golds, preds)]
|
||||
|
||||
|
||||
@@ -51,7 +52,7 @@ def corpus_bleu(golds: List[List[str]], preds: List[List[str]]) -> float:
|
||||
:return: corpus-level BLEU score
|
||||
"""
|
||||
if len(golds) != len(preds):
|
||||
raise ValueError('golds and preds must have the same length')
|
||||
raise ValueError("golds and preds must have the same length")
|
||||
return 100.0 * nltk.translate.bleu_score.corpus_bleu(
|
||||
[[gold] for gold in golds],
|
||||
preds,
|
||||
@@ -61,7 +62,7 @@ def corpus_bleu(golds: List[List[str]], preds: List[List[str]]) -> float:
|
||||
|
||||
|
||||
def edit_sim(
|
||||
gold: Union[str, List[str]], pred: Union[str, List[str]], sep: str = ' '
|
||||
gold: Union[str, List[str]], pred: Union[str, List[str]], sep: str = " "
|
||||
) -> float:
|
||||
"""
|
||||
Calculate char-level edit similarity, in the range of 0~100.
|
||||
@@ -83,7 +84,7 @@ def edit_sim(
|
||||
def batch_edit_sim(
|
||||
golds: List[Union[str, List[str]]],
|
||||
preds: List[Union[str, List[str]]],
|
||||
sep: str = ' ',
|
||||
sep: str = " ",
|
||||
) -> List[float]:
|
||||
"""
|
||||
Calculate char-level edit similarity for a batch of sentences.
|
||||
@@ -94,11 +95,11 @@ def batch_edit_sim(
|
||||
:return: list of char-level edit similarity
|
||||
"""
|
||||
if len(golds) != len(preds):
|
||||
raise ValueError('golds and preds must have the same length')
|
||||
raise ValueError("golds and preds must have the same length")
|
||||
return [edit_sim(gold, pred, sep) for gold, pred in zip(golds, preds)]
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def exact_match(gold: T, pred: T) -> float:
|
||||
@@ -123,12 +124,12 @@ def batch_exact_match(golds: List[T], preds: List[T]) -> List[float]:
|
||||
:return: list of exact match accuracy
|
||||
"""
|
||||
if len(golds) != len(preds):
|
||||
raise ValueError('golds and preds must have the same length')
|
||||
raise ValueError("golds and preds must have the same length")
|
||||
return [exact_match(gold, pred) for gold, pred in zip(golds, preds)]
|
||||
|
||||
|
||||
def rouge_l(
|
||||
gold: Union[str, List[str]], pred: Union[str, List[str]], sep: str = ' '
|
||||
gold: Union[str, List[str]], pred: Union[str, List[str]], sep: str = " "
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Calculate ROUGE-L F1, precision, and recall scores, in the range of 0~100.
|
||||
@@ -138,7 +139,7 @@ def rouge_l(
|
||||
:return: {"p": precision, "r": recall, "f": F1}
|
||||
"""
|
||||
if len(pred) == 0 or len(gold) == 0:
|
||||
return {'p': 0.0, 'r': 0.0, 'f': 0.0}
|
||||
return {"p": 0.0, "r": 0.0, "f": 0.0}
|
||||
if isinstance(gold, list):
|
||||
gold = sep.join(gold)
|
||||
if isinstance(pred, list):
|
||||
@@ -146,15 +147,15 @@ def rouge_l(
|
||||
try:
|
||||
rouge = Rouge()
|
||||
scores = rouge.get_scores(hyps=pred, refs=gold, avg=True)
|
||||
return {x: scores['rouge-l'][x] * 100.0 for x in ['p', 'r', 'f']}
|
||||
return {x: scores["rouge-l"][x] * 100.0 for x in ["p", "r", "f"]}
|
||||
except ValueError:
|
||||
return {'p': 0.0, 'r': 0.0, 'f': 0.0}
|
||||
return {"p": 0.0, "r": 0.0, "f": 0.0}
|
||||
|
||||
|
||||
def batch_rouge_l(
|
||||
golds: List[Union[str, List[str]]],
|
||||
preds: List[Union[str, List[str]]],
|
||||
sep: str = ' ',
|
||||
sep: str = " ",
|
||||
) -> Dict[str, List[float]]:
|
||||
"""
|
||||
Calculate ROUGE-L F1, precision, and recall scores for a batch of sentences.
|
||||
@@ -165,9 +166,9 @@ def batch_rouge_l(
|
||||
:return: list of {"p": precision, "r": recall, "f": F1}
|
||||
"""
|
||||
if len(golds) != len(preds):
|
||||
raise ValueError('golds and preds must have the same length')
|
||||
raise ValueError("golds and preds must have the same length")
|
||||
scores = [rouge_l(gold, pred, sep) for gold, pred in zip(golds, preds)]
|
||||
return {x: [score[x] for score in scores] for x in ['p', 'r', 'f']}
|
||||
return {x: [score[x] for score in scores] for x in ["p", "r", "f"]}
|
||||
|
||||
|
||||
def accuracy(
|
||||
@@ -219,7 +220,7 @@ def batch_accuracy(
|
||||
:return: list of accuracy
|
||||
"""
|
||||
if len(golds) != len(preds):
|
||||
raise ValueError('golds and preds must have the same length')
|
||||
raise ValueError("golds and preds must have the same length")
|
||||
return [accuracy(gold, pred, ignore) for gold, pred in zip(golds, preds)]
|
||||
|
||||
|
||||
@@ -273,7 +274,7 @@ def self_bleu(samples: List[List[str]]) -> float:
|
||||
return np.mean(scores).item()
|
||||
|
||||
|
||||
def self_edit_distance(samples: List[Union[str, List[str]]], sep=' ') -> float:
|
||||
def self_edit_distance(samples: List[Union[str, List[str]]], sep=" ") -> float:
|
||||
"""
|
||||
Calculate self-edit-distance among the samples.
|
||||
:param samples: the chosen m samples
|
||||
@@ -299,11 +300,12 @@ def self_edit_distance(samples: List[Union[str, List[str]]], sep=' ') -> float:
|
||||
return np.mean(scores).item()
|
||||
|
||||
|
||||
|
||||
QUALITY_METRICS: Dict[str, Callable[[List[str], List[str]], float]] = {
|
||||
'bleu': bleu,
|
||||
'xmatch': exact_match,
|
||||
'edit-sim': edit_sim,
|
||||
'rouge-f': lambda g, p: rouge_l(g, p)['f'],
|
||||
'rouge-p': lambda g, p: rouge_l(g, p)['p'],
|
||||
'rouge-r': lambda g, p: rouge_l(g, p)['r'],
|
||||
}
|
||||
"bleu": bleu,
|
||||
"xmatch": exact_match,
|
||||
"edit-sim": edit_sim,
|
||||
"rouge-f": lambda g, p: rouge_l(g, p)["f"],
|
||||
"rouge-p": lambda g, p: rouge_l(g, p)["p"],
|
||||
"rouge-r": lambda g, p: rouge_l(g, p)["r"],
|
||||
}
|
||||
@@ -1,41 +1,30 @@
|
||||
import re
|
||||
|
||||
from pygments.lexers.python import PythonLexer
|
||||
|
||||
|
||||
def tokenize_code(code):
|
||||
lexer = PythonLexer()
|
||||
tokens = process_pygments_tokens(lexer.get_tokens(code))
|
||||
return tokens
|
||||
|
||||
|
||||
def process_pygments_tokens(tokens):
|
||||
new_tokens = []
|
||||
|
||||
for token in tokens:
|
||||
if (
|
||||
str(token[0]) == 'Token.Text'
|
||||
and re.match(r'\s+', token[1])
|
||||
or str(token[0]) == 'Token.Text.Whitespace'
|
||||
):
|
||||
if str(token[0]) == "Token.Text" and re.match(r'\s+', token[1]) or str(token[0]) == "Token.Text.Whitespace":
|
||||
continue
|
||||
new_tokens.append(token[1])
|
||||
|
||||
new_tokens_final = []
|
||||
i = 0
|
||||
while i < len(new_tokens) - 2:
|
||||
if (
|
||||
new_tokens[i] == '"'
|
||||
and new_tokens[i + 1] == 'STR'
|
||||
and new_tokens[i + 2] == '"'
|
||||
):
|
||||
new_tokens_final.append('"STR"')
|
||||
while i < len(new_tokens)-2:
|
||||
if new_tokens[i] == '"' and new_tokens[i+1]=='STR' and new_tokens[i+2] == '"':
|
||||
new_tokens_final.append("\"STR\"")
|
||||
i = i + 3
|
||||
else:
|
||||
new_tokens_final.append(new_tokens[i])
|
||||
i = i + 1
|
||||
|
||||
for i in range(len(new_tokens) - 2, len(new_tokens)):
|
||||
|
||||
for i in range(len(new_tokens)-2, len(new_tokens)):
|
||||
if i >= 0:
|
||||
new_tokens_final.append(new_tokens[i])
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@ def get_config(
|
||||
codeact_enable_browsing=RUN_WITH_BROWSING,
|
||||
codeact_enable_llm_editor=False,
|
||||
condenser=metadata.condenser_config,
|
||||
enable_prompt_extensions=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
from evaluation.testgeneval.eval_infer import process_test_suite
|
||||
from openhands.events.serialization import event_from_dict
|
||||
|
||||
tqdm.pandas()
|
||||
|
||||
@@ -20,8 +20,7 @@ print(
|
||||
f'Downloading gold test suites from {args.dataset_name} (split: {args.split}) to {output_filepath}'
|
||||
)
|
||||
test_suites = [
|
||||
{'instance_id': row['instance_id'], 'test_suite': row['test_src']}
|
||||
for row in dataset
|
||||
{'instance_id': row['instance_id'], 'test_suite': row['test_src']} for row in dataset
|
||||
]
|
||||
print(f'{len(test_suites)} test suites loaded')
|
||||
pd.DataFrame(test_suites).to_json(output_filepath, lines=True, orient='records')
|
||||
|
||||
@@ -90,7 +90,9 @@ if __name__ == '__main__':
|
||||
break
|
||||
|
||||
# print the error counter (with percentage)
|
||||
print(f'Average coverage for {num_lines} ({coverage / num_lines * 100:.2f}%)')
|
||||
print(
|
||||
f'Average coverage for {num_lines} ({coverage / num_lines * 100:.2f}%)'
|
||||
)
|
||||
print(
|
||||
f'Average mutation score for {num_lines} ({mutation_score / num_lines * 100:.2f}%)'
|
||||
)
|
||||
|
||||
@@ -573,7 +573,6 @@ def get_default_sandbox_config_for_eval() -> SandboxConfig:
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
runtime_startup_env_vars={'NO_CHANGE_TIMEOUT_SECONDS': '30'},
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
|
||||
+9
-112
@@ -50,29 +50,27 @@ This will start the application in development mode. Open [http://localhost:3001
|
||||
|
||||
### Running the Application with the Actual Backend (Production Mode)
|
||||
|
||||
To run the application with the actual backend:
|
||||
There are two ways to run the application with the actual backend:
|
||||
|
||||
```sh
|
||||
# Build the application from the root directory
|
||||
make build
|
||||
|
||||
# Start the application
|
||||
make run
|
||||
make start
|
||||
```
|
||||
Or to run backend and frontend seperately.
|
||||
|
||||
OR
|
||||
|
||||
```sh
|
||||
# Start the backend from the root directory
|
||||
make start-backend
|
||||
|
||||
# Serve the frontend
|
||||
make start-frontend or
|
||||
cd frontend && npm start -- --port 3001
|
||||
```
|
||||
# Build the frontend
|
||||
cd frontend && npm run build
|
||||
|
||||
Start frontend with Mock Service Worker (MSW), see testing for more info.
|
||||
```sh
|
||||
npm run dev:mock or npm run dev:mock:saas
|
||||
# Serve the frontend
|
||||
npm start -- --port 3001
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
@@ -123,113 +121,12 @@ components
|
||||
|
||||
## Testing
|
||||
|
||||
### Testing Framework and Tools
|
||||
We use `Vitest` for testing. To run the tests, run the following command:
|
||||
|
||||
We use the following testing tools:
|
||||
- **Test Runner**: Vitest
|
||||
- **Rendering**: React Testing Library
|
||||
- **User Interactions**: @testing-library/user-event
|
||||
- **API Mocking**: [Mock Service Worker (MSW)](https://mswjs.io/)
|
||||
- **Code Coverage**: Vitest with V8 coverage
|
||||
|
||||
### Running Tests
|
||||
|
||||
To run all tests:
|
||||
```sh
|
||||
npm run test
|
||||
```
|
||||
|
||||
To run tests with coverage:
|
||||
```sh
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Testing Best Practices
|
||||
|
||||
1. **Component Testing**
|
||||
- Test components in isolation
|
||||
- Use our custom [`renderWithProviders()`](https://github.com/All-Hands-AI/OpenHands/blob/ce26f1c6d3feec3eedf36f823dee732b5a61e517/frontend/test-utils.tsx#L56-L85) that wraps the components we want to test in our providers. It is especially useful for components that use Redux
|
||||
- Use `render()` from React Testing Library to render components
|
||||
- Prefer querying elements by role, label, or test ID over CSS selectors
|
||||
- Test both rendering and interaction scenarios
|
||||
|
||||
2. **User Event Simulation**
|
||||
- Use `userEvent` for simulating realistic user interactions
|
||||
- Test keyboard events, clicks, typing, and other user actions
|
||||
- Handle edge cases like disabled states, empty inputs, etc.
|
||||
|
||||
3. **Mocking**
|
||||
- We test components that make network requests by mocking those requests with Mock Service Worker (MSW)
|
||||
- Use `vi.fn()` to create mock functions for callbacks and event handlers
|
||||
- Mock external dependencies and API calls (more info)[https://mswjs.io/docs/getting-started]
|
||||
- Verify mock function calls using `.toHaveBeenCalledWith()`, `.toHaveBeenCalledTimes()`
|
||||
|
||||
4. **Accessibility Testing**
|
||||
- Use `toBeInTheDocument()` to check element presence
|
||||
- Test keyboard navigation and screen reader compatibility
|
||||
- Verify correct ARIA attributes and roles
|
||||
|
||||
5. **State and Prop Testing**
|
||||
- Test component behavior with different prop combinations
|
||||
- Verify state changes and conditional rendering
|
||||
- Test error states and loading scenarios
|
||||
|
||||
6. **Internationalization (i18n) Testing**
|
||||
- Test translation keys and placeholders
|
||||
- Verify text rendering across different languages
|
||||
|
||||
Example Test Structure:
|
||||
```typescript
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
describe("ComponentName", () => {
|
||||
it("should render correctly", () => {
|
||||
render(<Component />);
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle user interactions", async () => {
|
||||
const mockCallback = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<Component onClick={mockCallback} />);
|
||||
const button = screen.getByRole("button");
|
||||
|
||||
await user.click(button);
|
||||
expect(mockCallback).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Example Tests in the Codebase
|
||||
|
||||
For real-world examples of testing, check out these test files:
|
||||
|
||||
1. **Chat Input Component Test**:
|
||||
[`__tests__/components/chat/chat-input.test.tsx`](https://github.com/All-Hands-AI/OpenHands/blob/main/frontend/__tests__/components/chat/chat-input.test.tsx)
|
||||
- Demonstrates comprehensive testing of a complex input component
|
||||
- Covers various scenarios like submission, disabled states, and user interactions
|
||||
|
||||
2. **File Explorer Component Test**:
|
||||
[`__tests__/components/file-explorer/file-explorer.test.tsx`](https://github.com/All-Hands-AI/OpenHands/blob/main/frontend/__tests__/components/file-explorer/file-explorer.test.tsx)
|
||||
- Shows testing of a more complex component with multiple interactions
|
||||
- Illustrates testing of nested components and state management
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Aim for high test coverage, especially for critical components
|
||||
- Focus on testing different scenarios and edge cases
|
||||
- Use code coverage reports to identify untested code paths
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
Tests are automatically run during:
|
||||
- Pre-commit hooks
|
||||
- Pull request checks
|
||||
- CI/CD pipeline
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read the [CONTRIBUTING.md](../CONTRIBUTING.md) file for details on our code of conduct, and the process for submitting pull requests to us.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { vi } from "vitest"
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual("react-i18next");
|
||||
@@ -49,7 +48,7 @@ describe("ExpandableMessage", () => {
|
||||
id="OBSERVATION_MESSAGE$RUN"
|
||||
message="Command executed successfully"
|
||||
type="action"
|
||||
success
|
||||
success={true}
|
||||
/>,
|
||||
);
|
||||
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
|
||||
@@ -94,31 +93,4 @@ describe("ExpandableMessage", () => {
|
||||
expect(container).toHaveClass("border-neutral-300");
|
||||
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the out of credits message when the user is out of credits", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - We only care about the APP_MODE and FEATURE_FLAGS fields
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: () => (
|
||||
<ExpandableMessage
|
||||
id="STATUS$ERROR_LLM_OUT_OF_CREDITS"
|
||||
message=""
|
||||
type=""
|
||||
/>
|
||||
),
|
||||
path: "/",
|
||||
},
|
||||
]);
|
||||
|
||||
renderWithProviders(<RouterStub />);
|
||||
await screen.findByTestId("out-of-credits");
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsProvider } from "#/context/settings-context";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("AnalyticsConsentFormModal", () => {
|
||||
@@ -16,7 +17,7 @@ describe("AnalyticsConsentFormModal", () => {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
<SettingsProvider>{children}</SettingsProvider>
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
),
|
||||
|
||||
+43
-82
@@ -1,4 +1,4 @@
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
vi,
|
||||
} from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
@@ -19,13 +18,10 @@ describe("ConversationCard", () => {
|
||||
const onClick = vi.fn();
|
||||
const onDelete = vi.fn();
|
||||
const onChangeTitle = vi.fn();
|
||||
const onDownloadWorkspace = vi.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal("window", {
|
||||
open: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
});
|
||||
vi.stubGlobal("window", { open: vi.fn() });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -37,7 +33,7 @@ describe("ConversationCard", () => {
|
||||
});
|
||||
|
||||
it("should render the conversation card", () => {
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
@@ -56,7 +52,7 @@ describe("ConversationCard", () => {
|
||||
});
|
||||
|
||||
it("should render the selectedRepository if available", () => {
|
||||
const { rerender } = renderWithProviders(
|
||||
const { rerender } = render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
@@ -87,7 +83,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
it("should toggle a context menu when clicking the ellipsis button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
@@ -112,7 +108,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
it("should call onDelete when the delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -136,7 +132,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
test("clicking the selectedRepository should not trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -157,7 +153,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
test("conversation title should call onChangeTitle when changed and blurred", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -186,7 +182,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
it("should reset title and not call onChangeTitle when the title is empty", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -210,7 +206,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
test("clicking the title should trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
@@ -230,7 +226,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
test("clicking the title should not trigger the onClick handler if edit mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -251,7 +247,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
test("clicking the delete button should not trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -273,13 +269,14 @@ describe("ConversationCard", () => {
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show display cost button only when showDisplayCostOption is true", async () => {
|
||||
it("should call onDownloadWorkspace when the download button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
isActive
|
||||
onDownloadWorkspace={onDownloadWorkspace}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
@@ -289,64 +286,17 @@ describe("ConversationCard", () => {
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
// Wait for context menu to appear
|
||||
const menu = await screen.findByTestId("context-menu");
|
||||
expect(
|
||||
within(menu).queryByTestId("display-cost-button"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Close menu
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
rerender(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
showDisplayCostOption
|
||||
isActive
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Open menu again
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
// Wait for context menu to appear and check for display cost button
|
||||
const newMenu = await screen.findByTestId("context-menu");
|
||||
within(newMenu).getByTestId("display-cost-button");
|
||||
});
|
||||
|
||||
it("should show metrics modal when clicking the display cost button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
showDisplayCostOption
|
||||
/>,
|
||||
);
|
||||
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const menu = screen.getByTestId("context-menu");
|
||||
const displayCostButton = within(menu).getByTestId("display-cost-button");
|
||||
const downloadButton = within(menu).getByTestId("download-button");
|
||||
|
||||
await user.click(displayCostButton);
|
||||
await user.click(downloadButton);
|
||||
|
||||
// Verify if metrics modal is displayed by checking for the modal content
|
||||
expect(screen.getByText("Metrics Information")).toBeInTheDocument();
|
||||
expect(onDownloadWorkspace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not display the edit or delete options if the handler is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = renderWithProviders(
|
||||
const { rerender } = render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
@@ -359,9 +309,8 @@ describe("ConversationCard", () => {
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const menu = await screen.findByTestId("context-menu");
|
||||
expect(within(menu).queryByTestId("edit-button")).toBeInTheDocument();
|
||||
expect(within(menu).queryByTestId("delete-button")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("edit-button")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("delete-button")).not.toBeInTheDocument();
|
||||
|
||||
// toggle to hide the context menu
|
||||
await user.click(ellipsisButton);
|
||||
@@ -377,19 +326,18 @@ describe("ConversationCard", () => {
|
||||
);
|
||||
|
||||
await user.click(ellipsisButton);
|
||||
const newMenu = await screen.findByTestId("context-menu");
|
||||
expect(
|
||||
within(newMenu).queryByTestId("edit-button"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(within(newMenu).queryByTestId("delete-button")).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId("edit-button")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("delete-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render the ellipsis button if there are no actions", () => {
|
||||
const { rerender } = renderWithProviders(
|
||||
const { rerender } = render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
onDownloadWorkspace={onDownloadWorkspace}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
@@ -402,6 +350,7 @@ describe("ConversationCard", () => {
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onDownloadWorkspace={onDownloadWorkspace}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
@@ -410,6 +359,18 @@ describe("ConversationCard", () => {
|
||||
|
||||
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDownloadWorkspace={onDownloadWorkspace}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("ellipsis-button")).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
@@ -424,7 +385,7 @@ describe("ConversationCard", () => {
|
||||
|
||||
describe("state indicator", () => {
|
||||
it("should render the 'STOPPED' indicator by default", () => {
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
@@ -439,7 +400,7 @@ describe("ConversationCard", () => {
|
||||
});
|
||||
|
||||
it("should render the other indicators when provided", () => {
|
||||
renderWithProviders(
|
||||
render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
isActive
|
||||
|
||||
+42
-130
@@ -1,4 +1,4 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
QueryClientProvider,
|
||||
@@ -13,7 +13,6 @@ import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
import { queryClientConfig } from "#/query-client-config";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
|
||||
describe("ConversationPanel", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
@@ -25,13 +24,14 @@ describe("ConversationPanel", () => {
|
||||
]);
|
||||
|
||||
const renderConversationPanel = (config?: QueryClientConfig) =>
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
usage: null
|
||||
}
|
||||
}
|
||||
render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient(config)}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const { endSessionMock } = vi.hoisted(() => ({
|
||||
@@ -53,38 +53,9 @@ describe("ConversationPanel", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
const mockConversations = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "Conversation 1",
|
||||
selected_repository: null,
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Conversation 2",
|
||||
selected_repository: null,
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
},
|
||||
{
|
||||
conversation_id: "3",
|
||||
title: "Conversation 3",
|
||||
selected_repository: null,
|
||||
last_updated_at: "2021-10-03T12:00:00Z",
|
||||
created_at: "2021-10-03T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
// Setup default mock for getUserConversations
|
||||
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([...mockConversations]);
|
||||
});
|
||||
|
||||
it("should render the conversations", async () => {
|
||||
@@ -112,7 +83,13 @@ describe("ConversationPanel", () => {
|
||||
new Error("Failed to fetch conversations"),
|
||||
);
|
||||
|
||||
renderConversationPanel();
|
||||
renderConversationPanel({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const error = await screen.findByText("Failed to fetch conversations");
|
||||
expect(error).toBeInTheDocument();
|
||||
@@ -147,20 +124,6 @@ describe("ConversationPanel", () => {
|
||||
|
||||
it("should call endSession after deleting a conversation that is the current session", async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockData = [...mockConversations];
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
// Wait for React Query to update its cache
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
@@ -177,60 +140,18 @@ describe("ConversationPanel", () => {
|
||||
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update with a longer timeout
|
||||
await waitFor(() => {
|
||||
const updatedCards = screen.getAllByTestId("conversation-card");
|
||||
expect(updatedCards).toHaveLength(2);
|
||||
}, { timeout: 2000 });
|
||||
// Ensure the conversation is deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(2);
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should delete a conversation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockData = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "Conversation 1",
|
||||
selected_repository: null,
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Conversation 2",
|
||||
selected_repository: null,
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
},
|
||||
{
|
||||
conversation_id: "3",
|
||||
title: "Conversation 3",
|
||||
selected_repository: null,
|
||||
last_updated_at: "2021-10-03T12:00:00Z",
|
||||
created_at: "2021-10-03T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
const deleteButton = screen.getByTestId("delete-button");
|
||||
@@ -244,11 +165,9 @@ describe("ConversationPanel", () => {
|
||||
|
||||
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update
|
||||
await waitFor(() => {
|
||||
const updatedCards = screen.getAllByTestId("conversation-card");
|
||||
expect(updatedCards).toHaveLength(2);
|
||||
});
|
||||
// Ensure the conversation is deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should rename a conversation", async () => {
|
||||
@@ -270,7 +189,7 @@ describe("ConversationPanel", () => {
|
||||
await user.tab();
|
||||
|
||||
// Ensure the conversation is renamed
|
||||
expect(updateUserConversationSpy).toHaveBeenCalledWith("1", {
|
||||
expect(updateUserConversationSpy).toHaveBeenCalledWith("3", {
|
||||
title: "Conversation 1 Renamed",
|
||||
});
|
||||
});
|
||||
@@ -295,7 +214,7 @@ describe("ConversationPanel", () => {
|
||||
// Ensure the conversation is not renamed
|
||||
expect(updateUserConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
await clickOnEditButton(user, card);
|
||||
await clickOnEditButton(user);
|
||||
|
||||
await user.type(title, "Conversation 1");
|
||||
await user.click(title);
|
||||
@@ -310,21 +229,17 @@ describe("ConversationPanel", () => {
|
||||
});
|
||||
|
||||
it("should call onClose after clicking a card", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const firstCard = cards[1];
|
||||
|
||||
await user.click(firstCard);
|
||||
await userEvent.click(firstCard);
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should refetch data on rerenders", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockResolvedValue([...mockConversations]);
|
||||
|
||||
// We need to simulate the toggling of the component to test the refetching
|
||||
function PanelWithToggle() {
|
||||
const [isOpen, setIsOpen] = React.useState(true);
|
||||
return (
|
||||
@@ -344,28 +259,25 @@ describe("ConversationPanel", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
renderWithProviders(<MyRouterStub />, {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
usage: null
|
||||
}
|
||||
}
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
render(<MyRouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient(queryClientConfig)}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const toggleButton = screen.getByText("Toggle");
|
||||
await waitFor(() => expect(getUserConversationsSpy).toHaveBeenCalledOnce());
|
||||
|
||||
// Initial render
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
const button = screen.getByText("Toggle");
|
||||
await userEvent.click(button);
|
||||
await userEvent.click(button);
|
||||
|
||||
// Toggle off
|
||||
await user.click(toggleButton);
|
||||
expect(screen.queryByTestId("conversation-card")).not.toBeInTheDocument();
|
||||
|
||||
// Toggle on
|
||||
await user.click(toggleButton);
|
||||
const newCards = await screen.findAllByTestId("conversation-card");
|
||||
expect(newCards).toHaveLength(3);
|
||||
await waitFor(() =>
|
||||
expect(getUserConversationsSpy).toHaveBeenCalledTimes(2),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,10 +30,6 @@ describe("GitHubRepositorySelector", () => {
|
||||
APP_SLUG: "openhands",
|
||||
GITHUB_CLIENT_ID: "test-client-id",
|
||||
POSTHOG_CLIENT_KEY: "test-posthog-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
|
||||
@@ -4,8 +4,10 @@ import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
import * as featureFlags from "#/utils/feature-flags";
|
||||
|
||||
describe("PaymentForm", () => {
|
||||
const billingSettingsSpy = vi.spyOn(featureFlags, "BILLING_SETTINGS");
|
||||
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
|
||||
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
@@ -20,16 +22,13 @@ describe("PaymentForm", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// useBalance hook will return the balance only if the APP_MODE is "saas" and the billing feature is enabled
|
||||
// useBalance hook will return the balance only if the APP_MODE is "saas"
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
billingSettingsSpy.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("TrajectoryActions", () => {
|
||||
expect(onNegativeFeedback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onExportTrajectory when export button is clicked", async () => {
|
||||
it("should call onExportTrajectory when negative feedback is clicked", async () => {
|
||||
renderWithProviders(
|
||||
<TrajectoryActions
|
||||
onPositiveFeedback={onPositiveFeedback}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import * as ChatSlice from "#/state/chat-slice";
|
||||
import {
|
||||
updateStatusWhenErrorMessagePresent,
|
||||
WsClientProvider,
|
||||
useWsClient,
|
||||
} from "#/context/ws-client-provider";
|
||||
import React from "react";
|
||||
|
||||
describe("Propagate error message", () => {
|
||||
it("should do nothing when no message was passed from server", () => {
|
||||
@@ -44,59 +41,3 @@ describe("Propagate error message", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Create a mock for socket.io-client
|
||||
const mockEmit = vi.fn();
|
||||
const mockOn = vi.fn();
|
||||
const mockOff = vi.fn();
|
||||
const mockDisconnect = vi.fn();
|
||||
|
||||
vi.mock("socket.io-client", () => {
|
||||
return {
|
||||
io: vi.fn(() => ({
|
||||
emit: mockEmit,
|
||||
on: mockOn,
|
||||
off: mockOff,
|
||||
disconnect: mockDisconnect,
|
||||
io: {
|
||||
opts: {
|
||||
query: {},
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock component to test the hook
|
||||
const TestComponent = () => {
|
||||
const { send } = useWsClient();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Send a test event
|
||||
send({ type: "test_event" });
|
||||
}, [send]);
|
||||
|
||||
return <div>Test Component</div>;
|
||||
};
|
||||
|
||||
describe("WsClientProvider", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should emit oh_user_action event when send is called", async () => {
|
||||
const { getByText } = render(
|
||||
<WsClientProvider conversationId="test-conversation-id">
|
||||
<TestComponent />
|
||||
</WsClientProvider>
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(getByText("Test Component")).toBeInTheDocument();
|
||||
|
||||
// Wait for the emit call to happen (useEffect needs time to run)
|
||||
await waitFor(() => {
|
||||
expect(mockEmit).toHaveBeenCalledWith("oh_user_action", { type: "test_event" });
|
||||
}, { timeout: 1000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,18 +3,15 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("useSaveSettings", () => {
|
||||
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const { result } = renderHook(() => useSaveSettings(), {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -55,8 +55,7 @@ describe("frontend/routes/_oh", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: This test fails when it shouldn't be, please investigate
|
||||
it.skip("should render and capture the user's consent if oss mode", async () => {
|
||||
it("should render and capture the user's consent if oss mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
@@ -69,10 +68,6 @@ describe("frontend/routes/_oh", () => {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
POSTHOG_CLIENT_KEY: "test-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error - We only care about the user_consents_to_analytics field
|
||||
@@ -104,10 +99,6 @@ describe("frontend/routes/_oh", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
POSTHOG_CLIENT_KEY: "test-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
@@ -8,6 +8,7 @@ import MainApp from "#/routes/_oh/route";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import Home from "#/routes/_oh._index/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as FeatureFlags from "#/utils/feature-flags";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
@@ -51,8 +52,6 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("Home Screen", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
it("should render the home screen", () => {
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
});
|
||||
@@ -69,14 +68,6 @@ describe("Home Screen", () => {
|
||||
});
|
||||
|
||||
it("should navigate to the settings when pressing 'Connect to GitHub' if the user isn't authenticated", async () => {
|
||||
// @ts-expect-error - we only need APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
@@ -128,14 +119,10 @@ describe("Settings 404", () => {
|
||||
});
|
||||
|
||||
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
|
||||
// TODO: Remove HIDE_LLM_SETTINGS check once released
|
||||
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
|
||||
// @ts-expect-error - we only need APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
getConfigSpy.mockResolvedValue({ APP_MODE: "saas" });
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
@@ -159,19 +146,14 @@ describe("Setup Payment modal", () => {
|
||||
// @ts-expect-error - we only need the APP_MODE for this test
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const setupPaymentModal = await screen.findByTestId(
|
||||
"proceed-to-stripe-button",
|
||||
);
|
||||
const setupPaymentModal = await screen.findByTestId("proceed-to-stripe-button");
|
||||
expect(setupPaymentModal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,11 @@ import { renderWithProviders } from "test-utils";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
import * as FeatureFlags from "#/utils/feature-flags";
|
||||
|
||||
describe("Settings Billing", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);
|
||||
|
||||
const RoutesStub = createRoutesStub([
|
||||
{
|
||||
@@ -35,10 +37,6 @@ describe("Settings Billing", () => {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -54,10 +52,6 @@ describe("Settings Billing", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -75,10 +69,6 @@ describe("Settings Billing", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: true,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
test,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent, { UserEvent } from "@testing-library/user-event";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
@@ -11,6 +20,7 @@ import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { PostApiSettings } from "#/types/settings";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
import AccountSettings from "#/routes/account-settings";
|
||||
import * as FeatureFlags from "#/utils/feature-flags";
|
||||
|
||||
const toggleAdvancedSettings = async (user: UserEvent) => {
|
||||
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
|
||||
@@ -29,6 +39,11 @@ describe("Settings Screen", () => {
|
||||
useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
// TODO: Remove this once we release
|
||||
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -72,10 +87,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,7 +123,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should set '<hidden>' placeholder if the GitHub token is set", async () => {
|
||||
it("should set asterik placeholder if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
@@ -122,7 +133,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("github-token-input");
|
||||
expect(input).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(input).toHaveProperty("placeholder", "**********");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -195,10 +206,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -213,10 +220,6 @@ describe("Settings Screen", () => {
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
APP_SLUG: "test-app",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -228,10 +231,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -309,10 +308,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -410,7 +405,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should set '<hidden>' placeholder if the LLM API key is set", async () => {
|
||||
it("should set asterik placeholder if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: "**********",
|
||||
@@ -420,7 +415,7 @@ describe("Settings Screen", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("llm-api-key-input");
|
||||
expect(input).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(input).toHaveProperty("placeholder", "**********");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -454,10 +449,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -472,10 +463,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -487,10 +474,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
@@ -509,10 +492,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
@@ -527,10 +506,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
@@ -1007,10 +982,6 @@ describe("Settings Screen", () => {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("Actions Service", () => {
|
||||
// Mock implementation to capture the message
|
||||
let capturedPartialMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **completed partially**")) {
|
||||
capturedPartialMessage = action.payload;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ describe("Actions Service", () => {
|
||||
|
||||
handleActionMessage(messagePartial);
|
||||
expect(capturedPartialMessage).toContain("I believe that the task was **completed partially**");
|
||||
|
||||
|
||||
// Test not completed
|
||||
const messageNotCompleted: ActionMessage = {
|
||||
id: 2,
|
||||
@@ -106,7 +106,7 @@ describe("Actions Service", () => {
|
||||
// Mock implementation to capture the message
|
||||
let capturedNotCompletedMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **not completed**")) {
|
||||
capturedNotCompletedMessage = action.payload;
|
||||
}
|
||||
@@ -114,7 +114,7 @@ describe("Actions Service", () => {
|
||||
|
||||
handleActionMessage(messageNotCompleted);
|
||||
expect(capturedNotCompletedMessage).toContain("I believe that the task was **not completed**");
|
||||
|
||||
|
||||
// Test completed successfully
|
||||
const messageCompleted: ActionMessage = {
|
||||
id: 3,
|
||||
@@ -133,7 +133,7 @@ describe("Actions Service", () => {
|
||||
// Mock implementation to capture the message
|
||||
let capturedCompletedMessage = "";
|
||||
(store.dispatch as any).mockImplementation((action: any) => {
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
if (action.type === "chat/addAssistantMessage" &&
|
||||
action.payload.includes("believe that the task was **completed successfully**")) {
|
||||
capturedCompletedMessage = action.payload;
|
||||
}
|
||||
|
||||
@@ -9,67 +9,67 @@ describe("formatTimeDelta", () => {
|
||||
|
||||
it("formats the yearly time correctly", () => {
|
||||
const oneYearAgo = new Date("2023-01-01T00:00:00Z");
|
||||
expect(formatTimeDelta(oneYearAgo)).toBe("1y");
|
||||
expect(formatTimeDelta(oneYearAgo)).toBe("1 year");
|
||||
|
||||
const twoYearsAgo = new Date("2022-01-01T00:00:00Z");
|
||||
expect(formatTimeDelta(twoYearsAgo)).toBe("2y");
|
||||
expect(formatTimeDelta(twoYearsAgo)).toBe("2 years");
|
||||
|
||||
const threeYearsAgo = new Date("2021-01-01T00:00:00Z");
|
||||
expect(formatTimeDelta(threeYearsAgo)).toBe("3y");
|
||||
expect(formatTimeDelta(threeYearsAgo)).toBe("3 years");
|
||||
});
|
||||
|
||||
it("formats the monthly time correctly", () => {
|
||||
const oneMonthAgo = new Date("2023-12-01T00:00:00Z");
|
||||
expect(formatTimeDelta(oneMonthAgo)).toBe("1mo");
|
||||
expect(formatTimeDelta(oneMonthAgo)).toBe("1 month");
|
||||
|
||||
const twoMonthsAgo = new Date("2023-11-01T00:00:00Z");
|
||||
expect(formatTimeDelta(twoMonthsAgo)).toBe("2mo");
|
||||
expect(formatTimeDelta(twoMonthsAgo)).toBe("2 months");
|
||||
|
||||
const threeMonthsAgo = new Date("2023-10-01T00:00:00Z");
|
||||
expect(formatTimeDelta(threeMonthsAgo)).toBe("3mo");
|
||||
expect(formatTimeDelta(threeMonthsAgo)).toBe("3 months");
|
||||
});
|
||||
|
||||
it("formats the daily time correctly", () => {
|
||||
const oneDayAgo = new Date("2023-12-31T00:00:00Z");
|
||||
expect(formatTimeDelta(oneDayAgo)).toBe("1d");
|
||||
expect(formatTimeDelta(oneDayAgo)).toBe("1 day");
|
||||
|
||||
const twoDaysAgo = new Date("2023-12-30T00:00:00Z");
|
||||
expect(formatTimeDelta(twoDaysAgo)).toBe("2d");
|
||||
expect(formatTimeDelta(twoDaysAgo)).toBe("2 days");
|
||||
|
||||
const threeDaysAgo = new Date("2023-12-29T00:00:00Z");
|
||||
expect(formatTimeDelta(threeDaysAgo)).toBe("3d");
|
||||
expect(formatTimeDelta(threeDaysAgo)).toBe("3 days");
|
||||
});
|
||||
|
||||
it("formats the hourly time correctly", () => {
|
||||
const oneHourAgo = new Date("2023-12-31T23:00:00Z");
|
||||
expect(formatTimeDelta(oneHourAgo)).toBe("1h");
|
||||
expect(formatTimeDelta(oneHourAgo)).toBe("1 hour");
|
||||
|
||||
const twoHoursAgo = new Date("2023-12-31T22:00:00Z");
|
||||
expect(formatTimeDelta(twoHoursAgo)).toBe("2h");
|
||||
expect(formatTimeDelta(twoHoursAgo)).toBe("2 hours");
|
||||
|
||||
const threeHoursAgo = new Date("2023-12-31T21:00:00Z");
|
||||
expect(formatTimeDelta(threeHoursAgo)).toBe("3h");
|
||||
expect(formatTimeDelta(threeHoursAgo)).toBe("3 hours");
|
||||
});
|
||||
|
||||
it("formats the minute time correctly", () => {
|
||||
const oneMinuteAgo = new Date("2023-12-31T23:59:00Z");
|
||||
expect(formatTimeDelta(oneMinuteAgo)).toBe("1m");
|
||||
expect(formatTimeDelta(oneMinuteAgo)).toBe("1 minute");
|
||||
|
||||
const twoMinutesAgo = new Date("2023-12-31T23:58:00Z");
|
||||
expect(formatTimeDelta(twoMinutesAgo)).toBe("2m");
|
||||
expect(formatTimeDelta(twoMinutesAgo)).toBe("2 minutes");
|
||||
|
||||
const threeMinutesAgo = new Date("2023-12-31T23:57:00Z");
|
||||
expect(formatTimeDelta(threeMinutesAgo)).toBe("3m");
|
||||
expect(formatTimeDelta(threeMinutesAgo)).toBe("3 minutes");
|
||||
});
|
||||
|
||||
it("formats the second time correctly", () => {
|
||||
const oneSecondAgo = new Date("2023-12-31T23:59:59Z");
|
||||
expect(formatTimeDelta(oneSecondAgo)).toBe("1s");
|
||||
expect(formatTimeDelta(oneSecondAgo)).toBe("1 second");
|
||||
|
||||
const twoSecondsAgo = new Date("2023-12-31T23:59:58Z");
|
||||
expect(formatTimeDelta(twoSecondsAgo)).toBe("2s");
|
||||
expect(formatTimeDelta(twoSecondsAgo)).toBe("2 seconds");
|
||||
|
||||
const threeSecondsAgo = new Date("2023-12-31T23:59:57Z");
|
||||
expect(formatTimeDelta(threeSecondsAgo)).toBe("3s");
|
||||
expect(formatTimeDelta(threeSecondsAgo)).toBe("3 seconds");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { vi } from "vitest";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const setupTestConfig = () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
POSTHOG_CLIENT_KEY: "test-key",
|
||||
});
|
||||
};
|
||||
|
||||
export const setupSaasTestConfig = () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
POSTHOG_CLIENT_KEY: "test-key",
|
||||
});
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.29.1",
|
||||
"version": "0.28.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.29.1",
|
||||
"version": "0.28.1",
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.4",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.29.1",
|
||||
"version": "0.28.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const openHands = axios.create({
|
||||
baseURL: `${window.location.protocol}//${import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host}`,
|
||||
});
|
||||
export const openHands = axios.create();
|
||||
|
||||
export const setAuthTokenHeader = (token: string) => {
|
||||
openHands.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
};
|
||||
|
||||
@@ -49,10 +49,6 @@ export interface GetConfigResponse {
|
||||
GITHUB_CLIENT_ID: string;
|
||||
POSTHOG_CLIENT_KEY: string;
|
||||
STRIPE_PUBLISHABLE_KEY?: string;
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: boolean;
|
||||
HIDE_LLM_SETTINGS: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetVSCodeUrlResponse {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { useCurrentSettings } from "#/context/settings-context";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
|
||||
@@ -15,14 +15,14 @@ interface AnalyticsConsentFormModalProps {
|
||||
export function AnalyticsConsentFormModal({
|
||||
onClose,
|
||||
}: AnalyticsConsentFormModalProps) {
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
const { saveUserSettings } = useCurrentSettings();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const analytics = formData.get("analytics") === "on";
|
||||
|
||||
saveUserSettings(
|
||||
await saveUserSettings(
|
||||
{ user_consents_to_analytics: analytics },
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import posthog from "posthog-js";
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
|
||||
import { DownloadModal } from "#/components/shared/download-modal";
|
||||
import type { RootState } from "#/store";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
@@ -17,11 +18,21 @@ export function ActionSuggestions({
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
|
||||
const [isDownloading, setIsDownloading] = React.useState(false);
|
||||
const [hasPullRequest, setHasPullRequest] = React.useState(false);
|
||||
|
||||
const handleDownloadClose = () => {
|
||||
setIsDownloading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 mb-2">
|
||||
{githubTokenIsSet && selectedRepository && (
|
||||
<DownloadModal
|
||||
initialPath=""
|
||||
onClose={handleDownloadClose}
|
||||
isOpen={isDownloading}
|
||||
/>
|
||||
{githubTokenIsSet && selectedRepository ? (
|
||||
<div className="flex flex-row gap-2 justify-center w-full">
|
||||
{!hasPullRequest ? (
|
||||
<>
|
||||
@@ -29,7 +40,7 @@ export function ActionSuggestions({
|
||||
suggestion={{
|
||||
label: "Push to Branch",
|
||||
value:
|
||||
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.",
|
||||
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("push_to_branch_button_clicked");
|
||||
@@ -40,7 +51,7 @@ export function ActionSuggestions({
|
||||
suggestion={{
|
||||
label: "Push & Create PR",
|
||||
value:
|
||||
"Please push the changes to GitHub and open a pull request. Please use the exact SAME branch name as the one you are currently on.",
|
||||
"Please push the changes to GitHub and open a pull request.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
@@ -63,6 +74,21 @@ export function ActionSuggestions({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<SuggestionItem
|
||||
suggestion={{
|
||||
label: !isDownloading
|
||||
? "Download files"
|
||||
: "Downloading, please wait...",
|
||||
value: "Download files",
|
||||
}}
|
||||
onClick={() => {
|
||||
posthog.capture("download_workspace_button_clicked");
|
||||
if (!isDownloading) {
|
||||
setIsDownloading(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,15 +17,19 @@ import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { Messages } from "./messages";
|
||||
import { ChatSuggestions } from "./chat-suggestions";
|
||||
import { ActionSuggestions } from "./action-suggestions";
|
||||
|
||||
import { ContinueButton } from "#/components/shared/buttons/continue-button";
|
||||
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-files";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
function getEntryPoint(hasRepository: boolean | null): string {
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
hasImportedProjectZip: boolean | null,
|
||||
): string {
|
||||
if (hasRepository) return "github";
|
||||
if (hasImportedProjectZip) return "zip";
|
||||
return "direct";
|
||||
}
|
||||
|
||||
@@ -44,7 +48,7 @@ export function ChatInterface() {
|
||||
>("positive");
|
||||
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
|
||||
const [messageToSend, setMessageToSend] = React.useState<string | null>(null);
|
||||
const { selectedRepository } = useSelector(
|
||||
const { selectedRepository, importedProjectZip } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
const params = useParams();
|
||||
@@ -53,8 +57,12 @@ export function ChatInterface() {
|
||||
const handleSendMessage = async (content: string, files: File[]) => {
|
||||
if (messages.length === 0) {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
entry_point: getEntryPoint(selectedRepository !== null),
|
||||
entry_point: getEntryPoint(
|
||||
selectedRepository !== null,
|
||||
importedProjectZip !== null,
|
||||
),
|
||||
query_character_length: content.length,
|
||||
uploaded_zip_size: importedProjectZip?.length,
|
||||
});
|
||||
} else {
|
||||
posthog.capture("user_message_sent", {
|
||||
@@ -77,6 +85,10 @@ export function ChatInterface() {
|
||||
send(generateAgentStateChangeEvent(AgentState.STOPPED));
|
||||
};
|
||||
|
||||
const handleSendContinueMsg = () => {
|
||||
handleSendMessage("Continue", []);
|
||||
};
|
||||
|
||||
const onClickShareFeedbackActionButton = async (
|
||||
polarity: "positive" | "negative",
|
||||
) => {
|
||||
@@ -153,6 +165,10 @@ export function ChatInterface() {
|
||||
/>
|
||||
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0">
|
||||
{messages.length > 2 &&
|
||||
curAgentState === AgentState.AWAITING_USER_INPUT && (
|
||||
<ContinueButton onClick={handleSendContinueMsg} />
|
||||
)}
|
||||
{curAgentState === AgentState.RUNNING && <TypingIndicator />}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import CheckCircle from "#/icons/check-circle-solid.svg?react";
|
||||
import XCircle from "#/icons/x-circle-solid.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { BILLING_SETTINGS } from "#/utils/feature-flags";
|
||||
|
||||
interface ExpandableMessageProps {
|
||||
id?: string;
|
||||
@@ -42,15 +43,12 @@ export function ExpandableMessage({
|
||||
const statusIconClasses = "h-4 w-4 ml-2 inline";
|
||||
|
||||
if (
|
||||
config?.FEATURE_FLAGS.ENABLE_BILLING &&
|
||||
BILLING_SETTINGS() &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
id === "STATUS$ERROR_LLM_OUT_OF_CREDITS"
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
data-testid="out-of-credits"
|
||||
className="flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 border-danger"
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 border-danger">
|
||||
<div className="text-sm w-full">
|
||||
<div className="font-bold text-danger">
|
||||
{t("STATUS$ERROR_LLM_OUT_OF_CREDITS")}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ContextMenuListItem({
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"text-sm px-4 py-2 w-full text-start hover:bg-white/10 first-of-type:rounded-t-md last-of-type:rounded-b-md",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent text-nowrap",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ContextMenu({
|
||||
<ul
|
||||
data-testid={testId}
|
||||
ref={ref}
|
||||
className={cn("bg-tertiary rounded-md", className)}
|
||||
className={cn("bg-tertiary rounded-md w-[140px]", className)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useParams } from "react-router";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { AgentControlBar } from "./agent-control-bar";
|
||||
import { AgentStatusBar } from "./agent-status-bar";
|
||||
import { SecurityLock } from "./security-lock";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { ConversationCard } from "../conversation-panel/conversation-card";
|
||||
import { useAutoTitle } from "#/hooks/use-auto-title";
|
||||
import { DownloadModal } from "#/components/shared/download-modal";
|
||||
|
||||
interface ControlsProps {
|
||||
setSecurityOpen: (isOpen: boolean) => void;
|
||||
@@ -17,7 +18,13 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
|
||||
const { data: conversation } = useUserConversation(
|
||||
params.conversationId ?? null,
|
||||
);
|
||||
useAutoTitle();
|
||||
|
||||
const [downloading, setDownloading] = React.useState(false);
|
||||
|
||||
const handleDownloadWorkspace = () => {
|
||||
posthog.capture("download_workspace_button_clicked");
|
||||
setDownloading(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -32,12 +39,17 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
|
||||
|
||||
<ConversationCard
|
||||
variant="compact"
|
||||
showDisplayCostOption
|
||||
onDownloadWorkspace={handleDownloadWorkspace}
|
||||
title={conversation?.title ?? ""}
|
||||
lastUpdatedAt={conversation?.created_at ?? ""}
|
||||
selectedRepository={conversation?.selected_repository ?? null}
|
||||
status={conversation?.status}
|
||||
conversationId={conversation?.conversation_id}
|
||||
/>
|
||||
|
||||
<DownloadModal
|
||||
initialPath=""
|
||||
onClose={() => setDownloading(false)}
|
||||
isOpen={downloading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-18
@@ -7,8 +7,7 @@ interface ConversationCardContextMenuProps {
|
||||
onClose: () => void;
|
||||
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownloadViaVSCode?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDownload?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
|
||||
@@ -16,8 +15,7 @@ export function ConversationCardContextMenu({
|
||||
onClose,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onDisplayCost,
|
||||
onDownloadViaVSCode,
|
||||
onDownload,
|
||||
position = "bottom",
|
||||
}: ConversationCardContextMenuProps) {
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
@@ -42,20 +40,9 @@ export function ConversationCardContextMenu({
|
||||
Edit Title
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onDownloadViaVSCode && (
|
||||
<ContextMenuListItem
|
||||
testId="download-vscode-button"
|
||||
onClick={onDownloadViaVSCode}
|
||||
>
|
||||
Download via VS Code
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onDisplayCost && (
|
||||
<ContextMenuListItem
|
||||
testId="display-cost-button"
|
||||
onClick={onDisplayCost}
|
||||
>
|
||||
Display Cost
|
||||
{onDownload && (
|
||||
<ContextMenuListItem testId="download-button" onClick={onDownload}>
|
||||
Download Workspace
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
</ContextMenu>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import {
|
||||
@@ -10,50 +8,36 @@ import {
|
||||
import { EllipsisButton } from "./ellipsis-button";
|
||||
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
onDelete?: () => void;
|
||||
onChangeTitle?: (title: string) => void;
|
||||
showDisplayCostOption?: boolean;
|
||||
onDownloadWorkspace?: () => void;
|
||||
isActive?: boolean;
|
||||
title: string;
|
||||
selectedRepository: string | null;
|
||||
lastUpdatedAt: string; // ISO 8601
|
||||
createdAt?: string; // ISO 8601
|
||||
status?: ProjectStatus;
|
||||
variant?: "compact" | "default";
|
||||
conversationId?: string; // Optional conversation ID for VS Code URL
|
||||
}
|
||||
|
||||
const MAX_TIME_BETWEEN_CREATION_AND_UPDATE = 1000 * 60 * 30; // 30 minutes
|
||||
|
||||
export function ConversationCard({
|
||||
onClick,
|
||||
onDelete,
|
||||
onChangeTitle,
|
||||
showDisplayCostOption,
|
||||
onDownloadWorkspace,
|
||||
isActive,
|
||||
title,
|
||||
selectedRepository,
|
||||
// lastUpdatedAt is kept in props for backward compatibility
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
lastUpdatedAt,
|
||||
createdAt,
|
||||
status = "STOPPED",
|
||||
variant = "default",
|
||||
conversationId,
|
||||
}: ConversationCardProps) {
|
||||
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// Subscribe to metrics data from Redux store
|
||||
const metrics = useSelector((state: RootState) => state.metrics);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputRef.current?.value) {
|
||||
const trimmed = inputRef.current.value.trim();
|
||||
@@ -94,36 +78,9 @@ export function ConversationCard({
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
const handleDownloadViaVSCode = async (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
const handleDownload = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
posthog.capture("download_via_vscode_button_clicked");
|
||||
|
||||
// Fetch the VS Code URL from the API
|
||||
if (conversationId) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/conversations/${conversationId}/vscode-url`,
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.vscode_url) {
|
||||
window.open(data.vscode_url, "_blank");
|
||||
}
|
||||
// VS Code URL not available
|
||||
} catch (error) {
|
||||
// Failed to fetch VS Code URL
|
||||
}
|
||||
}
|
||||
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
const handleDisplayCost = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setMetricsModalVisible(true);
|
||||
onDownloadWorkspace?.();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -132,129 +89,81 @@ export function ConversationCard({
|
||||
}
|
||||
}, [titleMode]);
|
||||
|
||||
const hasContextMenu = !!(onDelete || onChangeTitle || showDisplayCostOption);
|
||||
const timeBetweenUpdateAndCreation = createdAt
|
||||
? new Date(lastUpdatedAt).getTime() - new Date(createdAt).getTime()
|
||||
: 0;
|
||||
const showUpdateTime =
|
||||
createdAt &&
|
||||
timeBetweenUpdateAndCreation > MAX_TIME_BETWEEN_CREATION_AND_UPDATE;
|
||||
const hasContextMenu = !!(onDelete || onChangeTitle || onDownloadWorkspace);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="conversation-card"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer",
|
||||
variant === "compact" &&
|
||||
"h-auto w-fit rounded-xl border border-[#525252]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0 overflow-hidden mr-2">
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
|
||||
)}
|
||||
{titleMode === "edit" && (
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-testid="conversation-card-title"
|
||||
onClick={handleInputClick}
|
||||
onBlur={handleBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
type="text"
|
||||
defaultValue={title}
|
||||
className="text-sm leading-6 font-semibold bg-transparent w-full"
|
||||
/>
|
||||
)}
|
||||
{titleMode === "view" && (
|
||||
<p
|
||||
data-testid="conversation-card-title"
|
||||
className="text-sm leading-6 font-semibold bg-transparent truncate overflow-hidden"
|
||||
title={title}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<ConversationStateIndicator status={status} />
|
||||
{hasContextMenu && (
|
||||
<EllipsisButton
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenuVisible((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{contextMenuVisible && (
|
||||
<ConversationCardContextMenu
|
||||
onClose={() => setContextMenuVisible(false)}
|
||||
onDelete={onDelete && handleDelete}
|
||||
onEdit={onChangeTitle && handleEdit}
|
||||
onDownloadViaVSCode={
|
||||
conversationId ? handleDownloadViaVSCode : undefined
|
||||
}
|
||||
onDisplayCost={
|
||||
showDisplayCostOption ? handleDisplayCost : undefined
|
||||
}
|
||||
position={variant === "compact" ? "top" : "bottom"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
data-testid="conversation-card"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer",
|
||||
variant === "compact" &&
|
||||
"h-auto w-fit rounded-xl border border-[#525252]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0 overflow-hidden mr-2">
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
|
||||
)}
|
||||
{titleMode === "edit" && (
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-testid="conversation-card-title"
|
||||
onClick={handleInputClick}
|
||||
onBlur={handleBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
type="text"
|
||||
defaultValue={title}
|
||||
className="text-sm leading-6 font-semibold bg-transparent w-full"
|
||||
/>
|
||||
)}
|
||||
{titleMode === "view" && (
|
||||
<p
|
||||
data-testid="conversation-card-title"
|
||||
className="text-sm leading-6 font-semibold bg-transparent truncate overflow-hidden"
|
||||
title={title}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
variant === "compact" && "flex items-center justify-between mt-1",
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<ConversationStateIndicator status={status} />
|
||||
{hasContextMenu && (
|
||||
<EllipsisButton
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenuVisible((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{selectedRepository && (
|
||||
<ConversationRepoLink selectedRepository={selectedRepository} />
|
||||
{contextMenuVisible && (
|
||||
<ConversationCardContextMenu
|
||||
onClose={() => setContextMenuVisible(false)}
|
||||
onDelete={onDelete && handleDelete}
|
||||
onEdit={onChangeTitle && handleEdit}
|
||||
onDownload={onDownloadWorkspace && handleDownload}
|
||||
position={variant === "compact" ? "top" : "bottom"}
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-neutral-400">
|
||||
<span>Created </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))} ago
|
||||
</time>
|
||||
{showUpdateTime && (
|
||||
<>
|
||||
<span>, updated </span>
|
||||
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseModal
|
||||
isOpen={metricsModalVisible}
|
||||
onOpenChange={setMetricsModalVisible}
|
||||
title="Metrics Information"
|
||||
testID="metrics-modal"
|
||||
<div
|
||||
className={cn(
|
||||
variant === "compact" && "flex items-center justify-between mt-1",
|
||||
)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{metrics?.cost !== null && (
|
||||
<p>Total Cost: ${metrics.cost.toFixed(4)}</p>
|
||||
)}
|
||||
{metrics?.usage !== null && (
|
||||
<>
|
||||
<p>Tokens Used:</p>
|
||||
<ul className="list-inside space-y-1 ml-2">
|
||||
<li>- Input: {metrics.usage.prompt_tokens}</li>
|
||||
<li>- Output: {metrics.usage.completion_tokens}</li>
|
||||
<li>- Total: {metrics.usage.total_tokens}</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{!metrics?.cost && !metrics?.usage && (
|
||||
<p className="text-neutral-400">No metrics data available</p>
|
||||
)}
|
||||
</div>
|
||||
</BaseModal>
|
||||
</>
|
||||
{selectedRepository && (
|
||||
<ConversationRepoLink selectedRepository={selectedRepository} />
|
||||
)}
|
||||
<p className="text-xs text-neutral-400">
|
||||
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,9 +108,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
title={project.title}
|
||||
selectedRepository={project.selected_repository}
|
||||
lastUpdatedAt={project.last_updated_at}
|
||||
createdAt={project.created_at}
|
||||
status={project.status}
|
||||
conversationId={project.conversation_id}
|
||||
/>
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user