mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a820e45ec4 |
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -12,6 +12,3 @@ assignees: ''
|
||||
**Describe the UX or technical implementation you have in mind**
|
||||
|
||||
**Additional context**
|
||||
|
||||
|
||||
### If you find this feature request or enhancement useful, make sure to add a 👍 to the issue
|
||||
|
||||
71
.github/workflows/ghcr-build.yml
vendored
71
.github/workflows/ghcr-build.yml
vendored
@@ -145,7 +145,7 @@ jobs:
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
|
||||
run: make install-python-dependencies
|
||||
- name: Create source distribution and Dockerfile
|
||||
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
|
||||
- name: Lowercase Repository Owner
|
||||
@@ -174,19 +174,20 @@ jobs:
|
||||
build-args: ${{ env.DOCKER_BUILD_ARGS }}
|
||||
context: containers/runtime
|
||||
provenance: false
|
||||
# Forked repos can't push to GHCR, so we just build in order to populate the cache for rebuilding
|
||||
# Forked repos can't push to GHCR, so we need to upload the image as an artifact
|
||||
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: useblacksmith/build-push-action@v1
|
||||
with:
|
||||
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
context: containers/runtime
|
||||
- name: Upload runtime source for fork
|
||||
- name: Upload runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: runtime-src-${{ matrix.base_image.tag }}
|
||||
path: containers/runtime
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
path: /tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
|
||||
verify_hash_equivalence_in_runtime_and_app:
|
||||
name: Verify Hash Equivalence in Runtime and Docker images
|
||||
@@ -219,7 +220,7 @@ jobs:
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
|
||||
run: make install-python-dependencies
|
||||
- name: Get hash in App Image
|
||||
run: |
|
||||
echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
|
||||
@@ -247,7 +248,7 @@ jobs:
|
||||
test_runtime_root:
|
||||
name: RT Unit Tests (Root)
|
||||
needs: [ghcr_build_runtime, define-matrix]
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -257,23 +258,17 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Download runtime source for fork
|
||||
# Forked repos can't push to GHCR, so we need to download the image as an artifact
|
||||
- name: Download runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-src-${{ matrix.base_image.tag }}
|
||||
path: containers/runtime
|
||||
- name: Lowercase Repository Owner
|
||||
run: |
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
# Forked repos can't push to GHCR, so we need to rebuild using cache
|
||||
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: useblacksmith/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
context: containers/runtime
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
@@ -291,7 +286,10 @@ jobs:
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
|
||||
run: make install-python-dependencies
|
||||
- name: Lowercase Repository Owner
|
||||
run: |
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Run docker runtime tests
|
||||
run: |
|
||||
# We install pytest-xdist in order to run tests across CPUs
|
||||
@@ -310,7 +308,7 @@ jobs:
|
||||
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
RUN_AS_OPENHANDS=false \
|
||||
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
||||
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
@@ -319,7 +317,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-8vcpu-ubuntu-2204
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
needs: [ghcr_build_runtime, define-matrix]
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -329,23 +327,17 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Download runtime source for fork
|
||||
# Forked repos can't push to GHCR, so we need to download the image as an artifact
|
||||
- name: Download runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: runtime-src-${{ matrix.base_image.tag }}
|
||||
path: containers/runtime
|
||||
- name: Lowercase Repository Owner
|
||||
run: |
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
# Forked repos can't push to GHCR, so we need to rebuild using cache
|
||||
- name: Build runtime image ${{ matrix.base_image.image }} for fork
|
||||
name: runtime-${{ matrix.base_image.tag }}
|
||||
path: /tmp
|
||||
- name: Load runtime image for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: useblacksmith/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
context: containers/runtime
|
||||
run: |
|
||||
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
|
||||
- name: Cache Poetry dependencies
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
@@ -363,7 +355,10 @@ jobs:
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
|
||||
run: make install-python-dependencies
|
||||
- name: Lowercase Repository Owner
|
||||
run: |
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Run runtime tests
|
||||
run: |
|
||||
# We install pytest-xdist in order to run tests across CPUs
|
||||
@@ -379,7 +374,7 @@ jobs:
|
||||
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
RUN_AS_OPENHANDS=true \
|
||||
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
||||
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
|
||||
9
.github/workflows/openhands-resolver.yml
vendored
9
.github/workflows/openhands-resolver.yml
vendored
@@ -16,11 +16,6 @@ on:
|
||||
type: string
|
||||
default: "main"
|
||||
description: "Target branch to pull and create PR against"
|
||||
pr_type:
|
||||
required: false
|
||||
type: string
|
||||
default: "draft"
|
||||
description: "The PR type that is going to be created (draft, ready)"
|
||||
LLM_MODEL:
|
||||
required: false
|
||||
type: string
|
||||
@@ -285,9 +280,9 @@ jobs:
|
||||
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||
--target-branch ${{ env.TARGET_BRANCH }} \
|
||||
--pr-type ${{ inputs.pr_type || 'draft' }} \
|
||||
--pr-type draft \
|
||||
--reviewer ${{ github.actor }} | tee pr_result.txt && \
|
||||
grep "PR created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
|
||||
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
|
||||
else
|
||||
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||
|
||||
@@ -54,20 +54,3 @@ Frontend:
|
||||
## Template for Github Pull Request
|
||||
|
||||
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
These details may or may not be useful for your current task.
|
||||
|
||||
### Frontend
|
||||
|
||||
#### Action Handling:
|
||||
- Actions are defined in `frontend/src/types/action-type.ts`
|
||||
- The `HANDLED_ACTIONS` array in `frontend/src/state/chat-slice.ts` determines which actions are displayed as collapsible UI elements
|
||||
- To add a new action type to the UI:
|
||||
1. Add the action type to the `HANDLED_ACTIONS` array
|
||||
2. Implement the action handling in `addAssistantAction` function in chat-slice.ts
|
||||
3. Add a translation key in the format `ACTION_MESSAGE$ACTION_NAME` to the i18n files
|
||||
- Actions with `thought` property are displayed in the UI based on their action type:
|
||||
- Regular actions (like "run", "edit") display the thought as a separate message
|
||||
- Special actions (like "think") are displayed as collapsible elements only
|
||||
|
||||
@@ -118,7 +118,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.33-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.32-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
31
Makefile
31
Makefile
@@ -133,29 +133,20 @@ install-python-dependencies:
|
||||
export HNSWLIB_NO_NATIVE=1; \
|
||||
poetry run pip install chroma-hnswlib; \
|
||||
fi
|
||||
@if [ -n "${POETRY_GROUP}" ]; then \
|
||||
echo "Installing only POETRY_GROUP=${POETRY_GROUP}"; \
|
||||
poetry install --only $${POETRY_GROUP}; \
|
||||
@poetry install
|
||||
@if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
|
||||
poetry run pip install playwright; \
|
||||
poetry run playwright install chromium; \
|
||||
else \
|
||||
poetry install; \
|
||||
fi
|
||||
@if [ "${INSTALL_PLAYWRIGHT}" != "false" ] && [ "${INSTALL_PLAYWRIGHT}" != "0" ]; then \
|
||||
if [ -f "/etc/manjaro-release" ]; then \
|
||||
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
|
||||
poetry run pip install playwright; \
|
||||
poetry run playwright install chromium; \
|
||||
if [ ! -f cache/playwright_chromium_is_installed.txt ]; then \
|
||||
echo "Running playwright install --with-deps chromium..."; \
|
||||
poetry run playwright install --with-deps chromium; \
|
||||
mkdir -p cache; \
|
||||
touch cache/playwright_chromium_is_installed.txt; \
|
||||
else \
|
||||
if [ ! -f cache/playwright_chromium_is_installed.txt ]; then \
|
||||
echo "Running playwright install --with-deps chromium..."; \
|
||||
poetry run playwright install --with-deps chromium; \
|
||||
mkdir -p cache; \
|
||||
touch cache/playwright_chromium_is_installed.txt; \
|
||||
else \
|
||||
echo "Setup already done. Skipping playwright installation."; \
|
||||
fi \
|
||||
echo "Setup already done. Skipping playwright installation."; \
|
||||
fi \
|
||||
else \
|
||||
echo "Skipping Playwright installation (INSTALL_PLAYWRIGHT=${INSTALL_PLAYWRIGHT})."; \
|
||||
fi
|
||||
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
|
||||
|
||||
|
||||
45
README.md
45
README.md
@@ -27,7 +27,7 @@ Welcome to OpenHands (formerly OpenDevin), a platform for software development a
|
||||
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
|
||||
call APIs, and yes—even copy code snippets from StackOverflow.
|
||||
|
||||
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for OpenHands Cloud](https://app.all-hands.dev) to get started.
|
||||
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [Quick Start](#-quick-start).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Using OpenHands for work? We'd love to chat! Fill out
|
||||
@@ -36,50 +36,37 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for
|
||||
|
||||

|
||||
|
||||
## ☁️ OpenHands Cloud
|
||||
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
|
||||
which comes with $50 in free credits for new users.
|
||||
## ⚡ Quick Start
|
||||
|
||||
## 💻 Running OpenHands Locally
|
||||
|
||||
OpenHands can also run on your local system using Docker.
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) guide for
|
||||
system requirements and more information.
|
||||
|
||||
> [!WARNING]
|
||||
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
|
||||
> to secure your deployment by restricting network binding and implementing additional security measures.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
> [!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)!
|
||||
|
||||
When you open the application, you'll be asked to choose an LLM provider and add an API key.
|
||||
Finally, you'll need a model provider and API key.
|
||||
[Anthropic's Claude 3.5 Sonnet](https://www.anthropic.com/api) (`anthropic/claude-3-5-sonnet-20241022`)
|
||||
works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
|
||||
|
||||
## 💡 Other ways to run OpenHands
|
||||
|
||||
> [!CAUTION]
|
||||
> OpenHands is meant to be run by a single user on their local workstation.
|
||||
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
|
||||
>
|
||||
> If you're interested in running OpenHands in a multi-tenant environment, please
|
||||
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> for advanced deployment options.
|
||||
---
|
||||
|
||||
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem),
|
||||
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
|
||||
@@ -88,6 +75,14 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
|
||||
|
||||
Visit [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
|
||||
|
||||
> [!CAUTION]
|
||||
> OpenHands is meant to be run by a single user on their local workstation.
|
||||
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in isolation or scalability.
|
||||
>
|
||||
> If you're interested in running OpenHands in a multi-tenant environment, please
|
||||
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> for advanced deployment options.
|
||||
|
||||
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
|
||||
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting) can help.
|
||||
|
||||
@@ -216,13 +216,13 @@ model = "gpt-4o"
|
||||
[agent]
|
||||
|
||||
# Whether the browsing tool is enabled
|
||||
enable_browsing = true
|
||||
codeact_enable_browsing = true
|
||||
|
||||
# Whether the LLM draft editor is enabled
|
||||
enable_llm_editor = false
|
||||
codeact_enable_llm_editor = false
|
||||
|
||||
# Whether the IPython tool is enabled
|
||||
enable_jupyter = true
|
||||
codeact_enable_jupyter = true
|
||||
|
||||
# LLM config group to use
|
||||
#llm_config = 'your-llm-config-group'
|
||||
|
||||
@@ -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.33-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.32-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -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.33-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.32-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:
|
||||
|
||||
@@ -354,12 +354,12 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
|
||||
- Valeur par défaut : `true`
|
||||
- Description : Si l'appel de fonction est activé
|
||||
|
||||
- `enable_browsing`
|
||||
- `codeact_enable_browsing`
|
||||
- Type : `bool`
|
||||
- Valeur par défaut : `false`
|
||||
- Description : Si le délégué de navigation est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
|
||||
|
||||
- `enable_llm_editor`
|
||||
- `codeact_enable_llm_editor`
|
||||
- Type : `bool`
|
||||
- Valeur par défaut : `false`
|
||||
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
|
||||
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -21,18 +21,14 @@ OpenHands fournit un mode Interface Graphique (GUI) convivial pour interagir ave
|
||||
3. Entrez la `Clé API` correspondante pour le fournisseur choisi.
|
||||
4. Cliquez sur "Enregistrer" pour appliquer les paramètres.
|
||||
|
||||
### Jetons de Contrôle de Version
|
||||
|
||||
OpenHands prend en charge plusieurs fournisseurs de contrôle de version. Vous pouvez configurer des jetons pour plusieurs fournisseurs simultanément.
|
||||
|
||||
#### Configuration du Jeton GitHub
|
||||
### Configuration du Jeton GitHub
|
||||
|
||||
OpenHands exporte automatiquement un `GITHUB_TOKEN` vers l'environnement shell s'il est disponible. Cela peut se produire de deux manières :
|
||||
|
||||
1. **Localement (OSS)** : L'utilisateur saisit directement son jeton GitHub
|
||||
2. **En ligne (SaaS)** : Le jeton est obtenu via l'authentification OAuth GitHub
|
||||
|
||||
##### Configuration d'un Jeton GitHub Local
|
||||
#### Configuration d'un Jeton GitHub Local
|
||||
|
||||
1. **Générer un Personal Access Token (PAT)** :
|
||||
- Allez dans Paramètres GitHub > Paramètres développeur > Personal Access Tokens > Tokens (classique)
|
||||
@@ -44,11 +40,11 @@ OpenHands exporte automatiquement un `GITHUB_TOKEN` vers l'environnement shell s
|
||||
|
||||
2. **Entrer le Jeton dans OpenHands** :
|
||||
- Cliquez sur le bouton Paramètres (icône d'engrenage) en haut à droite
|
||||
- Accédez à la section "Git Provider Settings"
|
||||
- Accédez à la section "GitHub"
|
||||
- Collez votre jeton dans le champ "Jeton GitHub"
|
||||
- Cliquez sur "Enregistrer" pour appliquer les modifications
|
||||
|
||||
##### Politiques de Jetons Organisationnels
|
||||
#### Politiques de Jetons Organisationnels
|
||||
|
||||
Si vous travaillez avec des dépôts organisationnels, une configuration supplémentaire peut être nécessaire :
|
||||
|
||||
@@ -63,7 +59,7 @@ Si vous travaillez avec des dépôts organisationnels, une configuration supplé
|
||||
- Si nécessaire, cliquez sur "Activer SSO" à côté de votre organisation
|
||||
- Terminez le processus d'autorisation SSO
|
||||
|
||||
##### Authentification OAuth (Mode En Ligne)
|
||||
#### Authentification OAuth (Mode En Ligne)
|
||||
|
||||
Lorsque vous utilisez OpenHands en mode en ligne, le flux OAuth GitHub :
|
||||
|
||||
@@ -78,7 +74,7 @@ Lorsque vous utilisez OpenHands en mode en ligne, le flux OAuth GitHub :
|
||||
- Autorisez OpenHands à accéder à votre compte GitHub
|
||||
- Si vous utilisez une organisation, autorisez l'accès à l'organisation si vous y êtes invité
|
||||
|
||||
##### Dépannage
|
||||
#### Dépannage
|
||||
|
||||
Problèmes courants et solutions :
|
||||
|
||||
@@ -99,43 +95,6 @@ Problèmes courants et solutions :
|
||||
- Vérifiez la console du navigateur pour tout message d'erreur
|
||||
- Utilisez le bouton "Tester la connexion" dans les paramètres s'il est disponible
|
||||
|
||||
#### Configuration du Jeton GitLab
|
||||
|
||||
OpenHands exporte automatiquement un `GITLAB_TOKEN` vers l'environnement shell, uniquement pour les installations locales, s'il est disponible.
|
||||
|
||||
##### Configuration d'un Jeton GitLab
|
||||
|
||||
1. **Générer un Personal Access Token (PAT)** :
|
||||
- Sur GitLab, allez dans Paramètres utilisateur > Jetons d'accès
|
||||
- Créez un nouveau jeton avec les portées suivantes :
|
||||
- `api` (Accès API)
|
||||
- `read_user` (Lecture des informations utilisateur)
|
||||
- `read_repository` (Lecture du dépôt)
|
||||
- `write_repository` (Écriture du dépôt)
|
||||
- Définissez une date d'expiration ou laissez vide pour un jeton sans expiration
|
||||
|
||||
2. **Entrer le Jeton dans OpenHands** :
|
||||
- Cliquez sur le bouton Paramètres (icône d'engrenage)
|
||||
- Accédez à la section `Git Provider Settings`
|
||||
- Collez votre jeton dans le champ `Jeton GitLab`
|
||||
- Si vous utilisez GitLab auto-hébergé, entrez l'URL de votre instance GitLab
|
||||
- Cliquez sur `Enregistrer les modifications` pour appliquer les changements
|
||||
|
||||
##### Dépannage
|
||||
|
||||
Problèmes courants et solutions :
|
||||
|
||||
1. **Jeton Non Reconnu** :
|
||||
- Assurez-vous que le jeton est correctement enregistré dans les paramètres
|
||||
- Vérifiez que le jeton n'a pas expiré
|
||||
- Vérifiez que le jeton a les portées requises
|
||||
- Pour les instances auto-hébergées, vérifiez l'URL correcte de l'instance
|
||||
|
||||
2. **Accès Refusé** :
|
||||
- Vérifiez les permissions d'accès au projet
|
||||
- Vérifiez si le jeton possède les portées nécessaires
|
||||
- Pour les dépôts de groupe/organisation, assurez-vous d'avoir les accès appropriés
|
||||
|
||||
### Paramètres Avancés
|
||||
|
||||
1. Basculez sur `Options Avancées` pour accéder aux paramètres supplémentaires.
|
||||
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
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.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -348,12 +348,12 @@ dockerコマンドで使用する場合は、`-e LLM_<option>`として渡しま
|
||||
- デフォルト値: `true`
|
||||
- 説明: 関数呼び出しが有効かどうか
|
||||
|
||||
- `enable_browsing`
|
||||
- `codeact_enable_browsing`
|
||||
- 型: `bool`
|
||||
- デフォルト値: `false`
|
||||
- 説明: アクションスペースでブラウジングデリゲートが有効かどうか(関数呼び出しでのみ機能)
|
||||
|
||||
- `enable_llm_editor`
|
||||
- `codeact_enable_llm_editor`
|
||||
- 型: `bool`
|
||||
- デフォルト値: `false`
|
||||
- 説明: アクションスペースでLLMエディタが有効かどうか(関数呼び出しでのみ機能)
|
||||
|
||||
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -44,7 +44,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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@ OpenHandsは、AI アシスタントとやり取りするためのグラフィ
|
||||
3. 選択したプロバイダーに対応する`API Key`を入力します。
|
||||
4. `Save Changes`をクリックして設定を適用します。
|
||||
|
||||
### バージョン管理トークン
|
||||
|
||||
OpenHandsは複数のバージョン管理プロバイダーをサポートしています。複数のプロバイダーのトークンを同時に設定できます。
|
||||
|
||||
#### GitHubトークンの設定
|
||||
### GitHubトークンの設定
|
||||
|
||||
OpenHandsは、利用可能な場合、自動的に`GITHUB_TOKEN`をシェル環境にエクスポートします。これは2つの方法で行われます。
|
||||
|
||||
@@ -38,7 +34,7 @@ OpenHandsは、利用可能な場合、自動的に`GITHUB_TOKEN`をシェル環
|
||||
- Minimal Permissions(検索用に**Meta Data = Read-only**を選択し、ブランチ作成用に**Pull Requests = Read and Write**、**Content = Read and Write**を選択します)
|
||||
2. **OpenHandsにトークンを入力**:
|
||||
- 設定ボタン(歯車アイコン)をクリックします。
|
||||
- `Git Provider Settings`セクションに移動します。
|
||||
- `GitHub Settings`セクションに移動します。
|
||||
- `GitHub Token`フィールドにトークンを貼り付けます。
|
||||
- `Save Changes`をクリックして変更を適用します。
|
||||
</details>
|
||||
@@ -98,46 +94,6 @@ OpenHandsは、利用可能な場合、自動的に`GITHUB_TOKEN`をシェル環
|
||||
- 組織を使用している場合は、プロンプトが表示されたら組織へのアクセスを承認します。
|
||||
</details>
|
||||
|
||||
#### GitLabトークンの設定
|
||||
|
||||
OpenHandsは、利用可能な場合、ローカルインストールのみ、自動的に`GITLAB_TOKEN`をシェル環境にエクスポートします。
|
||||
|
||||
<details>
|
||||
<summary>GitLabトークンの設定</summary>
|
||||
|
||||
1. **Personal Access Token(PAT)の生成**:
|
||||
- GitLabで、User Settings > Access Tokensに移動します。
|
||||
- 以下のスコープを持つ新しいトークンを作成します:
|
||||
- `api`(APIアクセス)
|
||||
- `read_user`(ユーザー情報の読み取り)
|
||||
- `read_repository`(リポジトリ読み取り)
|
||||
- `write_repository`(リポジトリ書き込み)
|
||||
- 有効期限を設定するか、無期限トークンの場合は空白のままにします。
|
||||
2. **OpenHandsにトークンを入力**:
|
||||
- 設定ボタン(歯車アイコン)をクリックします。
|
||||
- `Git Provider Settings`セクションに移動します。
|
||||
- `GitLab Token`フィールドにトークンを貼り付けます。
|
||||
- セルフホスト型GitLabを使用している場合は、GitLabインスタンスのURLを入力します。
|
||||
- `Save Changes`をクリックして変更を適用します。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>トラブルシューティング</summary>
|
||||
|
||||
一般的な問題と解決策:
|
||||
|
||||
- **トークンが認識されない**:
|
||||
- トークンが設定に正しく保存されていることを確認します。
|
||||
- トークンの有効期限が切れていないことを確認します。
|
||||
- トークンに必要なスコープがあることを確認します。
|
||||
- セルフホスト型インスタンスの場合は、正しいインスタンスURLを確認します。
|
||||
|
||||
- **アクセスが拒否された**:
|
||||
- プロジェクトのアクセス権限を確認します。
|
||||
- トークンに必要なスコープがあるかどうかを確認します。
|
||||
- グループ/組織のリポジトリの場合は、適切なアクセス権があることを確認します。
|
||||
</details>
|
||||
|
||||
### 高度な設定
|
||||
|
||||
1. 設定ページ内で、`Advanced`オプションを切り替えて追加の設定にアクセスします。
|
||||
|
||||
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ OpenHandsがリポジトリで動作する際:
|
||||
```
|
||||
---
|
||||
name: <Microagentの名前>
|
||||
type: <Microagentのタイプ>
|
||||
version: <Microagentのバージョン>
|
||||
type: <MicroAgentのタイプ>
|
||||
version: <MicroAgentのバージョン>
|
||||
agent: <エージェントのタイプ (通常はCodeActAgent)>
|
||||
triggers:
|
||||
- <オプション: microagentをトリガーするキーワード。トリガーを削除すると、常に含まれるようになります>
|
||||
|
||||
@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
@@ -82,5 +82,5 @@ docker network create openhands-network
|
||||
# 分離されたネットワークで OpenHands を実行
|
||||
docker run # ... \
|
||||
--network openhands-network \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@ O GitHub Cloud Resolver automatiza correções de código e fornece assistência
|
||||
## Configuração
|
||||
|
||||
O Resolvedor do GitHub na Nuvem está disponível automaticamente quando você
|
||||
[concede acesso ao repositório do OpenHands Cloud](./openhands-cloud#adding-repository-access).
|
||||
[concede acesso ao repositório do OpenHands Cloud](./openhands-cloud.md#adding-repository-access).
|
||||
|
||||
## Uso
|
||||
|
||||
|
||||
@@ -292,17 +292,17 @@ As opções de configuração do agente são definidas nas seções `[agent]` e
|
||||
- Padrão: `true`
|
||||
- Descrição: Se a chamada de função está habilitada
|
||||
|
||||
- `enable_browsing`
|
||||
- `codeact_enable_browsing`
|
||||
- Tipo: `bool`
|
||||
- Padrão: `false`
|
||||
- Descrição: Se o delegado de navegação está habilitado no espaço de ação (funciona apenas com chamada de função)
|
||||
|
||||
- `enable_llm_editor`
|
||||
- `codeact_enable_llm_editor`
|
||||
- Tipo: `bool`
|
||||
- Padrão: `false`
|
||||
- Descrição: Se o editor LLM está habilitado no espaço de ação (funciona apenas com chamada de função)
|
||||
|
||||
- `enable_jupyter`
|
||||
- `codeact_enable_jupyter`
|
||||
- Tipo: `bool`
|
||||
- Padrão: `false`
|
||||
- Descrição: Se o Jupyter está habilitado no espaço de ação
|
||||
|
||||
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ O OpenHands fornece um modo de Interface Gráfica do Usuário (GUI) para interag
|
||||
3. Insira a `Chave de API` correspondente para o provedor escolhido.
|
||||
4. Clique em `Salvar Alterações` para aplicar as configurações.
|
||||
|
||||
### Tokens de Controle de Versão
|
||||
|
||||
O OpenHands suporta múltiplos provedores de controle de versão. Você pode configurar tokens para vários provedores simultaneamente.
|
||||
|
||||
#### Configuração do Token do GitHub
|
||||
### Configuração do Token do GitHub
|
||||
|
||||
O OpenHands exporta automaticamente um `GITHUB_TOKEN` para o ambiente shell se ele estiver disponível. Isso pode acontecer de duas maneiras:
|
||||
|
||||
@@ -39,7 +35,7 @@ O OpenHands exporta automaticamente um `GITHUB_TOKEN` para o ambiente shell se e
|
||||
- Minimal Permissions (Selecione **Meta Data = Read-only** para pesquisa, **Pull Requests = Read and Write**, **Content = Read and Write** para criação de branches)
|
||||
2. **Insira o Token no OpenHands**:
|
||||
- Clique no botão Settings (ícone de engrenagem).
|
||||
- Navegue até a seção `Git Provider Settings`.
|
||||
- Navegue até a seção `GitHub Settings`.
|
||||
- Cole seu token no campo `GitHub Token`.
|
||||
- Clique em `Save Changes` para aplicar as alterações.
|
||||
</details>
|
||||
@@ -99,46 +95,6 @@ O OpenHands exporta automaticamente um `GITHUB_TOKEN` para o ambiente shell se e
|
||||
- Se estiver usando uma organização, autorize o acesso à organização se solicitado.
|
||||
</details>
|
||||
|
||||
#### Configuração do Token do GitLab
|
||||
|
||||
O OpenHands exporta automaticamente um `GITLAB_TOKEN` para o ambiente shell, apenas para instalações locais, se ele estiver disponível.
|
||||
|
||||
<details>
|
||||
<summary>Configurando um Token do GitLab</summary>
|
||||
|
||||
1. **Gere um Personal Access Token (PAT)**:
|
||||
- No GitLab, vá para User Settings > Access Tokens.
|
||||
- Crie um novo token com os seguintes escopos:
|
||||
- `api` (Acesso à API)
|
||||
- `read_user` (Leitura de informações do usuário)
|
||||
- `read_repository` (Leitura do repositório)
|
||||
- `write_repository` (Escrita no repositório)
|
||||
- Defina uma data de expiração ou deixe em branco para um token sem expiração.
|
||||
2. **Insira o Token no OpenHands**:
|
||||
- Clique no botão Settings (ícone de engrenagem).
|
||||
- Navegue até a seção `Git Provider Settings`.
|
||||
- Cole seu token no campo `GitLab Token`.
|
||||
- Se estiver usando GitLab auto-hospedado, insira a URL da sua instância GitLab.
|
||||
- Clique em `Save Changes` para aplicar as alterações.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Solução de Problemas</summary>
|
||||
|
||||
Problemas comuns e soluções:
|
||||
|
||||
- **Token Não Reconhecido**:
|
||||
- Certifique-se de que o token esteja salvo corretamente nas configurações.
|
||||
- Verifique se o token não expirou.
|
||||
- Verifique se o token possui os escopos necessários.
|
||||
- Para instâncias auto-hospedadas, verifique a URL correta da instância.
|
||||
|
||||
- **Acesso Negado**:
|
||||
- Verifique as permissões de acesso ao projeto.
|
||||
- Verifique se o token possui os escopos necessários.
|
||||
- Para repositórios de grupo/organização, certifique-se de ter o acesso adequado.
|
||||
</details>
|
||||
|
||||
### Configurações Avançadas
|
||||
|
||||
1. Dentro da página Settings, ative as opções `Advanced` para acessar configurações adicionais.
|
||||
|
||||
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "escreva um script bash que imprima oi"
|
||||
```
|
||||
|
||||
|
||||
@@ -58,17 +58,17 @@
|
||||
A maneira mais fácil de executar o OpenHands é no Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
Você encontrará o OpenHands em execução em http://localhost:3000!
|
||||
|
||||
@@ -21,8 +21,8 @@ Todos os microagentes usam arquivos markdown com frontmatter YAML que possuem in
|
||||
```
|
||||
---
|
||||
name: <Nome do microagente>
|
||||
type: <Tipo do Microagent>
|
||||
version: <Versão do Microagent>
|
||||
type: <Tipo do MicroAgent>
|
||||
version: <Versão do MicroAgent>
|
||||
agent: <O tipo de agente (normalmente CodeActAgent)>
|
||||
triggers:
|
||||
- <Palavras-chave opcionais que acionam o microagente. Se os gatilhos forem removidos, ele sempre será incluído>
|
||||
|
||||
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -349,17 +349,17 @@ Agent 配置选项在 `config.toml` 文件的 `[agent]` 和 `[agent.<agent_name>
|
||||
- 默认值: `true`
|
||||
- 描述: 是否启用函数调用
|
||||
|
||||
- `enable_browsing`
|
||||
- `codeact_enable_browsing`
|
||||
- 类型: `bool`
|
||||
- 默认值: `false`
|
||||
- 描述: 是否在 action space 中启用浏览代理(仅适用于函数调用)
|
||||
|
||||
- `enable_llm_editor`
|
||||
- `codeact_enable_llm_editor`
|
||||
- 类型: `bool`
|
||||
- 默认值: `false`
|
||||
- 描述: 是否在 action space 中启用 LLM 编辑器(仅适用于函数调用)
|
||||
|
||||
- `enable_jupyter`
|
||||
- `codeact_enable_jupyter`
|
||||
- 类型: `bool`
|
||||
- 默认值: `false`
|
||||
- 描述: 是否在 action space 中启用 Jupyter
|
||||
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -19,18 +19,14 @@ OpenHands 提供了一个用户友好的图形用户界面(GUI)模式,用
|
||||
3. 输入所选提供商对应的 `API Key`。
|
||||
4. 点击"保存"应用设置。
|
||||
|
||||
### 版本控制令牌
|
||||
|
||||
OpenHands 支持多个版本控制提供商。您可以同时配置多个提供商的令牌。
|
||||
|
||||
#### GitHub Token 设置
|
||||
### GitHub Token 设置
|
||||
|
||||
如果可用,OpenHands 会自动将 `GITHUB_TOKEN` 导出到 shell 环境中。这可以通过两种方式实现:
|
||||
|
||||
1. **本地(OSS)**:用户直接输入他们的 GitHub token
|
||||
2. **在线(SaaS)**:通过 GitHub OAuth 身份验证获取 token
|
||||
|
||||
##### 设置本地 GitHub Token
|
||||
#### 设置本地 GitHub Token
|
||||
|
||||
1. **生成个人访问令牌(PAT)**:
|
||||
- 转到 GitHub 设置 > 开发者设置 > 个人访问令牌 > 令牌(经典)
|
||||
@@ -42,11 +38,11 @@ OpenHands 支持多个版本控制提供商。您可以同时配置多个提供
|
||||
|
||||
2. **在 OpenHands 中输入令牌**:
|
||||
- 点击右上角的设置按钮(齿轮图标)
|
||||
- 导航到"Git Provider Settings"部分
|
||||
- 导航到"GitHub"部分
|
||||
- 将令牌粘贴到"GitHub Token"字段中
|
||||
- 点击"保存"应用更改
|
||||
|
||||
##### 组织令牌策略
|
||||
#### 组织令牌策略
|
||||
|
||||
如果您使用组织仓库,可能需要额外的设置:
|
||||
|
||||
@@ -61,7 +57,7 @@ OpenHands 支持多个版本控制提供商。您可以同时配置多个提供
|
||||
- 如果需要,点击组织旁边的"启用 SSO"
|
||||
- 完成 SSO 授权过程
|
||||
|
||||
##### OAuth 身份验证(在线模式)
|
||||
#### OAuth 身份验证(在线模式)
|
||||
|
||||
在在线模式下使用 OpenHands 时,GitHub OAuth 流程:
|
||||
|
||||
@@ -76,7 +72,7 @@ OpenHands 支持多个版本控制提供商。您可以同时配置多个提供
|
||||
- 授权 OpenHands 访问您的 GitHub 帐户
|
||||
- 如果使用组织,在出现提示时授权组织访问
|
||||
|
||||
##### 故障排除
|
||||
#### 故障排除
|
||||
|
||||
常见问题和解决方案:
|
||||
|
||||
@@ -97,43 +93,6 @@ OpenHands 支持多个版本控制提供商。您可以同时配置多个提供
|
||||
- 检查浏览器控制台中是否有任何错误消息
|
||||
- 如果可用,使用设置中的"测试连接"按钮
|
||||
|
||||
#### GitLab Token 设置
|
||||
|
||||
OpenHands 会自动将 `GITLAB_TOKEN` 导出到 shell 环境中,仅适用于本地安装,如果它可用的话。
|
||||
|
||||
##### 设置 GitLab Token
|
||||
|
||||
1. **生成个人访问令牌(PAT)**:
|
||||
- 在 GitLab 中,转到用户设置 > 访问令牌
|
||||
- 创建具有以下范围的新令牌:
|
||||
- `api`(API 访问)
|
||||
- `read_user`(读取用户信息)
|
||||
- `read_repository`(读取仓库)
|
||||
- `write_repository`(写入仓库)
|
||||
- 设置过期日期或留空以获取永不过期的令牌
|
||||
|
||||
2. **在 OpenHands 中输入令牌**:
|
||||
- 点击设置按钮(齿轮图标)
|
||||
- 导航到 `Git Provider Settings` 部分
|
||||
- 将令牌粘贴到 `GitLab Token` 字段中
|
||||
- 如果使用自托管 GitLab,请输入您的 GitLab 实例 URL
|
||||
- 点击 `Save Changes` 应用更改
|
||||
|
||||
##### 故障排除
|
||||
|
||||
常见问题和解决方案:
|
||||
|
||||
1. **令牌无法识别**:
|
||||
- 确保令牌已正确保存在设置中
|
||||
- 检查令牌是否已过期
|
||||
- 验证令牌是否具有所需的范围
|
||||
- 对于自托管实例,验证正确的实例 URL
|
||||
|
||||
2. **访问被拒绝**:
|
||||
- 验证项目访问权限
|
||||
- 检查令牌是否具有必要的范围
|
||||
- 对于组/组织仓库,确保您拥有适当的访问权限
|
||||
|
||||
### 高级设置
|
||||
|
||||
1. 切换`高级选项`以访问其他设置。
|
||||
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
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.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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#adding-repository-access).
|
||||
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repository-access).
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ After visiting OpenHands Cloud, you will be asked to connect with your GitHub ac
|
||||
You can grant OpenHands specific repository access:
|
||||
1. Click the `Select a GitHub project` dropdown, select `Add more repositories...`.
|
||||
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
|
||||
<details>
|
||||
<summary>Permission Details for Repository Access</summary>
|
||||
|
||||
Openhands requests short-lived tokens (8-hour expiry) with these permissions:
|
||||
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
|
||||
- Actions: Read and write
|
||||
- Administration: Read-only
|
||||
- Commit statuses: Read and write
|
||||
@@ -34,12 +31,9 @@ You can grant OpenHands specific repository access:
|
||||
- Pull requests: Read and write
|
||||
- Webhooks: Read and write
|
||||
- Workflows: Read and write
|
||||
|
||||
Repository access for a user is granted based on:
|
||||
- Repository access for a user is granted based on:
|
||||
- Granted permission for the repository.
|
||||
- User's GitHub permissions (owner/collaborator).
|
||||
</details>
|
||||
|
||||
3. Click on `Install & Authorize`.
|
||||
|
||||
### Modifying Repository Access
|
||||
@@ -47,9 +41,3 @@ You can grant OpenHands specific repository access:
|
||||
You can modify repository access at any time by:
|
||||
* Using the same `Select a GitHub project > Add more repositories` workflow, or
|
||||
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.
|
||||
|
||||
## Conversation Persistence
|
||||
|
||||
- Conversations List – Displays only the 10 most recent conversations initiated within the past 10 days.
|
||||
- Workspaces – Conversation workspaces are retained for 14 days.
|
||||
- Runtimes – Runtimes remain active ("warm") for 30 minutes. After this period, resuming a conversation may take 1–2 minutes.
|
||||
@@ -291,17 +291,17 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
|
||||
- Default: `true`
|
||||
- Description: Whether function calling is enabled
|
||||
|
||||
- `enable_browsing`
|
||||
- `codeact_enable_browsing`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
- Description: Whether browsing delegate is enabled in the action space (only works with function calling)
|
||||
|
||||
- `enable_llm_editor`
|
||||
- `codeact_enable_llm_editor`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
- Description: Whether LLM editor is enabled in the action space (only works with function calling)
|
||||
|
||||
- `enable_jupyter`
|
||||
- `codeact_enable_jupyter`
|
||||
- Type: `bool`
|
||||
- Default: `false`
|
||||
- Description: Whether Jupyter is enabled in the action space
|
||||
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,11 +45,8 @@ 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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
This command will start an interactive session in Docker where you can input tasks and receive responses from OpenHands.
|
||||
|
||||
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host user’s
|
||||
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
|
||||
|
||||
@@ -18,11 +18,7 @@ OpenHands provides a Graphical User Interface (GUI) mode for interacting with th
|
||||
3. Enter the corresponding `API Key` for your chosen provider.
|
||||
4. Click `Save Changes` to apply the settings.
|
||||
|
||||
### Version Control Tokens
|
||||
|
||||
OpenHands supports multiple version control providers. You can configure tokens for multiple providers simultaneously.
|
||||
|
||||
#### GitHub Token Setup
|
||||
### GitHub Token Setup
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
|
||||
|
||||
@@ -40,7 +36,7 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it
|
||||
- Minimal Permissions ( Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `Git Provider Settings` section.
|
||||
- Navigate to the `GitHub Settings` section.
|
||||
- Paste your token in the `GitHub Token` field.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
</details>
|
||||
@@ -100,46 +96,6 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it
|
||||
- If using an organization, authorize organization access if prompted.
|
||||
</details>
|
||||
|
||||
#### GitLab Token Setup
|
||||
|
||||
OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment, for local installations only, if it is available.
|
||||
|
||||
<details>
|
||||
<summary>Setting Up a GitLab Token</summary>
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- On GitLab, go to User Settings > Access Tokens.
|
||||
- Create a new token with the following scopes:
|
||||
- `api` (API access)
|
||||
- `read_user` (Read user information)
|
||||
- `read_repository` (Read repository)
|
||||
- `write_repository` (Write repository)
|
||||
- Set an expiration date or leave it blank for a non-expiring token.
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `Git Provider Settings` section.
|
||||
- Paste your token in the `GitLab Token` field.
|
||||
- Enter your GitLab instance URL if using self-hosted GitLab.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Troubleshooting</summary>
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
- **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- For self-hosted instances, verify the correct instance URL.
|
||||
|
||||
- **Access Denied**:
|
||||
- Verify project access permissions.
|
||||
- Check if the token has the necessary scopes.
|
||||
- For group/organization repositories, ensure you have proper access.
|
||||
</details>
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
1. Inside the Settings page, toggle `Advanced` options to access additional settings.
|
||||
|
||||
@@ -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.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,13 +43,10 @@ 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.33 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host user’s
|
||||
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
|
||||
|
||||
## Advanced Headless Configurations
|
||||
|
||||
To view all available configuration options for headless mode, run the Python command with the `--help` flag.
|
||||
|
||||
@@ -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.33-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.33-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.32-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.33
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.32
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
@@ -35,8 +35,8 @@ A useful feature is the ability to connect to your local filesystem. To mount yo
|
||||
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
|
||||
any files that are mounted into its workspace.
|
||||
|
||||
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host user’s
|
||||
permissions. This prevents the agent from creating root-owned files in the mounted 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
|
||||
|
||||
|
||||
@@ -20,18 +20,3 @@ Try these in order:
|
||||
* If using Docker Desktop, ensure `Settings > Advanced > Allow the default Docker socket to be used` is enabled.
|
||||
* Depending on your configuration you may need `Settings > Resources > Network > Enable host networking` enabled in Docker Desktop.
|
||||
* Reinstall Docker Desktop.
|
||||
|
||||
### Permission Error
|
||||
|
||||
**Description**
|
||||
|
||||
On initial prompt, an error is seen with `Permission Denied` or `PermissionError`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
* Check if the `~/.openhands-state` is owned by `root`. If so, you can:
|
||||
* Change the directory's ownership: `sudo chown <user>:<user> ~/.openhands-state`.
|
||||
* or update permissions on the directory: `sudo chmod 777 ~/.openhands-state`
|
||||
* or delete it if you don’t need previous data. OpenHands will recreate it. You'll need to re-enter LLM settings.
|
||||
* If mounting a local directory, ensure your `WORKSPACE_BASE` has the necessary permissions for the user running
|
||||
OpenHands.
|
||||
|
||||
8
docs/package-lock.json
generated
8
docs/package-lock.json
generated
@@ -24,7 +24,7 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"typescript": "~5.8.3"
|
||||
"typescript": "~5.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
@@ -17638,9 +17638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"typescript": "~5.8.3"
|
||||
"typescript": "~5.8.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -9307,10 +9307,10 @@ typedarray-to-buffer@^3.1.5:
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
typescript@~5.8.2:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz"
|
||||
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
|
||||
@@ -131,9 +131,9 @@ def get_config(
|
||||
)
|
||||
)
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=False,
|
||||
enable_browsing=RUN_WITH_BROWSING,
|
||||
enable_llm_editor=False,
|
||||
codeact_enable_jupyter=False,
|
||||
codeact_enable_browsing=RUN_WITH_BROWSING,
|
||||
codeact_enable_llm_editor=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
@@ -79,8 +79,8 @@ def get_config(
|
||||
agent_config.enable_prompt_extensions = False
|
||||
agent_config = AgentConfig(
|
||||
function_calling=False,
|
||||
enable_jupyter=True,
|
||||
enable_browsing=True,
|
||||
codeact_enable_jupyter=True,
|
||||
codeact_enable_browsing_delegate=True,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
@@ -84,54 +84,39 @@ I've already taken care of all changes to any of the test files described in the
|
||||
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
|
||||
Your task is to make the minimal changes to non-test files in the /workspace/{workspace_dir_name} directory to ensure the <issue_description> is satisfied.
|
||||
|
||||
Follow these phases to resolve the issue:
|
||||
Follow these steps to resolve the issue:
|
||||
|
||||
Phase 1. READING: read the problem and reword it in clearer terms
|
||||
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
|
||||
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
|
||||
1.3 Explain the problem in clear terms.
|
||||
1.4 Enumerate the steps to reproduce the problem.
|
||||
1.5 Hightlight any best practices to take into account when testing and fixing the issue
|
||||
1. EXPLORATION: First, thoroughly explore the repository structure using tools like `find` and `grep`.
|
||||
- Identify all files mentioned in the problem statement
|
||||
- Locate where the issue occurs in the codebase
|
||||
- Understand the surrounding context and dependencies
|
||||
- Use `grep` to search for relevant functions, classes, or error messages
|
||||
|
||||
Phase 2. RUNNING: install and run the tests on the repository
|
||||
2.1 Follow the readme
|
||||
2.2 Install the environment and anything needed
|
||||
2.2 Iterate and figure out how to run the tests
|
||||
2. ANALYSIS: Based on your exploration, think carefully about the problem and propose 2-5 possible approaches to fix the issue.
|
||||
- Analyze the root cause of the problem
|
||||
- Consider trade-offs between different solutions
|
||||
- Select the most promising approach and explain your reasoning
|
||||
|
||||
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
|
||||
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
|
||||
3.2 Identify all files related to the problem statement.
|
||||
3.3 Propose the methods and files to fix the issue and explain why.
|
||||
3.4 From the possible file locations, select the most likely location to fix the issue.
|
||||
3. TEST CREATION: Before implementing any fix, create a script to reproduce and verify the issue.
|
||||
- Look at existing test files in the repository to understand the test format/structure
|
||||
- Create a minimal reproduction script that demonstrates the issue
|
||||
- Run your script to confirm the error exists
|
||||
|
||||
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
|
||||
4.1 Look at existing test files in the repository to understand the test format/structure.
|
||||
4.2 Create a minimal reproduction script that reproduces the located issue.
|
||||
4.3 Run the reproduction script to confirm you are reproducing the issue.
|
||||
4.4 Adjust the reproduction script as necessary.
|
||||
4. IMPLEMENTATION: Edit the source code to implement your chosen solution.
|
||||
- Make minimal, focused changes to fix the issue
|
||||
|
||||
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
|
||||
5.1 State clearly what the problem is.
|
||||
5.2 State clearly where the problem is located.
|
||||
5.3 State clearly how the test reproduces the issue.
|
||||
5.4 State clearly the best practices to take into account in the fix.
|
||||
5.5 State clearly how to fix the problem.
|
||||
5. VERIFICATION: Test your implementation thoroughly.
|
||||
- Run your reproduction script to verify the fix works
|
||||
- Add edge cases to your test script to ensure comprehensive coverage
|
||||
- Run existing tests related to the modified code to ensure you haven't broken anything
|
||||
|
||||
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
|
||||
6.1 Make minimal, focused changes to fix the issue.
|
||||
|
||||
Phase 7. VERIFICATION: Test your implementation thoroughly.
|
||||
7.1 Run your reproduction script to verify the fix works.
|
||||
7.2 Add edge cases to your test script to ensure comprehensive coverage.
|
||||
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
|
||||
|
||||
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance["base_commit"]}.
|
||||
8.1 Ensure you've fully addressed all requirements.
|
||||
8.2 Run any tests in the repository related to:
|
||||
8.2.1 The issue you are fixing
|
||||
8.2.2 The files you modified
|
||||
8.2.3 The functions you changed
|
||||
8.3 If any tests fail, revise your implementation until all tests pass
|
||||
6. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance["base_commit"]}.
|
||||
- Ensure you've fully addressed all requirements
|
||||
- Run any tests in the repository related to:
|
||||
* The issue you are fixing
|
||||
* The files you modified
|
||||
* The functions you changed
|
||||
- If any tests fail, revise your implementation until all tests pass
|
||||
|
||||
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
|
||||
"""
|
||||
@@ -225,9 +210,9 @@ def get_config(
|
||||
)
|
||||
)
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=False,
|
||||
enable_browsing=RUN_WITH_BROWSING,
|
||||
enable_llm_editor=False,
|
||||
codeact_enable_jupyter=False,
|
||||
codeact_enable_browsing=RUN_WITH_BROWSING,
|
||||
codeact_enable_llm_editor=False,
|
||||
condenser=metadata.condenser_config,
|
||||
enable_prompt_extensions=False,
|
||||
)
|
||||
@@ -238,7 +223,6 @@ def get_config(
|
||||
def initialize_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required
|
||||
metadata: EvalMetadata,
|
||||
):
|
||||
"""Initialize the runtime for the agent.
|
||||
|
||||
@@ -578,7 +562,7 @@ def process_instance(
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
try:
|
||||
initialize_runtime(runtime, instance, metadata)
|
||||
initialize_runtime(runtime, instance)
|
||||
|
||||
message_action = get_instruction(instance, metadata)
|
||||
|
||||
@@ -864,7 +848,7 @@ if __name__ == '__main__':
|
||||
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
|
||||
if (
|
||||
instance['instance_id'] not in added_instance_ids
|
||||
and instance['test_result'].get('git_patch', '').strip()
|
||||
and instance['test_result']['git_patch'].strip()
|
||||
):
|
||||
fout.write(line)
|
||||
added_instance_ids.add(instance['instance_id'])
|
||||
|
||||
@@ -158,9 +158,9 @@ def get_config(
|
||||
)
|
||||
)
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=False,
|
||||
enable_browsing=RUN_WITH_BROWSING,
|
||||
enable_llm_editor=False,
|
||||
codeact_enable_jupyter=False,
|
||||
codeact_enable_browsing=RUN_WITH_BROWSING,
|
||||
codeact_enable_llm_editor=False,
|
||||
condenser=metadata.condenser_config,
|
||||
enable_prompt_extensions=False,
|
||||
)
|
||||
|
||||
@@ -62,9 +62,9 @@ def get_config(
|
||||
)
|
||||
)
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=True,
|
||||
enable_browsing=True,
|
||||
enable_llm_editor=False,
|
||||
codeact_enable_jupyter=True,
|
||||
codeact_enable_browsing=True,
|
||||
codeact_enable_llm_editor=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
@@ -11,12 +11,15 @@
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@tanstack/query/recommended",
|
||||
"plugin:@tanstack/query/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["prettier", "unused-imports"],
|
||||
"rules": {
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"prettier/prettier": ["error"],
|
||||
"prettier/prettier": [
|
||||
"error"
|
||||
],
|
||||
// Resolves https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions/59268871#59268871
|
||||
"import/extensions": [
|
||||
"error",
|
||||
@@ -24,26 +27,32 @@
|
||||
{
|
||||
"": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never",
|
||||
},
|
||||
],
|
||||
"tsx": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
// Allow state modification in reduce and Redux reducers
|
||||
"no-param-reassign": [
|
||||
"error",
|
||||
{
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": ["acc", "state"],
|
||||
},
|
||||
"ignorePropertyModificationsFor": [
|
||||
"acc",
|
||||
"state"
|
||||
]
|
||||
}
|
||||
],
|
||||
// For https://stackoverflow.com/questions/55844608/stuck-with-eslint-error-i-e-separately-loops-should-be-avoided-in-favor-of-arra
|
||||
"no-restricted-syntax": "off",
|
||||
@@ -57,19 +66,24 @@
|
||||
2,
|
||||
{
|
||||
"required": {
|
||||
"some": ["nesting", "id"],
|
||||
},
|
||||
},
|
||||
"some": [
|
||||
"nesting",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react/no-array-index-key": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": ["**/tsconfig.json"],
|
||||
},
|
||||
},
|
||||
],
|
||||
"project": [
|
||||
"**/tsconfig.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
cd frontend
|
||||
npm run check-unlocalized-strings
|
||||
npx lint-staged
|
||||
npm test
|
||||
@@ -37,4 +37,4 @@ describe("CopyToClipboardButton", () => {
|
||||
const button = screen.getByTestId("copy-to-clipboard");
|
||||
expect(button).toHaveAttribute("aria-label", "BUTTON$COPIED");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -217,17 +217,6 @@ describe("ChatInput", () => {
|
||||
expect(onImagePaste).toHaveBeenCalledWith([file]);
|
||||
});
|
||||
|
||||
it("should use the default maxRows value", () => {
|
||||
// We can't directly test the maxRows prop as it's not exposed in the DOM
|
||||
// Instead, we'll verify the component renders with the default props
|
||||
render(<ChatInput onSubmit={onSubmitMock} />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
|
||||
// The actual verification of maxRows=16 is handled internally by the TextareaAutosize component
|
||||
// and affects how many rows the textarea can expand to
|
||||
});
|
||||
|
||||
it("should not submit when Enter is pressed during IME composition", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ChatInput onSubmit={onSubmitMock} />);
|
||||
|
||||
@@ -76,11 +76,11 @@ describe("ConversationCard", () => {
|
||||
const card = screen.getByTestId("conversation-card");
|
||||
|
||||
within(card).getByText("Conversation 1");
|
||||
|
||||
|
||||
// Just check that the card contains the expected text content
|
||||
expect(card).toHaveTextContent("Created");
|
||||
expect(card).toHaveTextContent("ago");
|
||||
|
||||
|
||||
// Use a regex to match the time part since it might have whitespace
|
||||
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
|
||||
expect(card).toHaveTextContent(timeRegex);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Command, appendInput, appendOutput } from "#/state/command-slice";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
|
||||
const renderTerminal = (commands: Command[] = []) =>
|
||||
renderWithProviders(<Terminal />, {
|
||||
renderWithProviders(<Terminal secrets={[]} />, {
|
||||
preloadedState: {
|
||||
cmd: {
|
||||
commands,
|
||||
@@ -121,7 +121,7 @@ describe.skip("Terminal", () => {
|
||||
|
||||
// This test fails because it expects `disposeMock` to have been called before the component is unmounted.
|
||||
it.skip("should dispose the terminal on unmount", () => {
|
||||
const { unmount } = renderWithProviders(<Terminal />);
|
||||
const { unmount } = renderWithProviders(<Terminal secrets={[]} />);
|
||||
|
||||
expect(mockTerminal.dispose).not.toHaveBeenCalled();
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import { Command } from "#/state/command-slice";
|
||||
|
||||
interface TestTerminalComponentProps {
|
||||
commands: Command[];
|
||||
secrets: string[];
|
||||
}
|
||||
|
||||
function TestTerminalComponent({
|
||||
commands,
|
||||
secrets,
|
||||
}: TestTerminalComponentProps) {
|
||||
const ref = useTerminal({ commands });
|
||||
const ref = useTerminal({ commands, secrets, disabled: false });
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ describe("useTerminal", () => {
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
render(<TestTerminalComponent commands={[]} />, {
|
||||
render(<TestTerminalComponent commands={[]} secrets={[]} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
});
|
||||
@@ -66,7 +68,7 @@ describe("useTerminal", () => {
|
||||
{ content: "hello", type: "output" },
|
||||
];
|
||||
|
||||
render(<TestTerminalComponent commands={commands} />, {
|
||||
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
@@ -74,8 +76,7 @@ describe("useTerminal", () => {
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
|
||||
});
|
||||
|
||||
// This test is no longer relevant as secrets filtering has been removed
|
||||
it.skip("should hide secrets in the terminal", () => {
|
||||
it("should hide secrets in the terminal", () => {
|
||||
const secret = "super_secret_github_token";
|
||||
const anotherSecret = "super_secret_another_token";
|
||||
const commands: Command[] = [
|
||||
@@ -89,12 +90,20 @@ describe("useTerminal", () => {
|
||||
render(
|
||||
<TestTerminalComponent
|
||||
commands={commands}
|
||||
secrets={[secret, anotherSecret]}
|
||||
/>,
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
// This test is no longer relevant as secrets filtering has been removed
|
||||
// BUG: `vi.clearAllMocks()` does not clear the number of calls
|
||||
// therefore, we need to assume the order of the calls based
|
||||
// on the test order
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
`export GITHUB_TOKEN=${"*".repeat(10)},${"*".repeat(10)},${"*".repeat(10)}`,
|
||||
);
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(4, "*".repeat(10));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { handleObservationMessage } from "#/services/observations";
|
||||
import store from "#/store";
|
||||
import { ObservationMessage } from "#/types/message";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("#/store", () => ({
|
||||
default: {
|
||||
dispatch: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Observations Service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("handleObservationMessage", () => {
|
||||
const createErrorMessage = (): ObservationMessage => ({
|
||||
id: 14,
|
||||
timestamp: "2025-04-14T13:37:54.451843",
|
||||
message: "The action has not been executed.",
|
||||
cause: 12,
|
||||
observation: "error",
|
||||
content: "The action has not been executed.",
|
||||
extras: {
|
||||
error_id: "",
|
||||
metadata: {},
|
||||
},
|
||||
});
|
||||
|
||||
it("should dispatch error messages exactly once", () => {
|
||||
const errorMessage = createErrorMessage();
|
||||
|
||||
handleObservationMessage(errorMessage);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.dispatch).toHaveBeenCalledWith({
|
||||
type: "chat/addAssistantObservation",
|
||||
payload: expect.objectContaining({
|
||||
observation: "error",
|
||||
content: "The action has not been executed.",
|
||||
source: "user",
|
||||
extras: {
|
||||
error_id: "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
21
frontend/package-lock.json
generated
21
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.33.0",
|
||||
"version": "0.32.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.33.0",
|
||||
"version": "0.32.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.7.6",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
@@ -83,7 +83,6 @@
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"lint-staged": "^15.5.0",
|
||||
@@ -9174,22 +9173,6 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unused-imports": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
|
||||
"integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
|
||||
"eslint": "^9.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.33.0",
|
||||
"version": "0.32.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -53,7 +53,7 @@
|
||||
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
|
||||
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=false react-router dev",
|
||||
"dev:mock:saas": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=true react-router dev",
|
||||
"build": "npm run make-i18n && react-router build",
|
||||
"build": "npm run make-i18n && npm run typecheck && react-router build",
|
||||
"start": "npx sirv-cli build/ --single",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
@@ -62,7 +62,7 @@
|
||||
"preview": "vite preview",
|
||||
"make-i18n": "node scripts/make-i18n-translations.cjs",
|
||||
"prelint": "npm run make-i18n",
|
||||
"lint": "npm run typecheck && eslint src --ext .ts,.tsx,.js && prettier --check src/**/*.{ts,tsx}",
|
||||
"lint": "eslint src --ext .ts,.tsx,.js && prettier --check src/**/*.{ts,tsx}",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx,.js --fix && prettier --write src/**/*.{ts,tsx}",
|
||||
"prepare": "cd .. && husky frontend/.husky",
|
||||
"typecheck": "react-router typegen && tsc",
|
||||
@@ -107,7 +107,6 @@
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.0.0",
|
||||
"lint-staged": "^15.5.0",
|
||||
|
||||
@@ -47,7 +47,6 @@ const SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
||||
// Attributes that typically don't contain user-facing text
|
||||
const NON_TEXT_ATTRIBUTES = [
|
||||
"className",
|
||||
"i18nKey",
|
||||
"testId",
|
||||
"id",
|
||||
"name",
|
||||
@@ -686,23 +685,23 @@ function scanDirectoryForUnlocalizedStrings(dirPath) {
|
||||
try {
|
||||
const srcPath = path.resolve(__dirname, '../src');
|
||||
console.log('Checking for unlocalized strings in frontend code...');
|
||||
|
||||
|
||||
// Get unlocalized strings using the AST scanner
|
||||
const results = scanDirectoryForUnlocalizedStrings(srcPath);
|
||||
|
||||
|
||||
// If we found any unlocalized strings, format them for output and exit with error
|
||||
if (results.size > 0) {
|
||||
const formattedResults = Array.from(results.entries())
|
||||
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
|
||||
.join('\n');
|
||||
|
||||
|
||||
console.error(`Error: Found unlocalized strings in the following files:${formattedResults}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ No unlocalized strings found in frontend code.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error running unlocalized strings check:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
22
frontend/src/assets/cmd-line.tsx
Normal file
22
frontend/src/assets/cmd-line.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
function CmdLine() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CmdLine;
|
||||
@@ -29,7 +29,7 @@ export function ChatInput({
|
||||
disabled,
|
||||
showButton = true,
|
||||
value,
|
||||
maxRows = 16,
|
||||
maxRows = 4,
|
||||
onSubmit,
|
||||
onStop,
|
||||
onChange,
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ChatMessage({
|
||||
"rounded-xl relative",
|
||||
"flex flex-col gap-2",
|
||||
type === "user" && " max-w-[305px] p-4 bg-tertiary self-end",
|
||||
type === "assistant" && "mt-6 max-w-full bg-transparent",
|
||||
type === "assistant" && "mt-6 max-w-full bg-tranparent",
|
||||
)}
|
||||
>
|
||||
<CopyToClipboardButton
|
||||
|
||||
@@ -47,7 +47,7 @@ export function AgentStatusBar() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (message.trim()) {
|
||||
if (curAgentState === AgentState.LOADING && message.trim()) {
|
||||
setStatusMessage(message);
|
||||
} else {
|
||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function TerminalStatusLabel() {
|
||||
const { t } = useTranslation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
curAgentState === AgentState.LOADING ||
|
||||
curAgentState === AgentState.STOPPED
|
||||
? "bg-red-500 animate-pulse"
|
||||
: "bg-green-500",
|
||||
)}
|
||||
/>
|
||||
{t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,16 +2,24 @@ import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
|
||||
function Terminal() {
|
||||
interface TerminalProps {
|
||||
secrets: string[];
|
||||
}
|
||||
|
||||
function Terminal({ secrets }: TerminalProps) {
|
||||
const { commands } = useSelector((state: RootState) => state.cmd);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const ref = useTerminal({
|
||||
commands,
|
||||
secrets,
|
||||
disabled: RUNTIME_INACTIVE_STATES.includes(curAgentState),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full p-2 min-h-0 flex-grow">
|
||||
<div className="h-full p-2 min-h-0">
|
||||
<div ref={ref} className="h-full w-full" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Container({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-base-secondary border border-neutral-600 rounded-xl flex flex-col h-full",
|
||||
"bg-base-secondary border border-neutral-600 rounded-xl flex flex-col",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -39,7 +39,7 @@ export function Container({
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-hidden flex-grow rounded-b-xl">{children}</div>
|
||||
<div className="overflow-hidden h-full rounded-b-xl">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function NavTab({ to, label, icon, isBeta }: NavTabProps) {
|
||||
className={cn(
|
||||
"px-2 border-b border-r border-neutral-600 bg-base-secondary flex-1",
|
||||
"first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0",
|
||||
"flex items-center gap-2 h-full min-h-[36px]",
|
||||
"flex items-center gap-2",
|
||||
)}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
|
||||
@@ -2,7 +2,9 @@ import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import React from "react";
|
||||
import { Command } from "#/state/command-slice";
|
||||
import { getTerminalCommand } from "#/services/terminal-service";
|
||||
import { parseTerminalOutput } from "#/utils/parse-terminal-output";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
|
||||
/*
|
||||
NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component.
|
||||
@@ -11,40 +13,27 @@ import { parseTerminalOutput } from "#/utils/parse-terminal-output";
|
||||
|
||||
interface UseTerminalConfig {
|
||||
commands: Command[];
|
||||
secrets: string[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_TERMINAL_CONFIG: UseTerminalConfig = {
|
||||
commands: [],
|
||||
secrets: [],
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const renderCommand = (command: Command, terminal: Terminal) => {
|
||||
const { content, type } = command;
|
||||
|
||||
if (type === "input") {
|
||||
terminal.write("$ ");
|
||||
terminal.writeln(
|
||||
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
|
||||
);
|
||||
} else {
|
||||
terminal.write(`\n`);
|
||||
terminal.writeln(
|
||||
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
|
||||
);
|
||||
terminal.write(`\n`);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a persistent reference that survives component unmounts
|
||||
// This ensures terminal history is preserved when navigating away and back
|
||||
const persistentLastCommandIndex = { current: 0 };
|
||||
|
||||
export const useTerminal = ({
|
||||
commands,
|
||||
secrets,
|
||||
disabled,
|
||||
}: UseTerminalConfig = DEFAULT_TERMINAL_CONFIG) => {
|
||||
const { send } = useWsClient();
|
||||
const terminal = React.useRef<Terminal | null>(null);
|
||||
const fitAddon = React.useRef<FitAddon | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const lastCommandIndex = persistentLastCommandIndex; // Use the persistent reference
|
||||
const lastCommandIndex = React.useRef(0);
|
||||
const keyEventDisposable = React.useRef<{ dispose: () => void } | null>(null);
|
||||
|
||||
const createTerminal = () =>
|
||||
new Terminal({
|
||||
@@ -62,57 +51,153 @@ export const useTerminal = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize terminal and handle cleanup
|
||||
const copySelection = (selection: string) => {
|
||||
const clipboardItem = new ClipboardItem({
|
||||
"text/plain": new Blob([selection], { type: "text/plain" }),
|
||||
});
|
||||
|
||||
navigator.clipboard.write([clipboardItem]);
|
||||
};
|
||||
|
||||
const pasteSelection = (callback: (text: string) => void) => {
|
||||
navigator.clipboard.readText().then(callback);
|
||||
};
|
||||
|
||||
const pasteHandler = (event: KeyboardEvent, cb: (text: string) => void) => {
|
||||
const isControlOrMetaPressed =
|
||||
event.type === "keydown" && (event.ctrlKey || event.metaKey);
|
||||
|
||||
if (isControlOrMetaPressed) {
|
||||
if (event.code === "KeyV") {
|
||||
pasteSelection((text: string) => {
|
||||
terminal.current?.write(text);
|
||||
cb(text);
|
||||
});
|
||||
}
|
||||
|
||||
if (event.code === "KeyC") {
|
||||
const selection = terminal.current?.getSelection();
|
||||
if (selection) copySelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEnter = (command: string) => {
|
||||
terminal.current?.write("\r\n");
|
||||
send(getTerminalCommand(command));
|
||||
};
|
||||
|
||||
const handleBackspace = (command: string) => {
|
||||
terminal.current?.write("\b \b");
|
||||
return command.slice(0, -1);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
/* Create a new terminal instance */
|
||||
terminal.current = createTerminal();
|
||||
fitAddon.current = new FitAddon();
|
||||
|
||||
if (ref.current) {
|
||||
initializeTerminal();
|
||||
// Render all commands in array
|
||||
// This happens when we just switch to Terminal from other tabs
|
||||
if (commands.length > 0) {
|
||||
for (let i = 0; i < commands.length; i += 1) {
|
||||
renderCommand(commands[i], terminal.current);
|
||||
}
|
||||
lastCommandIndex.current = commands.length;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
terminal.current?.dispose();
|
||||
};
|
||||
}, [commands]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Render new commands when they are added to the commands array
|
||||
if (
|
||||
terminal.current &&
|
||||
commands.length > 0 &&
|
||||
lastCommandIndex.current < commands.length
|
||||
) {
|
||||
for (let i = lastCommandIndex.current; i < commands.length; i += 1) {
|
||||
renderCommand(commands[i], terminal.current);
|
||||
}
|
||||
lastCommandIndex.current = commands.length;
|
||||
}
|
||||
}, [commands]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.current?.fit();
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
/* Initialize the terminal in the DOM */
|
||||
initializeTerminal();
|
||||
terminal.current.write("$ ");
|
||||
|
||||
/* Listen for resize events */
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.current?.fit();
|
||||
});
|
||||
resizeObserver.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
terminal.current?.dispose();
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
/* Write commands to the terminal */
|
||||
if (terminal.current && commands.length > 0) {
|
||||
// Start writing commands from the last command index
|
||||
for (let i = lastCommandIndex.current; i < commands.length; i += 1) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { content, type } = commands[i];
|
||||
|
||||
secrets.forEach((secret) => {
|
||||
content = content.replaceAll(secret, "*".repeat(10));
|
||||
});
|
||||
|
||||
terminal.current?.writeln(
|
||||
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
|
||||
);
|
||||
|
||||
if (type === "output") {
|
||||
terminal.current.write(`\n$ `);
|
||||
}
|
||||
}
|
||||
|
||||
lastCommandIndex.current = commands.length; // Update the position of the last command
|
||||
}
|
||||
}, [commands]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (terminal.current) {
|
||||
// Dispose of existing listeners if they exist
|
||||
if (keyEventDisposable.current) {
|
||||
keyEventDisposable.current.dispose();
|
||||
keyEventDisposable.current = null;
|
||||
}
|
||||
|
||||
let commandBuffer = "";
|
||||
|
||||
if (!disabled) {
|
||||
// Add new key event listener and store the disposable
|
||||
keyEventDisposable.current = terminal.current.onKey(
|
||||
({ key, domEvent }) => {
|
||||
if (domEvent.key === "Enter") {
|
||||
handleEnter(commandBuffer);
|
||||
commandBuffer = "";
|
||||
} else if (domEvent.key === "Backspace") {
|
||||
if (commandBuffer.length > 0) {
|
||||
commandBuffer = handleBackspace(commandBuffer);
|
||||
}
|
||||
} else {
|
||||
// Ignore paste event
|
||||
if (key.charCodeAt(0) === 22) {
|
||||
return;
|
||||
}
|
||||
commandBuffer += key;
|
||||
terminal.current?.write(key);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add custom key handler and store the disposable
|
||||
terminal.current.attachCustomKeyEventHandler((event) =>
|
||||
pasteHandler(event, (text) => {
|
||||
commandBuffer += text;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Add a noop handler when disabled
|
||||
keyEventDisposable.current = terminal.current.onKey((e) => {
|
||||
e.domEvent.preventDefault();
|
||||
e.domEvent.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (keyEventDisposable.current) {
|
||||
keyEventDisposable.current.dispose();
|
||||
keyEventDisposable.current = null;
|
||||
}
|
||||
};
|
||||
}, [disabled]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
@@ -4961,19 +4961,19 @@
|
||||
"tr": "Gezinme tamamlandı"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$RECALL": {
|
||||
"en": "Microagent Activated",
|
||||
"en": "MicroAgent Activated",
|
||||
"ja": "マイクロエージェントが有効化されました",
|
||||
"zh-CN": "微代理已激活",
|
||||
"zh-TW": "微代理已啟動",
|
||||
"ko-KR": "마이크로에이전트 활성화됨",
|
||||
"no": "MikroAgent aktivert",
|
||||
"it": "Microagent attivato",
|
||||
"pt": "Microagent ativado",
|
||||
"es": "Microagent activado",
|
||||
"it": "MicroAgent attivato",
|
||||
"pt": "MicroAgent ativado",
|
||||
"es": "MicroAgent activado",
|
||||
"ar": "تم تنشيط الوكيل المصغر",
|
||||
"fr": "Microagent activé",
|
||||
"fr": "MicroAgent activé",
|
||||
"tr": "MikroAjan Etkinleştirildi",
|
||||
"de": "Microagent aktiviert"
|
||||
"de": "MicroAgent aktiviert"
|
||||
},
|
||||
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
|
||||
"en": "Show details",
|
||||
@@ -6089,4 +6089,4 @@
|
||||
"tr": "belgelendirme",
|
||||
"de": "Dokumentation"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="scale(0.85) translate(1.8, 1.8)">
|
||||
<path d="M19.5 1.5C18.67 1.5 18 2.17 18 3C18 3.83 18.67 4.5 19.5 4.5C20.33 4.5 21 3.83 21 3C21 2.17 20.33 1.5 19.5 1.5Z" fill="currentColor"/>
|
||||
<path d="M12 18C8.5 18 5.5 16.8 4 15C4 18.3137 7.13401 21 12 21C16.866 21 20 18.3137 20 15C18.5 16.8 15.5 18 12 18Z" fill="currentColor"/>
|
||||
<path d="M12 6C15.5 6 18.5 7.2 20 9C20 5.68629 16.866 3 12 3C7.13401 3 4 5.68629 4 9C5.5 7.2 8.5 6 12 6Z" fill="currentColor"/>
|
||||
<path d="M7.5 21C6.67 21 6 21.67 6 22.5C6 23.33 6.67 24 7.5 24C8.33 24 9 23.33 9 22.5C9 21.67 8.33 21 7.5 21Z" fill="currentColor"/>
|
||||
<path d="M4.5 5.5C3.67 5.5 3 4.83 3 4C3 3.17 3.67 2.5 4.5 2.5C5.33 2.5 6 3.17 6 4C6 4.83 5.33 5.5 4.5 5.5Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 853 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 5C5.44772 5 5 5.44772 5 6V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V6C19 5.44772 18.5523 5 18 5H6ZM6 6H18V18H6V6Z" fill="currentColor"/>
|
||||
<path d="M8.14645 9.64645C7.95118 9.84171 7.95118 10.1583 8.14645 10.3536L10.7929 13L8.14645 15.6464C7.95118 15.8417 7.95118 16.1583 8.14645 16.3536C8.34171 16.5488 8.65829 16.5488 8.85355 16.3536L11.8536 13.3536C12.0488 13.1583 12.0488 12.8417 11.8536 12.6464L8.85355 9.64645C8.65829 9.45118 8.34171 9.45118 8.14645 9.64645Z" fill="currentColor"/>
|
||||
<path d="M13 16C12.7239 16 12.5 16.2239 12.5 16.5C12.5 16.7761 12.7239 17 13 17H16C16.2761 17 16.5 16.7761 16.5 16.5C16.5 16.2239 16.2761 16 16 16H13Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 799 B |
@@ -17,7 +17,6 @@ export default [
|
||||
route("browser", "routes/browser-tab.tsx"),
|
||||
route("jupyter", "routes/jupyter-tab.tsx"),
|
||||
route("served", "routes/served-tab.tsx"),
|
||||
route("terminal", "routes/terminal-tab.tsx"),
|
||||
]),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
@@ -124,7 +124,7 @@ function AccountSettings() {
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString().trim() || "";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString() || "";
|
||||
const inputApiKey = formData.get("llm-api-key-input")?.toString() || "";
|
||||
const llmApiKey =
|
||||
inputApiKey === "" && isLLMKeySet
|
||||
|
||||
@@ -15,8 +15,7 @@ import { clearTerminal } from "#/state/command-slice";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
import CodeIcon from "#/icons/code.svg?react";
|
||||
import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import JupyterIcon from "#/icons/jupyter.svg?react";
|
||||
import TerminalIcon from "#/icons/terminal.svg?react";
|
||||
import ListIcon from "#/icons/list-type-number.svg?react";
|
||||
import { clearJupyter } from "#/state/jupyter-slice";
|
||||
import { FilesProvider } from "#/context/files";
|
||||
import { ChatInterface } from "../components/features/chat/chat-interface";
|
||||
@@ -32,6 +31,7 @@ import Security from "#/components/shared/modals/security/security";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { ServedAppLabel } from "#/components/layout/served-app-label";
|
||||
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
@@ -57,6 +57,17 @@ function AppContent() {
|
||||
|
||||
const [width, setWidth] = React.useState(window.innerWidth);
|
||||
|
||||
const secrets = React.useMemo(
|
||||
// secrets to filter go here
|
||||
() => [].filter((secret) => secret !== null),
|
||||
[],
|
||||
);
|
||||
|
||||
const Terminal = React.useMemo(
|
||||
() => React.lazy(() => import("#/components/features/terminal/terminal")),
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFetched && !conversation) {
|
||||
displayErrorToast(
|
||||
@@ -110,7 +121,7 @@ function AppContent() {
|
||||
function renderMain() {
|
||||
if (width <= 640) {
|
||||
return (
|
||||
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full bg-base-secondary">
|
||||
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full">
|
||||
<ChatInterface />
|
||||
</div>
|
||||
);
|
||||
@@ -124,40 +135,56 @@ function AppContent() {
|
||||
secondClassName="flex flex-col overflow-hidden"
|
||||
firstChild={<ChatInterface />}
|
||||
secondChild={
|
||||
<Container
|
||||
className="h-full w-full"
|
||||
labels={[
|
||||
{
|
||||
label: t(I18nKey.WORKSPACE$TITLE),
|
||||
to: "",
|
||||
icon: <CodeIcon />,
|
||||
},
|
||||
{
|
||||
label: t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL),
|
||||
to: "terminal",
|
||||
icon: <TerminalIcon />,
|
||||
},
|
||||
{ label: "Jupyter", to: "jupyter", icon: <JupyterIcon /> },
|
||||
{
|
||||
label: <ServedAppLabel />,
|
||||
to: "served",
|
||||
icon: <FaServer />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
{t(I18nKey.BROWSER$TITLE)}
|
||||
</div>
|
||||
),
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
<ResizablePanel
|
||||
orientation={Orientation.VERTICAL}
|
||||
className="grow h-full min-h-0 min-w-0"
|
||||
initialSize={500}
|
||||
firstClassName="rounded-xl overflow-hidden border border-neutral-600"
|
||||
secondClassName="flex flex-col overflow-hidden"
|
||||
firstChild={
|
||||
<Container
|
||||
className="h-full"
|
||||
labels={[
|
||||
{
|
||||
label: t(I18nKey.WORKSPACE$TITLE),
|
||||
to: "",
|
||||
icon: <CodeIcon />,
|
||||
},
|
||||
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
|
||||
{
|
||||
label: <ServedAppLabel />,
|
||||
to: "served",
|
||||
icon: <FaServer />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-1">
|
||||
{t(I18nKey.BROWSER$TITLE)}
|
||||
</div>
|
||||
),
|
||||
to: "browser",
|
||||
icon: <GlobeIcon />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FilesProvider>
|
||||
<Outlet />
|
||||
</FilesProvider>
|
||||
</Container>
|
||||
}
|
||||
secondChild={
|
||||
<Container
|
||||
className="h-full overflow-scroll"
|
||||
label={<TerminalStatusLabel />}
|
||||
>
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal secrets={secrets} />
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function TerminalTab() {
|
||||
const Terminal = React.useMemo(
|
||||
() => React.lazy(() => import("#/components/features/terminal/terminal")),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-grow overflow-auto">
|
||||
{/* Terminal uses some API that is not compatible in a server-environment. For this reason, we lazy load it to ensure
|
||||
* that it loads only in the client-side. */}
|
||||
<React.Suspense fallback={<div className="h-full" />}>
|
||||
<Terminal />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TerminalTab;
|
||||
@@ -104,12 +104,7 @@ export function handleActionMessage(message: ActionMessage) {
|
||||
}
|
||||
|
||||
if (message.source === "agent") {
|
||||
// Only add thought as a message if it's not a "think" action
|
||||
if (
|
||||
message.args &&
|
||||
message.args.thought &&
|
||||
message.action !== ActionType.THINK
|
||||
) {
|
||||
if (message.args && message.args.thought) {
|
||||
store.dispatch(addAssistantMessage(message.args.thought));
|
||||
}
|
||||
// Need to convert ActionMessage to RejectAction
|
||||
|
||||
@@ -52,7 +52,6 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
case ObservationType.THINK:
|
||||
case ObservationType.NULL:
|
||||
case ObservationType.RECALL:
|
||||
case ObservationType.ERROR:
|
||||
break; // We don't display the default message for these observations
|
||||
default:
|
||||
store.dispatch(addAssistantMessage(message.message));
|
||||
|
||||
@@ -24,7 +24,6 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
|
||||
"browse_interactive",
|
||||
"edit",
|
||||
"recall",
|
||||
"think",
|
||||
];
|
||||
|
||||
function getRiskText(risk: ActionSecurityRisk) {
|
||||
|
||||
@@ -32,9 +32,6 @@ enum ObservationType {
|
||||
// An observation that shows agent's context extension
|
||||
RECALL = "recall",
|
||||
|
||||
// An error observation
|
||||
ERROR = "error",
|
||||
|
||||
// A no-op observation
|
||||
NULL = "null",
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { downloadJSON } from "./download-json";
|
||||
|
||||
function isSaveFilePickerSupported(): boolean {
|
||||
return typeof window !== "undefined" && "showSaveFilePicker" in window;
|
||||
}
|
||||
@@ -8,37 +6,25 @@ export async function downloadTrajectory(
|
||||
conversationId: string,
|
||||
data: unknown[] | null,
|
||||
): Promise<void> {
|
||||
// Ensure data is an object for downloadJSON
|
||||
const jsonData = data || {};
|
||||
|
||||
if (!isSaveFilePickerSupported()) {
|
||||
// Fallback for browsers that don't support File System Access API (Safari, Firefox)
|
||||
// This method dumps the JSON data right into the download folder
|
||||
downloadJSON(jsonData as object, `trajectory-${conversationId}.json`);
|
||||
return;
|
||||
throw new Error(
|
||||
"Your browser doesn't support downloading files. Please use Chrome, Edge, or another browser that supports the File System Access API.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const options: SaveFilePickerOptions = {
|
||||
suggestedName: `trajectory-${conversationId}.json`,
|
||||
types: [
|
||||
{
|
||||
description: "JSON File", // This is a file type description, not user-facing text
|
||||
accept: {
|
||||
"application/json": [".json"],
|
||||
},
|
||||
const options: SaveFilePickerOptions = {
|
||||
suggestedName: `trajectory-${conversationId}.json`,
|
||||
types: [
|
||||
{
|
||||
description: "JSON File", // This is a file type description, not user-facing text
|
||||
accept: {
|
||||
"application/json": [".json"],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fileHandle = await window.showSaveFilePicker(options);
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(JSON.stringify(data, null, 2));
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
// If an error occurs, fall back to the downloadJSON method
|
||||
if (error instanceof Error && error.name !== "AbortError") {
|
||||
downloadJSON(jsonData as object, `trajectory-${conversationId}.json`);
|
||||
}
|
||||
}
|
||||
const fileHandle = await window.showSaveFilePicker(options);
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(JSON.stringify(data, null, 2));
|
||||
await writable.close();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenHands Microagents
|
||||
# OpenHands MicroAgents
|
||||
|
||||
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They help developers by providing expert guidance, automating common tasks, and ensuring consistent practices across projects. Each microagent is designed to excel in a specific area, from Git operations to code review processes.
|
||||
MicroAgents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They help developers by providing expert guidance, automating common tasks, and ensuring consistent practices across projects. Each microagent is designed to excel in a specific area, from Git operations to code review processes.
|
||||
|
||||
## Sources of Microagents
|
||||
|
||||
@@ -49,7 +49,7 @@ When OpenHands works with a repository, it:
|
||||
2. Loads relevant knowledge agents based on keywords in conversations
|
||||
3. Enable task agent if user select one of them
|
||||
|
||||
## Types of Microagents
|
||||
## Types of MicroAgents
|
||||
|
||||
Most microagents use markdown files with YAML frontmatter. For repository agents (repo.md), the frontmatter is optional - if not provided, the file will be loaded with default settings as a repository agent.
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a pull request, send the user a short message with a link to the pull request.
|
||||
* Prefer "Draft" pull requests when possible
|
||||
* Do NOT mark a pull request as ready to review unless the user explicitly says so
|
||||
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
|
||||
@@ -30,6 +30,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
||||
curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/merge_requests" \
|
||||
-H "Authorization: Bearer $GITLAB_TOKEN" \
|
||||
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
||||
-d '{"source_branch": "create-widget", "target_branch": "openhands-workspace", "title": "Create widget"}'
|
||||
```
|
||||
|
||||
@@ -53,9 +53,9 @@ The agent provides several built-in tools:
|
||||
## Configuration
|
||||
|
||||
Tools can be enabled/disabled through configuration parameters:
|
||||
- `enable_browsing`: Enable browser interaction tools
|
||||
- `enable_jupyter`: Enable IPython code execution
|
||||
- `enable_llm_editor`: Enable LLM-based file editing (falls back to string replacement editor if disabled)
|
||||
- `codeact_enable_browsing`: Enable browser interaction tools
|
||||
- `codeact_enable_jupyter`: Enable IPython code execution
|
||||
- `codeact_enable_llm_editor`: Enable LLM-based file editing (falls back to string replacement editor if disabled)
|
||||
|
||||
## Micro-agents
|
||||
|
||||
|
||||
@@ -62,21 +62,21 @@ class CodeActAgent(Agent):
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
- config (AgentConfig): The configuration for this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
self.pending_actions: deque[Action] = deque()
|
||||
self.reset()
|
||||
|
||||
built_in_tools = codeact_function_calling.get_tools(
|
||||
enable_browsing=self.config.enable_browsing,
|
||||
enable_jupyter=self.config.enable_jupyter,
|
||||
enable_llm_editor=self.config.enable_llm_editor,
|
||||
# Retrieve the enabled tools
|
||||
self.tools = codeact_function_calling.get_tools(
|
||||
codeact_enable_browsing=self.config.codeact_enable_browsing,
|
||||
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
|
||||
codeact_enable_llm_editor=self.config.codeact_enable_llm_editor,
|
||||
llm=self.llm,
|
||||
)
|
||||
|
||||
self.tools = built_in_tools
|
||||
|
||||
logger.debug(
|
||||
f"TOOLS loaded for CodeActAgent: {', '.join([tool.get('function').get('name') for tool in self.tools])}"
|
||||
)
|
||||
self.prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
)
|
||||
@@ -137,23 +137,10 @@ class CodeActAgent(Agent):
|
||||
'messages': self.llm.format_messages_for_llm(messages),
|
||||
}
|
||||
params['tools'] = self.tools
|
||||
|
||||
if self.mcp_tools:
|
||||
# Only add tools with unique names
|
||||
existing_names = {tool['function']['name'] for tool in params['tools']}
|
||||
unique_mcp_tools = [
|
||||
tool
|
||||
for tool in self.mcp_tools
|
||||
if tool['function']['name'] not in existing_names
|
||||
]
|
||||
params['tools'] += unique_mcp_tools
|
||||
|
||||
# log to litellm proxy if possible
|
||||
params['extra_body'] = {'metadata': state.to_llm_metadata(agent_name=self.name)}
|
||||
response = self.llm.completion(**params)
|
||||
logger.debug(f'Response from LLM: {response}')
|
||||
actions = codeact_function_calling.response_to_actions(response)
|
||||
logger.debug(f'Actions after response_to_actions: {actions}')
|
||||
for action in actions:
|
||||
self.pending_actions.append(action)
|
||||
return self.pending_actions.popleft()
|
||||
|
||||
@@ -24,7 +24,6 @@ from openhands.core.exceptions import (
|
||||
FunctionCallNotExistsError,
|
||||
FunctionCallValidationError,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AgentDelegateAction,
|
||||
@@ -38,11 +37,9 @@ from openhands.events.action import (
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.action.mcp import McpAction
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
from openhands.llm import LLM
|
||||
from openhands.mcp import MCPClientTool
|
||||
|
||||
|
||||
def combine_thought(action: Action, thought: str) -> Action:
|
||||
@@ -73,7 +70,6 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
# Process each tool call to OpenHands action
|
||||
for i, tool_call in enumerate(assistant_msg.tool_calls):
|
||||
action: Action
|
||||
logger.debug(f'Tool call in function_calling.py: {tool_call}')
|
||||
try:
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
@@ -195,15 +191,6 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
f'Missing required argument "url" in tool call {tool_call.function.name}'
|
||||
)
|
||||
action = BrowseURLAction(url=arguments['url'])
|
||||
|
||||
# ================================================
|
||||
# McpAction (MCP)
|
||||
# ================================================
|
||||
elif tool_call.function.name.endswith(MCPClientTool.postfix()):
|
||||
action = McpAction(
|
||||
name=tool_call.function.name.rstrip(MCPClientTool.postfix()),
|
||||
arguments=tool_call.function.arguments,
|
||||
)
|
||||
else:
|
||||
raise FunctionCallNotExistsError(
|
||||
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
|
||||
@@ -240,9 +227,9 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
|
||||
|
||||
|
||||
def get_tools(
|
||||
enable_browsing: bool = False,
|
||||
enable_llm_editor: bool = False,
|
||||
enable_jupyter: bool = False,
|
||||
codeact_enable_browsing: bool = False,
|
||||
codeact_enable_llm_editor: bool = False,
|
||||
codeact_enable_jupyter: bool = False,
|
||||
llm: LLM | None = None,
|
||||
) -> list[ChatCompletionToolParam]:
|
||||
SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
|
||||
@@ -259,12 +246,12 @@ def get_tools(
|
||||
ThinkTool,
|
||||
FinishTool,
|
||||
]
|
||||
if enable_browsing:
|
||||
if codeact_enable_browsing:
|
||||
tools.append(WebReadTool)
|
||||
tools.append(BrowserTool)
|
||||
if enable_jupyter:
|
||||
if codeact_enable_jupyter:
|
||||
tools.append(IPythonTool)
|
||||
if enable_llm_editor:
|
||||
if codeact_enable_llm_editor:
|
||||
tools.append(LLMBasedFileEditTool)
|
||||
else:
|
||||
tools.append(
|
||||
|
||||
@@ -37,7 +37,6 @@ class Agent(ABC):
|
||||
self.config = config
|
||||
self._complete = False
|
||||
self.prompt_manager: 'PromptManager' | None = None
|
||||
self.mcp_tools: list[dict] = []
|
||||
|
||||
@property
|
||||
def complete(self) -> bool:
|
||||
@@ -112,11 +111,3 @@ class Agent(ABC):
|
||||
if not bool(cls._registry):
|
||||
raise AgentNotRegisteredError()
|
||||
return list(cls._registry.keys())
|
||||
|
||||
def set_mcp_tools(self, mcp_tools: list[dict]) -> None:
|
||||
"""Sets the list of MCP tools for the agent.
|
||||
|
||||
Args:
|
||||
- mcp_tools (list[dict]): The list of MCP tools.
|
||||
"""
|
||||
self.mcp_tools = mcp_tools
|
||||
|
||||
@@ -66,7 +66,7 @@ from openhands.events.observation import (
|
||||
)
|
||||
from openhands.events.serialization.event import event_to_trajectory, truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.llm.metrics import Metrics, TokenUsage
|
||||
|
||||
# note: RESUME is only available on web GUI
|
||||
TRAFFIC_CONTROL_REMINDER = (
|
||||
@@ -1114,38 +1114,36 @@ class AgentController:
|
||||
|
||||
To avoid performance issues with long conversations, we only keep:
|
||||
- accumulated_cost: The current total cost
|
||||
- accumulated_token_usage: Accumulated token statistics across all API calls
|
||||
- latest token_usage: Token statistics from the most recent API call
|
||||
|
||||
Args:
|
||||
action: The action to attach metrics to
|
||||
"""
|
||||
# Create a minimal metrics object with just what the frontend needs
|
||||
metrics = Metrics(model_name=self.agent.llm.metrics.model_name)
|
||||
metrics.accumulated_cost = self.agent.llm.metrics.accumulated_cost
|
||||
metrics._accumulated_token_usage = (
|
||||
self.agent.llm.metrics.accumulated_token_usage
|
||||
)
|
||||
|
||||
action.llm_metrics = metrics
|
||||
|
||||
# Log the metrics information for debugging
|
||||
# Get the latest usage directly from the agent's metrics
|
||||
latest_usage = None
|
||||
if self.agent.llm.metrics.token_usages:
|
||||
latest_usage = self.agent.llm.metrics.token_usages[-1]
|
||||
metrics.add_token_usage(
|
||||
prompt_tokens=latest_usage.prompt_tokens,
|
||||
completion_tokens=latest_usage.completion_tokens,
|
||||
cache_read_tokens=latest_usage.cache_read_tokens,
|
||||
cache_write_tokens=latest_usage.cache_write_tokens,
|
||||
response_id=latest_usage.response_id,
|
||||
)
|
||||
action.llm_metrics = metrics
|
||||
|
||||
accumulated_usage = self.agent.llm.metrics.accumulated_token_usage
|
||||
# Log the metrics information for frontend display
|
||||
log_usage: TokenUsage | None = (
|
||||
metrics.token_usages[-1] if metrics.token_usages else None
|
||||
)
|
||||
self.log(
|
||||
'debug',
|
||||
f'Action metrics - accumulated_cost: {metrics.accumulated_cost}, '
|
||||
f'latest tokens (prompt/completion/cache_read/cache_write): '
|
||||
f'{latest_usage.prompt_tokens if latest_usage else 0}/'
|
||||
f'{latest_usage.completion_tokens if latest_usage else 0}/'
|
||||
f'{latest_usage.cache_read_tokens if latest_usage else 0}/'
|
||||
f'{latest_usage.cache_write_tokens if latest_usage else 0}, '
|
||||
f'accumulated tokens (prompt/completion): '
|
||||
f'{accumulated_usage.prompt_tokens}/'
|
||||
f'{accumulated_usage.completion_tokens}',
|
||||
f'tokens (prompt/completion/cache_read/cache_write): '
|
||||
f'{log_usage.prompt_tokens if log_usage else 0}/'
|
||||
f'{log_usage.completion_tokens if log_usage else 0}/'
|
||||
f'{log_usage.cache_read_tokens if log_usage else 0}/'
|
||||
f'{log_usage.cache_write_tokens if log_usage else 0}',
|
||||
extra={'msg_type': 'METRICS'},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
import toml
|
||||
from prompt_toolkit import PromptSession, print_formatted_text
|
||||
from prompt_toolkit.application import Application
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.formatted_text import HTML, FormattedText
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.layout.containers import HSplit, Window
|
||||
from prompt_toolkit.layout.controls import FormattedTextControl
|
||||
from prompt_toolkit.layout.layout import Layout
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.shortcuts import clear, print_container
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
|
||||
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
|
||||
from openhands import __version__
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
parse_arguments,
|
||||
@@ -51,176 +37,88 @@ from openhands.events.observation import (
|
||||
AgentStateChangedObservation,
|
||||
CmdOutputObservation,
|
||||
FileEditObservation,
|
||||
FileReadObservation,
|
||||
)
|
||||
from openhands.io import read_task
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.mcp import fetch_mcp_tools_from_config
|
||||
from openhands.microagent.microagent import BaseMicroagent
|
||||
|
||||
# Color and styling constants
|
||||
COLOR_GOLD = '#FFD700'
|
||||
COLOR_GREY = '#808080'
|
||||
DEFAULT_STYLE = Style.from_dict(
|
||||
{
|
||||
'gold': COLOR_GOLD,
|
||||
'grey': COLOR_GREY,
|
||||
'prompt': f'{COLOR_GOLD} bold',
|
||||
}
|
||||
)
|
||||
|
||||
COMMANDS = {
|
||||
'/exit': 'Exit the application',
|
||||
'/help': 'Display available commands',
|
||||
'/init': 'Initialize a new repository',
|
||||
}
|
||||
|
||||
REPO_MD_CREATE_PROMPT = """
|
||||
Please explore this repository. Create the file .openhands/microagents/repo.md with:
|
||||
- A description of the project
|
||||
- An overview of the file structure
|
||||
- Any information on how to run tests or other relevant commands
|
||||
- Any other information that would be helpful to a brand new developer
|
||||
Keep it short--just a few paragraphs will do.
|
||||
"""
|
||||
prompt_session = PromptSession()
|
||||
|
||||
|
||||
class CommandCompleter(Completer):
|
||||
"""Custom completer for commands."""
|
||||
|
||||
def get_completions(self, document, complete_event):
|
||||
text = document.text
|
||||
|
||||
# Only show completions if the user has typed '/'
|
||||
if text.startswith('/'):
|
||||
# If just '/' is typed, show all commands
|
||||
if text == '/':
|
||||
for command, description in COMMANDS.items():
|
||||
yield Completion(
|
||||
command[1:], # Remove the leading '/' as it's already typed
|
||||
start_position=0,
|
||||
display=f'{command} - {description}',
|
||||
)
|
||||
# Otherwise show matching commands
|
||||
else:
|
||||
for command, description in COMMANDS.items():
|
||||
if command.startswith(text):
|
||||
yield Completion(
|
||||
command[len(text) :], # Complete the remaining part
|
||||
start_position=0,
|
||||
display=f'{command} - {description}',
|
||||
)
|
||||
|
||||
|
||||
class UsageMetrics:
|
||||
def __init__(self):
|
||||
self.total_cost: float = 0.00
|
||||
self.total_input_tokens: int = 0
|
||||
self.total_output_tokens: int = 0
|
||||
self.total_cache_read: int = 0
|
||||
self.total_cache_write: int = 0
|
||||
|
||||
|
||||
prompt_session = PromptSession(style=DEFAULT_STYLE, completer=CommandCompleter())
|
||||
|
||||
|
||||
def display_message(message: str):
|
||||
message = message.strip()
|
||||
|
||||
if message:
|
||||
print_formatted_text(f'\n{message}\n')
|
||||
|
||||
|
||||
def display_command(command: str):
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=command,
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='Command Run',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_container(container)
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_confirmation(confirmation_state: ActionConfirmationStatus):
|
||||
status_map = {
|
||||
ActionConfirmationStatus.CONFIRMED: ('ansigreen', '✅'),
|
||||
ActionConfirmationStatus.REJECTED: ('ansired', '❌'),
|
||||
ActionConfirmationStatus.AWAITING_CONFIRMATION: ('ansiyellow', '⏳'),
|
||||
}
|
||||
color, icon = status_map.get(confirmation_state, ('ansiyellow', ''))
|
||||
|
||||
def display_message(message: str) -> None:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
(color, f'{icon} '),
|
||||
(color, str(confirmation_state)),
|
||||
('ansiyellow', '🤖 '),
|
||||
('ansiyellow', message),
|
||||
('', '\n'),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def display_command_output(output: str):
|
||||
def display_command(command: str) -> None:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
('', '❯ '),
|
||||
('ansigreen', command),
|
||||
('', '\n'),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def display_confirmation(confirmation_state: ActionConfirmationStatus) -> None:
|
||||
if confirmation_state == ActionConfirmationStatus.CONFIRMED:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
('ansigreen', '✅ '),
|
||||
('ansigreen', str(confirmation_state)),
|
||||
('', '\n'),
|
||||
]
|
||||
)
|
||||
)
|
||||
elif confirmation_state == ActionConfirmationStatus.REJECTED:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
('ansired', '❌ '),
|
||||
('ansired', str(confirmation_state)),
|
||||
('', '\n'),
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
('ansiyellow', '⏳ '),
|
||||
('ansiyellow', str(confirmation_state)),
|
||||
('', '\n'),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def display_command_output(output: str) -> None:
|
||||
lines = output.split('\n')
|
||||
formatted_lines = []
|
||||
for line in lines:
|
||||
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
|
||||
# TODO: clean this up once we clean up terminal output
|
||||
continue
|
||||
formatted_lines.append(line)
|
||||
formatted_lines.append('\n')
|
||||
|
||||
# Remove the last newline if it exists
|
||||
if formatted_lines:
|
||||
formatted_lines.pop()
|
||||
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=''.join(formatted_lines),
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='Command Output',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_container(container)
|
||||
print_formatted_text(FormattedText([('ansiblue', line)]))
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_file_edit(event: FileEditAction | FileEditObservation):
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=f'{event}',
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='File Edit',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
def display_file_edit(event: FileEditAction | FileEditObservation) -> None:
|
||||
print_formatted_text(
|
||||
FormattedText(
|
||||
[
|
||||
('ansigreen', str(event)),
|
||||
('', '\n'),
|
||||
]
|
||||
)
|
||||
)
|
||||
print_container(container)
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_file_read(event: FileReadObservation):
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=f'{event}',
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='File Read',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_container(container)
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_event(event: Event, config: AppConfig) -> None:
|
||||
@@ -238,115 +136,10 @@ def display_event(event: Event, config: AppConfig) -> None:
|
||||
display_file_edit(event)
|
||||
if isinstance(event, FileEditObservation):
|
||||
display_file_edit(event)
|
||||
if isinstance(event, FileReadObservation):
|
||||
display_file_read(event)
|
||||
if hasattr(event, 'confirmation_state') and config.security.confirmation_mode:
|
||||
display_confirmation(event.confirmation_state)
|
||||
|
||||
|
||||
def display_help(style=DEFAULT_STYLE):
|
||||
print_formatted_text(
|
||||
HTML(f'\n<grey>OpenHands CLI v{__version__}</grey>\n'), style=style
|
||||
)
|
||||
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'<gold>OpenHands CLI lets you interact with the OpenHands agent from the command line.</gold>'
|
||||
)
|
||||
)
|
||||
print_formatted_text('')
|
||||
|
||||
print_formatted_text('Things that you can try:')
|
||||
print_formatted_text(
|
||||
HTML('• Ask questions about the codebase <grey>> How does main.py work?</grey>')
|
||||
)
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'• Edit files or add new features <grey>> Add a new function to ...</grey>'
|
||||
)
|
||||
)
|
||||
print_formatted_text(
|
||||
HTML('• Find and fix issues <grey>> Fix the type error in ...</grey>')
|
||||
)
|
||||
print_formatted_text('')
|
||||
|
||||
print_formatted_text('Some tips to get the most out of OpenHands:')
|
||||
print_formatted_text(
|
||||
'• Be as specific as possible about the desired outcome or the problem to be solved.'
|
||||
)
|
||||
print_formatted_text(
|
||||
'• Provide context, including relevant file paths and line numbers if available.'
|
||||
)
|
||||
print_formatted_text('• Break large tasks into smaller, manageable prompts.')
|
||||
print_formatted_text('• Include relevant error messages or logs.')
|
||||
print_formatted_text(
|
||||
'• Specify the programming language or framework, if not obvious.'
|
||||
)
|
||||
print_formatted_text('')
|
||||
|
||||
print_formatted_text(HTML('Interactive commands:'), style=style)
|
||||
for command, description in COMMANDS.items():
|
||||
print_formatted_text(
|
||||
HTML(f'<gold><b>{command}</b></gold> - <grey>{description}</grey>'),
|
||||
style=style,
|
||||
)
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'<grey>Learn more at: https://docs.all-hands.dev/modules/usage/getting-started</grey>'
|
||||
)
|
||||
)
|
||||
print_formatted_text('')
|
||||
|
||||
|
||||
def display_banner(session_id: str, is_loaded: asyncio.Event):
|
||||
print_formatted_text(
|
||||
HTML(r"""<gold>
|
||||
___ _ _ _
|
||||
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
|
||||
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
|
||||
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
|
||||
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|
||||
|_|
|
||||
</gold>"""),
|
||||
style=DEFAULT_STYLE,
|
||||
)
|
||||
|
||||
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
|
||||
|
||||
banner_text = (
|
||||
'Initialized session' if is_loaded.is_set() else 'Initializing session'
|
||||
)
|
||||
print_formatted_text(HTML(f'\n<grey>{banner_text} {session_id}</grey>\n'))
|
||||
|
||||
|
||||
def display_welcome_message():
|
||||
print_formatted_text(
|
||||
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
|
||||
)
|
||||
print_formatted_text(
|
||||
HTML('What do you want to build? <grey>Type /help for help</grey>\n'),
|
||||
style=DEFAULT_STYLE,
|
||||
)
|
||||
|
||||
|
||||
def display_initialization_animation(text, is_loaded: asyncio.Event):
|
||||
ANIMATION_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
|
||||
i = 0
|
||||
while not is_loaded.is_set():
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.write(
|
||||
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.1)
|
||||
i += 1
|
||||
|
||||
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
async def read_prompt_input(multiline=False):
|
||||
try:
|
||||
if multiline:
|
||||
@@ -356,293 +149,35 @@ async def read_prompt_input(multiline=False):
|
||||
def _(event):
|
||||
event.current_buffer.validate_and_handle()
|
||||
|
||||
with patch_stdout():
|
||||
message = await prompt_session.prompt_async(
|
||||
'Enter your message and press Ctrl+D to finish:\n',
|
||||
multiline=True,
|
||||
key_bindings=kb,
|
||||
)
|
||||
message = await prompt_session.prompt_async(
|
||||
'Enter your message and press Ctrl+D to finish:\n',
|
||||
multiline=True,
|
||||
key_bindings=kb,
|
||||
)
|
||||
else:
|
||||
with patch_stdout():
|
||||
message = await prompt_session.prompt_async(
|
||||
'> ',
|
||||
)
|
||||
message = await prompt_session.prompt_async(
|
||||
'>> ',
|
||||
)
|
||||
return message
|
||||
except KeyboardInterrupt:
|
||||
return '/exit'
|
||||
return 'exit'
|
||||
except EOFError:
|
||||
return '/exit'
|
||||
return 'exit'
|
||||
|
||||
|
||||
async def read_confirmation_input():
|
||||
try:
|
||||
confirmation = await prompt_session.prompt_async(
|
||||
'Confirm action (possible security risk)? (y/n) > ',
|
||||
'Confirm action (possible security risk)? (y/n) >> ',
|
||||
)
|
||||
return confirmation.lower() == 'y'
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
return False
|
||||
|
||||
|
||||
async def init_repository(current_dir: str) -> bool:
|
||||
repo_file_path = Path(current_dir) / '.openhands' / 'microagents' / 'repo.md'
|
||||
init_repo = False
|
||||
|
||||
if repo_file_path.exists():
|
||||
try:
|
||||
content = await asyncio.get_event_loop().run_in_executor(
|
||||
None, read_file, repo_file_path
|
||||
)
|
||||
|
||||
print_formatted_text(
|
||||
'Repository instructions file (repo.md) already exists.\n'
|
||||
)
|
||||
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=content,
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='Repository Instructions (repo.md)',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_container(container)
|
||||
print_formatted_text('') # Add a newline after the frame
|
||||
|
||||
init_repo = cli_confirm(
|
||||
'Do you want to re-initialize?',
|
||||
['Yes, re-initialize', 'No, dismiss'],
|
||||
)
|
||||
|
||||
if init_repo:
|
||||
write_to_file(repo_file_path, '')
|
||||
except Exception:
|
||||
print_formatted_text('Error reading repository instructions file (repo.md)')
|
||||
init_repo = False
|
||||
else:
|
||||
print_formatted_text(
|
||||
'\nRepository instructions file will be created by exploring the repository.\n'
|
||||
)
|
||||
|
||||
init_repo = cli_confirm(
|
||||
'Do you want to proceed?',
|
||||
['Yes, create', 'No, dismiss'],
|
||||
)
|
||||
|
||||
return init_repo
|
||||
|
||||
|
||||
def read_file(file_path):
|
||||
with open(file_path, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write_to_file(file_path, content):
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def cli_confirm(question: str = 'Are you sure?', choices: Optional[List[str]] = None):
|
||||
if choices is None:
|
||||
choices = ['Yes', 'No']
|
||||
selected = [0] # Using list to allow modification in closure
|
||||
|
||||
def get_choice_text():
|
||||
return [
|
||||
('class:question', f'{question}\n\n'),
|
||||
] + [
|
||||
(
|
||||
'class:selected' if i == selected[0] else 'class:unselected',
|
||||
f"{'> ' if i == selected[0] else ' '}{choice}\n",
|
||||
)
|
||||
for i, choice in enumerate(choices)
|
||||
]
|
||||
|
||||
kb = KeyBindings()
|
||||
|
||||
@kb.add('up')
|
||||
def _(event):
|
||||
selected[0] = (selected[0] - 1) % len(choices)
|
||||
|
||||
@kb.add('down')
|
||||
def _(event):
|
||||
selected[0] = (selected[0] + 1) % len(choices)
|
||||
|
||||
@kb.add('enter')
|
||||
def _(event):
|
||||
event.app.exit(result=selected[0] == 0)
|
||||
|
||||
style = Style.from_dict({'selected': COLOR_GOLD, 'unselected': ''})
|
||||
|
||||
layout = Layout(
|
||||
HSplit(
|
||||
[
|
||||
Window(
|
||||
FormattedTextControl(get_choice_text),
|
||||
always_hide_cursor=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
app = Application(
|
||||
layout=layout,
|
||||
key_bindings=kb,
|
||||
style=style,
|
||||
mouse_support=True,
|
||||
full_screen=False,
|
||||
)
|
||||
|
||||
return app.run(in_thread=True)
|
||||
|
||||
|
||||
def update_usage_metrics(event: Event, usage_metrics: UsageMetrics):
|
||||
"""Updates the UsageMetrics object with data from an event's llm_metrics."""
|
||||
if hasattr(event, 'llm_metrics'):
|
||||
llm_metrics: Metrics | None = getattr(event, 'llm_metrics', None)
|
||||
if llm_metrics:
|
||||
# Safely get accumulated_cost
|
||||
cost = getattr(llm_metrics, 'accumulated_cost', 0)
|
||||
# Ensure cost is a number before adding
|
||||
usage_metrics.total_cost += cost if isinstance(cost, float) else 0
|
||||
|
||||
# Safely get token usage details object/dict
|
||||
token_usage = getattr(llm_metrics, 'accumulated_token_usage', None)
|
||||
if token_usage:
|
||||
# Assume object access using getattr, providing defaults
|
||||
prompt_tokens = getattr(token_usage, 'prompt_tokens', 0)
|
||||
completion_tokens = getattr(token_usage, 'completion_tokens', 0)
|
||||
cache_read = getattr(token_usage, 'cache_read_tokens', 0)
|
||||
cache_write = getattr(token_usage, 'cache_write_tokens', 0)
|
||||
|
||||
# Ensure tokens are numbers before adding
|
||||
usage_metrics.total_input_tokens += (
|
||||
prompt_tokens if isinstance(prompt_tokens, int) else 0
|
||||
)
|
||||
usage_metrics.total_output_tokens += (
|
||||
completion_tokens if isinstance(completion_tokens, int) else 0
|
||||
)
|
||||
usage_metrics.total_cache_read += (
|
||||
cache_read if isinstance(cache_read, int) else 0
|
||||
)
|
||||
usage_metrics.total_cache_write += (
|
||||
cache_write if isinstance(cache_write, int) else 0
|
||||
)
|
||||
|
||||
|
||||
def shutdown(usage_metrics: UsageMetrics, session_id: str):
|
||||
cost_str = f'${usage_metrics.total_cost:.6f}'
|
||||
input_tokens_str = f'{usage_metrics.total_input_tokens:,}'
|
||||
cache_read_str = f'{usage_metrics.total_cache_read:,}'
|
||||
cache_write_str = f'{usage_metrics.total_cache_write:,}'
|
||||
output_tokens_str = f'{usage_metrics.total_output_tokens:,}'
|
||||
total_tokens_str = (
|
||||
f'{usage_metrics.total_input_tokens + usage_metrics.total_output_tokens:,}'
|
||||
)
|
||||
|
||||
labels_and_values = [
|
||||
(' Total Cost (USD):', cost_str),
|
||||
(' Total Input Tokens:', input_tokens_str),
|
||||
(' Cache Hits:', cache_read_str),
|
||||
(' Cache Writes:', cache_write_str),
|
||||
(' Total Output Tokens:', output_tokens_str),
|
||||
(' Total Tokens:', total_tokens_str),
|
||||
]
|
||||
|
||||
# Calculate max widths for alignment
|
||||
max_label_width = max(len(label) for label, _ in labels_and_values)
|
||||
max_value_width = max(len(value) for _, value in labels_and_values)
|
||||
|
||||
# Construct the summary text with aligned columns
|
||||
summary_lines = [
|
||||
f'{label:<{max_label_width}} {value:>{max_value_width}}'
|
||||
for label, value in labels_and_values
|
||||
]
|
||||
summary_text = '\n'.join(summary_lines)
|
||||
|
||||
container = Frame(
|
||||
TextArea(
|
||||
text=summary_text,
|
||||
read_only=True,
|
||||
style=COLOR_GREY,
|
||||
wrap_lines=True,
|
||||
),
|
||||
title='Session Summary',
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
print_container(container)
|
||||
print_formatted_text(HTML(f'\n<grey>Closed session {session_id}</grey>\n'))
|
||||
|
||||
|
||||
def manage_openhands_file(folder_path=None, add_to_trusted=False):
|
||||
openhands_file = Path.home() / '.openhands.toml'
|
||||
default_content: dict = {'trusted_dirs': []}
|
||||
|
||||
if not openhands_file.exists():
|
||||
with open(openhands_file, 'w') as f:
|
||||
toml.dump(default_content, f)
|
||||
|
||||
if folder_path:
|
||||
with open(openhands_file, 'r') as f:
|
||||
try:
|
||||
config = toml.load(f)
|
||||
except Exception:
|
||||
config = default_content
|
||||
|
||||
if 'trusted_dirs' not in config:
|
||||
config['trusted_dirs'] = []
|
||||
|
||||
if folder_path in config['trusted_dirs']:
|
||||
return True
|
||||
|
||||
if add_to_trusted:
|
||||
config['trusted_dirs'].append(folder_path)
|
||||
with open(openhands_file, 'w') as f:
|
||||
toml.dump(config, f)
|
||||
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_folder_security_agreement(current_dir):
|
||||
is_trusted = manage_openhands_file(current_dir)
|
||||
|
||||
if not is_trusted:
|
||||
security_frame = Frame(
|
||||
TextArea(
|
||||
text=(
|
||||
f'Do you trust the files in this folder?\n\n'
|
||||
f'{current_dir}\n\n'
|
||||
'OpenHands may read and execute files in this folder with your permission.'
|
||||
),
|
||||
style=COLOR_GREY,
|
||||
read_only=True,
|
||||
wrap_lines=True,
|
||||
),
|
||||
style=f'fg:{COLOR_GREY}',
|
||||
)
|
||||
|
||||
clear()
|
||||
print_container(security_frame)
|
||||
|
||||
confirm = cli_confirm('Do you wish to continue?', ['Yes, proceed', 'No, exit'])
|
||||
|
||||
if confirm:
|
||||
manage_openhands_file(current_dir, add_to_trusted=True)
|
||||
|
||||
return confirm
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def main(loop: asyncio.AbstractEventLoop):
|
||||
async def main(loop: asyncio.AbstractEventLoop) -> None:
|
||||
"""Runs the agent in CLI mode."""
|
||||
|
||||
reload_microagents = False
|
||||
|
||||
args = parse_arguments()
|
||||
|
||||
logger.setLevel(logging.WARNING)
|
||||
@@ -650,12 +185,6 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
# Load config from toml and override with command line arguments
|
||||
config: AppConfig = setup_config_from_args(args)
|
||||
|
||||
# TODO: Set working directory from config or use current working directory?
|
||||
current_dir = config.workspace_base
|
||||
|
||||
if not current_dir:
|
||||
raise ValueError('Workspace base directory not specified')
|
||||
|
||||
# Read task from file, CLI args, or stdin
|
||||
task_str = read_task(args, config.cli_multiline_input)
|
||||
|
||||
@@ -663,19 +192,10 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
initial_user_action = MessageAction(content=task_str) if task_str else None
|
||||
|
||||
sid = str(uuid4())
|
||||
is_loaded = asyncio.Event()
|
||||
|
||||
# Show OpenHands banner and session ID
|
||||
display_banner(session_id=sid, is_loaded=is_loaded)
|
||||
|
||||
# Show Initialization loader
|
||||
loop.run_in_executor(
|
||||
None, display_initialization_animation, 'Initializing...', is_loaded
|
||||
)
|
||||
display_message(f'Session ID: {sid}')
|
||||
|
||||
agent = create_agent(config)
|
||||
mcp_tools = await fetch_mcp_tools_from_config(config.mcp)
|
||||
agent.set_mcp_tools(mcp_tools)
|
||||
|
||||
runtime = create_runtime(
|
||||
config,
|
||||
sid=sid,
|
||||
@@ -687,64 +207,26 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
|
||||
usage_metrics = UsageMetrics()
|
||||
|
||||
async def prompt_for_next_task():
|
||||
nonlocal reload_microagents
|
||||
while True:
|
||||
next_message = await read_prompt_input(config.cli_multiline_input)
|
||||
|
||||
if not next_message.strip():
|
||||
continue
|
||||
|
||||
if next_message == '/exit':
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
|
||||
)
|
||||
shutdown(usage_metrics, sid)
|
||||
return
|
||||
elif next_message == '/help':
|
||||
display_help()
|
||||
continue
|
||||
elif next_message == '/init':
|
||||
if config.runtime == 'local':
|
||||
init_repo = await init_repository(current_dir)
|
||||
if init_repo:
|
||||
event_stream.add_event(
|
||||
MessageAction(content=REPO_MD_CREATE_PROMPT),
|
||||
EventSource.USER,
|
||||
)
|
||||
reload_microagents = True
|
||||
return
|
||||
else:
|
||||
print_formatted_text(
|
||||
'\nRepository initialization through the CLI is only supported for local runtime.\n'
|
||||
)
|
||||
continue
|
||||
|
||||
action = MessageAction(content=next_message)
|
||||
event_stream.add_event(action, EventSource.USER)
|
||||
async def prompt_for_next_task() -> None:
|
||||
next_message = await read_prompt_input(config.cli_multiline_input)
|
||||
if not next_message.strip():
|
||||
await prompt_for_next_task()
|
||||
if next_message == 'exit':
|
||||
event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
|
||||
)
|
||||
return
|
||||
action = MessageAction(content=next_message)
|
||||
event_stream.add_event(action, EventSource.USER)
|
||||
|
||||
async def on_event_async(event: Event) -> None:
|
||||
nonlocal reload_microagents
|
||||
display_event(event, config)
|
||||
update_usage_metrics(event, usage_metrics)
|
||||
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state in [
|
||||
AgentState.AWAITING_USER_INPUT,
|
||||
AgentState.FINISHED,
|
||||
]:
|
||||
# Reload microagents after initialization of repo.md
|
||||
if reload_microagents:
|
||||
microagents: list[BaseMicroagent] = (
|
||||
runtime.get_microagents_from_selected_repo(None)
|
||||
)
|
||||
memory.load_user_workspace_microagents(microagents)
|
||||
reload_microagents = False
|
||||
await prompt_for_next_task()
|
||||
|
||||
if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
|
||||
user_confirmed = await read_confirmation_input()
|
||||
if user_confirmed:
|
||||
@@ -782,22 +264,6 @@ async def main(loop: asyncio.AbstractEventLoop):
|
||||
repo_directory=repo_directory,
|
||||
)
|
||||
|
||||
# Clear loading animation
|
||||
is_loaded.set()
|
||||
|
||||
if not check_folder_security_agreement(current_dir):
|
||||
# User rejected, exit application
|
||||
return
|
||||
|
||||
# Clear the terminal
|
||||
clear()
|
||||
|
||||
# Show OpenHands banner and session ID
|
||||
display_banner(session_id=sid, is_loaded=is_loaded)
|
||||
|
||||
# Show OpenHands welcome
|
||||
display_welcome_message()
|
||||
|
||||
if initial_user_action:
|
||||
# If there's an initial user action, enqueue it and do not prompt again
|
||||
event_stream.add_event(initial_user_action, EventSource.USER)
|
||||
|
||||
@@ -10,9 +10,10 @@ class AgentConfig(BaseModel):
|
||||
"""Configuration for the agent.
|
||||
|
||||
Attributes:
|
||||
enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
|
||||
enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
|
||||
enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
|
||||
function_calling: Whether function calling is enabled. Default is True.
|
||||
codeact_enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
|
||||
codeact_enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
|
||||
codeact_enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
|
||||
llm_config: The name of the llm config to use. If specified, this will override global llm config.
|
||||
enable_prompt_extensions: Whether to use prompt extensions (e.g., microagents, inject runtime info). Default is True.
|
||||
disabled_microagents: A list of microagents to disable (by name, without .py extension, e.g. ["github", "lint"]). Default is None.
|
||||
@@ -22,9 +23,9 @@ class AgentConfig(BaseModel):
|
||||
"""
|
||||
|
||||
llm_config: str | None = Field(default=None)
|
||||
enable_browsing: bool = Field(default=True)
|
||||
enable_llm_editor: bool = Field(default=False)
|
||||
enable_jupyter: bool = Field(default=True)
|
||||
codeact_enable_browsing: bool = Field(default=True)
|
||||
codeact_enable_llm_editor: bool = Field(default=False)
|
||||
codeact_enable_jupyter: bool = Field(default=True)
|
||||
enable_prompt_extensions: bool = Field(default=True)
|
||||
disabled_microagents: list[str] = Field(default_factory=list)
|
||||
enable_history_truncation: bool = Field(default=True)
|
||||
|
||||
@@ -11,7 +11,6 @@ from openhands.core.config.config_utils import (
|
||||
)
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.config.mcp_config import MCPConfig
|
||||
from openhands.core.config.sandbox_config import SandboxConfig
|
||||
from openhands.core.config.security_config import SecurityConfig
|
||||
|
||||
@@ -48,7 +47,6 @@ class AppConfig(BaseModel):
|
||||
file_uploads_allowed_extensions: Allowed file extensions. `['.*']` allows all.
|
||||
cli_multiline_input: Whether to enable multiline input in CLI. When disabled,
|
||||
input is read line by line. When enabled, input continues until /exit command.
|
||||
mcp: MCP configuration settings.
|
||||
"""
|
||||
|
||||
llms: dict[str, LLMConfig] = Field(default_factory=dict)
|
||||
@@ -90,7 +88,6 @@ class AppConfig(BaseModel):
|
||||
max_concurrent_conversations: int = Field(
|
||||
default=3
|
||||
) # Maximum number of concurrent agent loops allowed per user
|
||||
mcp: MCPConfig = Field(default_factory=MCPConfig)
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
@@ -155,21 +153,6 @@ class StructuredSummaryCondenserConfig(BaseModel):
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class CondenserPipelineConfig(BaseModel):
|
||||
"""Configuration for the CondenserPipeline.
|
||||
|
||||
Not currently supported by the TOML or ENV_VAR configuration strategies.
|
||||
"""
|
||||
|
||||
type: Literal['pipeline'] = Field('pipeline')
|
||||
condensers: list[CondenserConfig] = Field(
|
||||
default_factory=list,
|
||||
description='List of condenser configurations to be used in the pipeline.',
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
# Type alias for convenience
|
||||
CondenserConfig = (
|
||||
NoOpCondenserConfig
|
||||
@@ -180,7 +163,6 @@ CondenserConfig = (
|
||||
| AmortizedForgettingCondenserConfig
|
||||
| LLMAttentionCondenserConfig
|
||||
| StructuredSummaryCondenserConfig
|
||||
| CondenserPipelineConfig
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
from typing import List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
|
||||
class MCPConfig(BaseModel):
|
||||
"""Configuration for MCP (Message Control Protocol) settings.
|
||||
|
||||
Attributes:
|
||||
mcp_servers: List of MCP SSE (Server-Sent Events) server URLs.
|
||||
"""
|
||||
|
||||
mcp_servers: List[str] = Field(default_factory=list)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
def validate_servers(self) -> None:
|
||||
"""Validate that server URLs are valid and unique."""
|
||||
# Check for duplicate server URLs
|
||||
if len(set(self.mcp_servers)) != len(self.mcp_servers):
|
||||
raise ValueError('Duplicate MCP server URLs are not allowed')
|
||||
|
||||
# Validate URLs
|
||||
for url in self.mcp_servers:
|
||||
try:
|
||||
result = urlparse(url)
|
||||
if not all([result.scheme, result.netloc]):
|
||||
raise ValueError(f'Invalid URL format: {url}')
|
||||
except Exception as e:
|
||||
raise ValueError(f'Invalid URL {url}: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, 'MCPConfig']:
|
||||
"""
|
||||
Create a mapping of MCPConfig instances from a toml dictionary representing the [mcp] section.
|
||||
|
||||
The configuration is built from all keys in data.
|
||||
|
||||
Returns:
|
||||
dict[str, MCPConfig]: A mapping where the key "mcp" corresponds to the [mcp] configuration
|
||||
"""
|
||||
# Initialize the result mapping
|
||||
mcp_mapping: dict[str, MCPConfig] = {}
|
||||
|
||||
try:
|
||||
# Create SSE config if present
|
||||
mcp_config = MCPConfig.model_validate(data)
|
||||
mcp_config.validate_servers()
|
||||
# Create the main MCP config
|
||||
mcp_mapping['mcp'] = cls(mcp_servers=mcp_config.mcp_servers)
|
||||
except ValidationError as e:
|
||||
raise ValueError(f'Invalid MCP configuration: {e}')
|
||||
|
||||
return mcp_mapping
|
||||
@@ -23,7 +23,6 @@ from openhands.core.config.config_utils import (
|
||||
)
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.config.mcp_config import MCPConfig
|
||||
from openhands.core.config.sandbox_config import SandboxConfig
|
||||
from openhands.core.config.security_config import SecurityConfig
|
||||
from openhands.storage import get_file_store
|
||||
@@ -203,21 +202,6 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
|
||||
# Re-raise ValueError from SandboxConfig.from_toml_section
|
||||
raise ValueError('Error in [sandbox] section in config.toml')
|
||||
|
||||
# Process MCP sections if present
|
||||
if 'mcp' in toml_config:
|
||||
try:
|
||||
mcp_mapping = MCPConfig.from_toml_section(toml_config['mcp'])
|
||||
# We only use the base mcp config for now
|
||||
if 'mcp' in mcp_mapping:
|
||||
cfg.mcp = mcp_mapping['mcp']
|
||||
except (TypeError, KeyError, ValidationError) as e:
|
||||
logger.openhands_logger.warning(
|
||||
f'Cannot parse MCP config from toml, values have not been applied.\nError: {e}'
|
||||
)
|
||||
except ValueError:
|
||||
# Re-raise ValueError from MCPConfig.from_toml_section
|
||||
raise ValueError('Error in MCP sections in config.toml')
|
||||
|
||||
# Process condenser section if present
|
||||
if 'condenser' in toml_config:
|
||||
try:
|
||||
@@ -275,7 +259,6 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
|
||||
'security',
|
||||
'sandbox',
|
||||
'condenser',
|
||||
'mcp',
|
||||
}
|
||||
for key in toml_config:
|
||||
if key.lower() not in known_sections:
|
||||
|
||||
@@ -222,14 +222,14 @@ class BrowserUnavailableException(Exception):
|
||||
# ============================================
|
||||
|
||||
|
||||
class MicroagentError(Exception):
|
||||
class MicroAgentError(Exception):
|
||||
"""Base exception for all microagent errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MicroagentValidationError(MicroagentError):
|
||||
class MicroAgentValidationError(MicroAgentError):
|
||||
"""Raised when there's a validation error in microagent metadata."""
|
||||
|
||||
def __init__(self, message: str = 'Microagent validation failed') -> None:
|
||||
def __init__(self, message: str = 'Micro agent validation failed') -> None:
|
||||
super().__init__(message)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user