mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c250b0c39 | |||
| 5219f85bfa | |||
| a237b578c0 | |||
| f42a4f75cb | |||
| 3e645f8649 | |||
| 5182388323 | |||
| 471d272c7c | |||
| 0522734875 | |||
| f4fd8ea907 | |||
| e9413aaded | |||
| ef004962cc | |||
| 58d67a2480 | |||
| 72179f45d3 | |||
| 15e7709ff6 | |||
| bb563d6dd1 | |||
| d991b9880d | |||
| fe82cfd277 | |||
| 16fa8ea7be | |||
| f292f3a84d | |||
| 5076f21e86 | |||
| 2640d43159 | |||
| 609fefc1b6 | |||
| 5db0d495d4 | |||
| 60fa7b3d01 | |||
| cca2a55166 | |||
| c5e58572d5 | |||
| baaa41ed99 | |||
| 19bae5ac0f | |||
| 93e1cd44c6 | |||
| c0ce78c64a | |||
| 399bf92ed1 | |||
| 2bbe15a329 | |||
| 6f22092d07 | |||
| c034cc5dfb | |||
| 9bd02440b0 | |||
| c9d8782566 | |||
| ef49994700 | |||
| 0f92bdc9a8 |
+24
-14
@@ -1,5 +1,5 @@
|
||||
# Workflow that builds and tests the CLI binary executable
|
||||
name: CLI - Build and Test Binary
|
||||
name: CLI - Build binary and optionally release
|
||||
|
||||
# Run on pushes to main branch and CLI tags, and on pull requests when CLI files change
|
||||
on:
|
||||
@@ -12,17 +12,28 @@ on:
|
||||
paths:
|
||||
- "openhands-cli/**"
|
||||
|
||||
permissions:
|
||||
contents: write # needed to create releases or upload assets
|
||||
|
||||
# Cancel previous runs if a new commit is pushed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test-binary:
|
||||
name: Build and test binary executable
|
||||
build-binary:
|
||||
name: Build binary executable
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
include:
|
||||
# Build on Ubuntu 22.04 for maximum GLIBC compatibility (GLIBC 2.31)
|
||||
- os: ubuntu-22.04
|
||||
platform: linux
|
||||
artifact_name: openhands-cli-linux
|
||||
# Build on macOS for macOS users
|
||||
- os: macos-15
|
||||
platform: macos
|
||||
artifact_name: openhands-cli-macos
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
@@ -60,18 +71,17 @@ jobs:
|
||||
|
||||
echo "✅ Build & test finished without ❌ markers"
|
||||
|
||||
- name: Upload binary artifact (for releases only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openhands-cli-${{ matrix.os }}
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: openhands-cli/dist/openhands*
|
||||
retention-days: 30
|
||||
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-test-binary
|
||||
needs: build-binary
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -85,12 +95,12 @@ jobs:
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
# Rename binaries to include OS in filename
|
||||
if [ -f artifacts/openhands-cli-ubuntu-latest/openhands ]; then
|
||||
cp artifacts/openhands-cli-ubuntu-latest/openhands release-assets/openhands-linux
|
||||
# Copy binaries with appropriate names for release
|
||||
if [ -f artifacts/openhands-cli-linux/openhands ]; then
|
||||
cp artifacts/openhands-cli-linux/openhands release-assets/openhands-linux
|
||||
fi
|
||||
if [ -f artifacts/openhands-cli-macos-latest/openhands ]; then
|
||||
cp artifacts/openhands-cli-macos-latest/openhands release-assets/openhands-macos
|
||||
if [ -f artifacts/openhands-cli-macos/openhands ]; then
|
||||
cp artifacts/openhands-cli-macos/openhands release-assets/openhands-macos
|
||||
fi
|
||||
ls -la release-assets/
|
||||
|
||||
@@ -101,4 +111,4 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CLI_RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Enterprise Check Migrations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'enterprise/migrations/**'
|
||||
|
||||
jobs:
|
||||
check-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Fetch base branch
|
||||
run: git fetch origin ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Check if base branch is ancestor of PR
|
||||
id: check_up_to_date
|
||||
shell: bash
|
||||
run: |
|
||||
BASE="origin/${{ github.event.pull_request.base.ref }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
if git merge-base --is-ancestor "$BASE" "$HEAD"; then
|
||||
echo "We're up to date with base $BASE"
|
||||
exit 0
|
||||
else
|
||||
echo "NOT up to date with base $BASE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: |
|
||||
⚠️ This PR contains **migrations**
|
||||
|
||||
- name: Comment warning on PR
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
⚠️ This PR contains **migrations**. Please synchronize before merging to prevent conflicts.
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
|
||||
- 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
|
||||
run: poetry run python3 -m openhands.runtime.utils.runtime_build --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
|
||||
- name: Lowercase Repository Owner
|
||||
run: |
|
||||
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
run: pip install pre-commit==4.2.0
|
||||
- name: Run pre-commit hooks
|
||||
working-directory: ./enterprise
|
||||
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
|
||||
run: pre-commit run --all-files --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
lint-cli-python:
|
||||
name: Lint CLI python
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# Publishes the OpenHands PyPi package
|
||||
name: Publish PyPi Package
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
description: "What are you publishing?"
|
||||
required: true
|
||||
default: ''
|
||||
type: choice
|
||||
options:
|
||||
- app server
|
||||
- cli
|
||||
default: app server
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
@@ -16,8 +19,10 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
# Only run for tags that don't contain '-cli'
|
||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli')
|
||||
# Run when manually dispatched for "app server" OR for tag pushes that don't contain '-cli'
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'app server')
|
||||
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli'))
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: useblacksmith/setup-python@v6
|
||||
@@ -38,8 +43,10 @@ jobs:
|
||||
release-cli:
|
||||
name: Publish CLI to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
# Only run for tags that contain '-cli'
|
||||
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli')
|
||||
# Run when manually dispatched for "cli" OR for tag pushes that contain '-cli'
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'cli')
|
||||
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli'))
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -64,4 +71,4 @@ jobs:
|
||||
- name: Publish CLI to PyPI
|
||||
working-directory: openhands-cli
|
||||
run: |
|
||||
uv publish --token ${{ secrets.PYPI_TOKEN }}
|
||||
uv publish --token ${{ secrets.PYPI_TOKEN_OPENHANDS }}
|
||||
|
||||
+1
-1
@@ -159,7 +159,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.58-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.59-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
|
||||
<img src="https://raw.githubusercontent.com/All-Hands-AI/docs/main/openhands/static/img/logo.png" alt="Logo" width="200">
|
||||
<h1 align="center">OpenHands: Code Less, Make More</h1>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,12 @@ 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.
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Upcoming change**: We are renaming our GitHub Org from `All-Hands-AI` to `OpenHands` on October 20th, 2025.
|
||||
> Check the [tracking issue](https://github.com/All-Hands-AI/OpenHands/issues/11376) for more information.
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Using OpenHands for work? We'd love to chat! Fill out
|
||||
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
@@ -76,17 +82,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
|
||||
You can also run OpenHands directly with Docker:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.58
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.59
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.58-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.59-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -6,7 +6,7 @@ that depends on the `base_image` **AND** a [Python source distribution](https://
|
||||
The following command will generate a `Dockerfile` file for `nikolaik/python-nodejs:python3.12-nodejs22` (the default base image), an updated `config.sh` and the runtime source distribution files/folders into `containers/runtime`:
|
||||
|
||||
```bash
|
||||
poetry run python3 openhands/runtime/utils/runtime_build.py \
|
||||
poetry run python3 -m openhands.runtime.utils.runtime_build \
|
||||
--base_image nikolaik/python-nodejs:python3.12-nodejs22 \
|
||||
--build_folder containers/runtime
|
||||
```
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.59-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 for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -13,7 +13,7 @@ from integrations.solvability.models.report import SolvabilityReport
|
||||
from integrations.solvability.models.summary import SolvabilitySummary
|
||||
from integrations.utils import ENABLE_SOLVABILITY_ANALYSIS
|
||||
from pydantic import ValidationError
|
||||
from server.auth.token_manager import get_config
|
||||
from server.config import get_config
|
||||
from storage.database import session_maker
|
||||
from storage.saas_settings_store import SaasSettingsStore
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ from integrations.utils import (
|
||||
from jinja2 import Environment
|
||||
from pydantic.dataclasses import dataclass
|
||||
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
|
||||
from server.auth.token_manager import TokenManager, get_config
|
||||
from server.auth.token_manager import TokenManager
|
||||
from server.config import get_config
|
||||
from storage.database import session_maker
|
||||
from storage.proactive_conversation_store import ProactiveConversationStore
|
||||
from storage.saas_secrets_store import SaasSecretsStore
|
||||
|
||||
@@ -4,7 +4,8 @@ from integrations.models import Message
|
||||
from integrations.types import ResolverViewInterface, UserData
|
||||
from integrations.utils import HOST, get_oh_labels, has_exact_mention
|
||||
from jinja2 import Environment
|
||||
from server.auth.token_manager import TokenManager, get_config
|
||||
from server.auth.token_manager import TokenManager
|
||||
from server.config import get_config
|
||||
from storage.database import session_maker
|
||||
from storage.saas_secrets_store import SaasSecretsStore
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Enterprise Migrations
|
||||
|
||||
## Migration conflicts
|
||||
|
||||
OpenHands PRs can fall out of sync with `main` quickly. When adding a migration, it's safest to sync the PR with main before merging to ensure you are caught up to any others that have been added.
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Sync DB with Models
|
||||
|
||||
Revision ID: 076
|
||||
Revises: 075
|
||||
Create Date: 2025-10-05 11:28:41.772294
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversationStartTaskStatus,
|
||||
)
|
||||
from openhands.app_server.event_callback.event_callback_result_models import (
|
||||
EventCallbackResultStatus,
|
||||
)
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '076'
|
||||
down_revision: Union[str, Sequence[str], None] = '075'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column('max_budget_per_task', sa.Float(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column('cache_read_tokens', sa.Integer(), server_default='0'),
|
||||
)
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column('cache_write_tokens', sa.Integer(), server_default='0'),
|
||||
)
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column('reasoning_tokens', sa.Integer(), server_default='0'),
|
||||
)
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column('context_window', sa.Integer(), server_default='0'),
|
||||
)
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column('per_turn_token', sa.Integer(), server_default='0'),
|
||||
)
|
||||
op.add_column(
|
||||
'conversation_metadata',
|
||||
sa.Column(
|
||||
'conversation_version', sa.String(), nullable=False, server_default='V0'
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_conversation_metadata_conversation_version'),
|
||||
'conversation_metadata',
|
||||
['conversation_version'],
|
||||
unique=False,
|
||||
)
|
||||
op.add_column('conversation_metadata', sa.Column('sandbox_id', sa.String()))
|
||||
op.create_index(
|
||||
op.f('ix_conversation_metadata_sandbox_id'),
|
||||
'conversation_metadata',
|
||||
['sandbox_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
'app_conversation_start_task',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('created_by_user_id', sa.String(), nullable=True),
|
||||
sa.Column('status', sa.Enum(AppConversationStartTaskStatus), nullable=True),
|
||||
sa.Column('detail', sa.String(), nullable=True),
|
||||
sa.Column('app_conversation_id', sa.UUID(), nullable=True),
|
||||
sa.Column('sandbox_id', sa.String(), nullable=True),
|
||||
sa.Column('agent_server_url', sa.String(), nullable=True),
|
||||
sa.Column('request', sa.JSON(), nullable=True),
|
||||
sa.Column(
|
||||
'created_at',
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text('(CURRENT_TIMESTAMP)'),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_app_conversation_start_task_created_at'),
|
||||
'app_conversation_start_task',
|
||||
['created_at'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_app_conversation_start_task_created_by_user_id'),
|
||||
'app_conversation_start_task',
|
||||
['created_by_user_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_app_conversation_start_task_updated_at'),
|
||||
'app_conversation_start_task',
|
||||
['updated_at'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
'event_callback',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('conversation_id', sa.UUID(), nullable=True),
|
||||
sa.Column('processor', sa.JSON(), nullable=True),
|
||||
sa.Column('event_kind', sa.String(), nullable=True),
|
||||
sa.Column(
|
||||
'created_at',
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text('(CURRENT_TIMESTAMP)'),
|
||||
nullable=True,
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_created_at'),
|
||||
'event_callback',
|
||||
['created_at'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
'event_callback_result',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('status', sa.Enum(EventCallbackResultStatus), nullable=True),
|
||||
sa.Column('event_callback_id', sa.UUID(), nullable=True),
|
||||
sa.Column('event_id', sa.UUID(), nullable=True),
|
||||
sa.Column('conversation_id', sa.UUID(), nullable=True),
|
||||
sa.Column('detail', sa.String(), nullable=True),
|
||||
sa.Column(
|
||||
'created_at',
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text('(CURRENT_TIMESTAMP)'),
|
||||
nullable=True,
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_result_conversation_id'),
|
||||
'event_callback_result',
|
||||
['conversation_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_result_created_at'),
|
||||
'event_callback_result',
|
||||
['created_at'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_result_event_callback_id'),
|
||||
'event_callback_result',
|
||||
['event_callback_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_result_event_id'),
|
||||
'event_callback_result',
|
||||
['event_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
'v1_remote_sandbox',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('created_by_user_id', sa.String(), nullable=True),
|
||||
sa.Column('sandbox_spec_id', sa.String(), nullable=True),
|
||||
sa.Column(
|
||||
'created_at',
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text('(CURRENT_TIMESTAMP)'),
|
||||
nullable=True,
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_v1_remote_sandbox_created_at'),
|
||||
'v1_remote_sandbox',
|
||||
['created_at'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_v1_remote_sandbox_created_by_user_id'),
|
||||
'v1_remote_sandbox',
|
||||
['created_by_user_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_v1_remote_sandbox_sandbox_spec_id'),
|
||||
'v1_remote_sandbox',
|
||||
['sandbox_spec_id'],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(
|
||||
op.f('ix_v1_remote_sandbox_sandbox_spec_id'), table_name='v1_remote_sandbox'
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_v1_remote_sandbox_created_by_user_id'), table_name='v1_remote_sandbox'
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_v1_remote_sandbox_created_at'), table_name='v1_remote_sandbox'
|
||||
)
|
||||
op.drop_table('v1_remote_sandbox')
|
||||
op.drop_index(
|
||||
op.f('ix_event_callback_result_event_id'),
|
||||
table_name='event_callback_result',
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_event_callback_result_event_callback_id'),
|
||||
table_name='event_callback_result',
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_event_callback_result_created_at'),
|
||||
table_name='event_callback_result',
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_event_callback_result_conversation_id'),
|
||||
table_name='event_callback_result',
|
||||
)
|
||||
op.drop_table('event_callback_result')
|
||||
op.drop_index(op.f('ix_event_callback_created_at'), table_name='event_callback')
|
||||
op.drop_table('event_callback')
|
||||
op.drop_index(
|
||||
op.f('ix_app_conversation_start_task_updated_at'),
|
||||
table_name='app_conversation_start_task',
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_app_conversation_start_task_created_by_user_id'),
|
||||
table_name='app_conversation_start_task',
|
||||
)
|
||||
op.drop_index(
|
||||
op.f('ix_app_conversation_start_task_created_at'),
|
||||
table_name='app_conversation_start_task',
|
||||
)
|
||||
op.drop_table('app_conversation_start_task')
|
||||
op.drop_column('conversation_metadata', 'sandbox_id')
|
||||
op.drop_column('conversation_metadata', 'conversation_version')
|
||||
op.drop_column('conversation_metadata', 'per_turn_token')
|
||||
op.drop_column('conversation_metadata', 'context_window')
|
||||
op.drop_column('conversation_metadata', 'reasoning_tokens')
|
||||
op.drop_column('conversation_metadata', 'cache_write_tokens')
|
||||
op.drop_column('conversation_metadata', 'cache_read_tokens')
|
||||
op.drop_column('conversation_metadata', 'max_budget_per_task')
|
||||
op.execute('DROP TYPE appconversationstarttaskstatus')
|
||||
op.execute('DROP TYPE eventcallbackresultstatus')
|
||||
# ### end Alembic commands ###
|
||||
Generated
+231
-49
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
@@ -148,6 +148,25 @@ files = [
|
||||
frozenlist = ">=1.1.0"
|
||||
typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.21.0"
|
||||
description = "asyncio bridge to the standard sqlite3 module"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"},
|
||||
{file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing_extensions = ">=4.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"]
|
||||
docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.16.5"
|
||||
@@ -1061,7 +1080,7 @@ files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
|
||||
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "comm"
|
||||
@@ -1797,6 +1816,25 @@ files = [
|
||||
{file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.19.1"
|
||||
description = "ECDSA cryptographic signature library (pure python)"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"},
|
||||
{file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.9.0"
|
||||
|
||||
[package.extras]
|
||||
gmpy = ["gmpy"]
|
||||
gmpy2 = ["gmpy2"]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.3.0"
|
||||
@@ -1968,38 +2006,79 @@ websockets = ["websockets (>=15.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastuuid"
|
||||
version = "0.12.0"
|
||||
version = "0.13.5"
|
||||
description = "Python bindings to Rust's UUID library."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "fastuuid-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22a900ef0956aacf862b460e20541fdae2d7c340594fe1bd6fdcb10d5f0791a9"},
|
||||
{file = "fastuuid-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0302f5acf54dc75de30103025c5a95db06d6c2be36829043a0aa16fc170076bc"},
|
||||
{file = "fastuuid-0.12.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7946b4a310cfc2d597dcba658019d72a2851612a2cebb949d809c0e2474cf0a6"},
|
||||
{file = "fastuuid-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1b6764dd42bf0c46c858fb5ade7b7a3d93b7a27485a7a5c184909026694cd88"},
|
||||
{file = "fastuuid-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bced35269315d16fe0c41003f8c9d63f2ee16a59295d90922cad5e6a67d0418"},
|
||||
{file = "fastuuid-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82106e4b0a24f4f2f73c88f89dadbc1533bb808900740ca5db9bbb17d3b0c824"},
|
||||
{file = "fastuuid-0.12.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:4db1bc7b8caa1d7412e1bea29b016d23a8d219131cff825b933eb3428f044dca"},
|
||||
{file = "fastuuid-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:07afc8e674e67ac3d35a608c68f6809da5fab470fb4ef4469094fdb32ba36c51"},
|
||||
{file = "fastuuid-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:328694a573fe9dce556b0b70c9d03776786801e028d82f0b6d9db1cb0521b4d1"},
|
||||
{file = "fastuuid-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02acaea2c955bb2035a7d8e7b3fba8bd623b03746ae278e5fa932ef54c702f9f"},
|
||||
{file = "fastuuid-0.12.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ed9f449cba8cf16cced252521aee06e633d50ec48c807683f21cc1d89e193eb0"},
|
||||
{file = "fastuuid-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:0df2ea4c9db96fd8f4fa38d0e88e309b3e56f8fd03675a2f6958a5b082a0c1e4"},
|
||||
{file = "fastuuid-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fe2407316a04ee8f06d3dbc7eae396d0a86591d92bafe2ca32fce23b1145786"},
|
||||
{file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c"},
|
||||
{file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37"},
|
||||
{file = "fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9"},
|
||||
{file = "fastuuid-0.12.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2925f67b88d47cb16aa3eb1ab20fdcf21b94d74490e0818c91ea41434b987493"},
|
||||
{file = "fastuuid-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7b15c54d300279ab20a9cc0579ada9c9f80d1bc92997fc61fb7bf3103d7cb26b"},
|
||||
{file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:458f1bc3ebbd76fdb89ad83e6b81ccd3b2a99fa6707cd3650b27606745cfb170"},
|
||||
{file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:a8f0f83fbba6dc44271a11b22e15838641b8c45612cdf541b4822a5930f6893c"},
|
||||
{file = "fastuuid-0.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:7cfd2092253d3441f6a8c66feff3c3c009da25a5b3da82bc73737558543632be"},
|
||||
{file = "fastuuid-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9303617e887429c193d036d47d0b32b774ed3618431123e9106f610d601eb57e"},
|
||||
{file = "fastuuid-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8790221325b376e1122e95f865753ebf456a9fb8faf0dca4f9bf7a3ff620e413"},
|
||||
{file = "fastuuid-0.12.0-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:e4b12d3e23515e29773fa61644daa660ceb7725e05397a986c2109f512579a48"},
|
||||
{file = "fastuuid-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:e41656457c34b5dcb784729537ea64c7d9bbaf7047b480c6c6a64c53379f455a"},
|
||||
{file = "fastuuid-0.12.0.tar.gz", hash = "sha256:d0bd4e5b35aad2826403f4411937c89e7c88857b1513fe10f696544c03e9bd8e"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b9edf8ee30718aee787cdd2e9e1ff3d4a3ec6ddb32fba0a23fa04956df69ab07"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f67ea1e25c5e782f7fb5aaa5208f157d950401dd9321ce56bcc6d4dc3d72ed60"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ff3fc87e1f19603dd53c38f42c2ea8d5d5462554deab69e9cf1800574e4756c"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6e5337fa7698dc52bc724da7e9239e93c5b24a09f6904b8660dfb8c41ce3dee"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9db596023c10dabb12489a88c51b75297c3a2478cb2be645e06905934e7b9fc"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:191ff6192fe53c5fc9d4d241ee1156b30a7ed6f1677b1cc2423e7ecdbc26222b"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:348ce9f296dda701ba46d8dceeff309f90dbc75dd85080bbed2b299aa908890a"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:46954fb644995d7fc8bbd710fbd4c65cedaa48c921c86fdbafef0229168a8c96"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22da0f66041e1c10c7d465b495cc6cd8e17e080dda34b4bd5ff5240b860fbb82"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-win32.whl", hash = "sha256:3e6b548f06c1ed7bad951a17a09eef69d6f24eb2b874cb4833e26b886d82990f"},
|
||||
{file = "fastuuid-0.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:c82838e52189d16b1307631179cb2cd37778dd8f4ddc00e9ce3c26f920b3b2f7"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c122558ca4b5487e2bd0863467e4ccfe636afd1274803741487d48f2e32ea0e1"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d7abd42a03a17a681abddd19aa4d44ca2747138cf8a48373b395cf1341a10de2"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2705cf7c2d6f7c03053404b75a4c44f872a73f6f9d5ea34f1dc6bba400c4a97c"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d220a056fcbad25932c1f25304261198612f271f4d150b2a84e81adb877daf7"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f29f93b5a0c5f5579f97f77d5319e9bfefd61d8678ec59d850201544faf33bf"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:399d86623fb806151b1feb9fdd818ebfc1d50387199a35f7264f98dfc1540af5"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:689e8795a1edd573b2c9a455024e4edf605a9690339bba29709857f7180894ea"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:25e82c4a1734da168b36f7308e397afbe9c9b353799a9c69563a605f11dd4641"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f62299e3cca69aad6a6fb37e26e45055587954d498ad98903fea24382377ea0e"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-win32.whl", hash = "sha256:68227f2230381b89fb1ad362ca6e433de85c6c11c36312b41757cad47b8a8e32"},
|
||||
{file = "fastuuid-0.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:4a32306982bd031cb20d5d1a726b7b958a55babebd2300ce6c8e352d3496e931"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85"},
|
||||
{file = "fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ea17dfd35e0e91920a35d91e65e5f9c9d1985db55ac4ff2f1667a0f61189cefa"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be6ad91e5fefbcc2a4b478858a2715e386d405834ea3ae337c3b6b95cc0e47d6"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea6df13a306aab3e0439d58c312ff1e6f4f07f09f667579679239b4a6121f64a"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2354c1996d3cf12dc2ba3752e2c4d6edc46e1a38c63893146777b1939f3062d4"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cf9b7469fc26d1f9b1c43ac4b192e219e85b88fdf81d71aa755a6c08c8a817"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92ba539170097b9047551375f1ca09d8d2b4aefcc79eeae3e1c43fe49b42072e"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:dbb81d05617bc2970765c1ad82db7e8716f6a2b7a361a14b83de5b9240ade448"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:d973bd6bf9d754d3cca874714ac0a6b22a47f239fb3d3c8687569db05aac3471"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e725ceef79486423f05ee657634d4b4c1ca5fb2c8a94e0708f5d6356a83f2a83"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-win32.whl", hash = "sha256:a1c430a332ead0b2674f1ef71b17f43b8139ec5a4201182766a21f131a31e021"},
|
||||
{file = "fastuuid-0.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:241fdd362fd96e6b337db62a65dd7cb3dfac20adf854573247a47510e192db6f"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e353c0a0d978a5ecd97171ac4fb7f55a6bd6cbae90f1ec4e828e5317f11b995e"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:904ac3eb37f4742e23f6a51be0d0451d1d3aceb50df8dac7afc6bf5209793650"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6b070e0dc1965d53b9e07c291537095ececf7d7e36e60aed9b22400fa6c5c7f"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0225f8bc78071a191cb458b3b0e23b04a7f03013575b8a3083da2a84c450e200"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f3f8f10b962cf2e11d3affc0cf2697ac5c9accc0d282dce981ed555a44ce15"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1ae87968614fe6d3029a8198671b5893341aac9459289e93d201027be9ea7e8"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6ca01f5e614530a1a858bf185dd5556805a4c11b6eba0a2536890b68ed954922"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d3b6b10d78b9f7056445ac377612443980349da7221a3dd3e3f382f7c437be3"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ae95e4dcf94775d948ebb843f4443d33cd224bb31174030e106ee3cab66527c"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-win32.whl", hash = "sha256:5d753bc9ba8de6dd9caa8bbac045578c2fbe1c6ae40c2026b614676776fbe9dc"},
|
||||
{file = "fastuuid-0.13.5-cp38-cp38-win_amd64.whl", hash = "sha256:f9530f1328b05b80c6fa111e7f2a5d55fa30fbbd72d708326d0c7b55b67ed772"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ed78153c589e5efb34faaa216836a5bf8a0b9d34e82183203166011238d9ed13"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a67e18c2d7fba8be6ea4aed8ca5a20fcf273f003efa01c1f33a096b72537e69e"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e362a3d66874d3d11a1ee9a8e717e32c2817cdb5d7a4e913290bf6e0f2a7fd8"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9baa9c33848ec0926231e7ecfef9e02faa0f6d24265b64108ea41f7a0bb3f48"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec6dab282162c19ec2172f33bafd467cffe26b92345789278adcbec19428d1"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2424a90688dbc44f119686fa452ff21aa106c9da258214f577816462ad606d5"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f8f2cc6972941ab030f3776961ed8454772c3acad88781fc262d71514df89973"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a41257ea172b5de199c3cfa71cc6c574dcf22367fe51e26cba0d359107f11f30"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cade5f3b8023dbba5a006e5685a7baf0d7a30c43cea17113768aa9ef9582d799"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-win32.whl", hash = "sha256:880f0d03ad2518b96757ca422cba6ff76cea5464db2b3ad75c32acf1890e058f"},
|
||||
{file = "fastuuid-0.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:ebe95b730f81808eabc90247ac3d412b96d9fae1c406760b163bb9f134b7af69"},
|
||||
{file = "fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4187,14 +4266,14 @@ dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[tom
|
||||
|
||||
[[package]]
|
||||
name = "libtmux"
|
||||
version = "0.39.0"
|
||||
version = "0.46.2"
|
||||
description = "Typed library that provides an ORM wrapper for tmux, a terminal multiplexer."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "libtmux-0.39.0-py3-none-any.whl", hash = "sha256:6b6e338be2727f67aa6b7eb67fa134368fa3c3eac5df27565396467692891c1e"},
|
||||
{file = "libtmux-0.39.0.tar.gz", hash = "sha256:59346aeef3c0d6017f3bc5e23248d43cdf50f32b775b9cb5d9ff5e2e5f3059f4"},
|
||||
{file = "libtmux-0.46.2-py3-none-any.whl", hash = "sha256:6c32dbf22bde8e5e33b2714a4295f6e838dc640f337cd4c085a044f6828c7793"},
|
||||
{file = "libtmux-0.46.2.tar.gz", hash = "sha256:9a398fec5d714129c8344555d466e1a903dfc0f741ba07aabe75a8ceb25c5dda"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4228,26 +4307,24 @@ valkey = ["valkey (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.76.1"
|
||||
version = "1.77.7"
|
||||
description = "Library to easily interface with LLM API providers"
|
||||
optional = false
|
||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||
python-versions = ">=3.8.1,<4.0, !=3.9.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "litellm-1.76.1-py3-none-any.whl", hash = "sha256:938f05075372f26098211ea9b3cb0a6bb7b46111330226b70d42d40bd307812f"},
|
||||
{file = "litellm-1.76.1.tar.gz", hash = "sha256:d5a3a3efda04999b60ec0d1c29c1eaaa12f89a7b29db4bda691c7fb55b4fa6ad"},
|
||||
]
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.10"
|
||||
click = "*"
|
||||
fastuuid = ">=0.12.0"
|
||||
fastuuid = ">=0.13.0"
|
||||
httpx = ">=0.23.0"
|
||||
importlib-metadata = ">=6.8.0"
|
||||
jinja2 = ">=3.1.2,<4.0.0"
|
||||
jsonschema = ">=4.22.0,<5.0.0"
|
||||
jinja2 = "^3.1.2"
|
||||
jsonschema = "^4.22.0"
|
||||
openai = ">=1.99.5"
|
||||
pydantic = ">=2.5.0,<3.0.0"
|
||||
pydantic = "^2.5.0"
|
||||
python-dotenv = ">=0.2.0"
|
||||
tiktoken = ">=0.7.0"
|
||||
tokenizers = "*"
|
||||
@@ -4256,10 +4333,16 @@ tokenizers = "*"
|
||||
caching = ["diskcache (>=5.6.1,<6.0.0)"]
|
||||
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-iam (>=2.19.1,<3.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"]
|
||||
mlflow = ["mlflow (>3.1.4) ; python_version >= \"3.10\""]
|
||||
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.36.0)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.19)", "litellm-proxy-extras (==0.2.18)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"]
|
||||
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.36.0)", "cryptography", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.20)", "litellm-proxy-extras (==0.2.25)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"]
|
||||
semantic-router = ["semantic-router ; python_version >= \"3.9\""]
|
||||
utils = ["numpydoc"]
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/BerriAI/litellm.git"
|
||||
reference = "v1.77.7.dev9"
|
||||
resolved_reference = "763d2f8ccdd8412dbe6d4ac0e136d9ac34dcd4c0"
|
||||
|
||||
[[package]]
|
||||
name = "llvmlite"
|
||||
version = "0.44.0"
|
||||
@@ -5430,9 +5513,36 @@ youtube-transcript-api = ">=0.6.2"
|
||||
[package.extras]
|
||||
llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0)", "llama-index-retrievers-bm25 (>=0.5.2,<0.6.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "openhands-agent-server"
|
||||
version = "1.0.0"
|
||||
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
aiosqlite = ">=0.19"
|
||||
alembic = ">=1.13"
|
||||
docker = ">=7.1,<8"
|
||||
fastapi = ">=0.104"
|
||||
pydantic = ">=2"
|
||||
sqlalchemy = ">=2"
|
||||
uvicorn = ">=0.31.1"
|
||||
websockets = ">=12"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/All-Hands-AI/agent-sdk.git"
|
||||
reference = "08cf609a996523c0199c61c768d74417b7e96109"
|
||||
resolved_reference = "08cf609a996523c0199c61c768d74417b7e96109"
|
||||
subdirectory = "openhands/agent_server"
|
||||
|
||||
[[package]]
|
||||
name = "openhands-ai"
|
||||
version = "0.57.0"
|
||||
version = "0.59.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
optional = false
|
||||
python-versions = "^3.12,<3.14"
|
||||
@@ -5444,6 +5554,7 @@ develop = true
|
||||
aiohttp = ">=3.9.0,!=3.11.13"
|
||||
anthropic = {version = "*", extras = ["vertex"]}
|
||||
anyio = "4.9.0"
|
||||
asyncpg = "^0.30.0"
|
||||
bashlex = "^0.18"
|
||||
boto3 = "*"
|
||||
browsergym-core = "0.13.3"
|
||||
@@ -5465,21 +5576,26 @@ joblib = "*"
|
||||
json-repair = "*"
|
||||
jupyter_kernel_gateway = "*"
|
||||
kubernetes = "^33.1.0"
|
||||
libtmux = ">=0.37,<0.40"
|
||||
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*"
|
||||
libtmux = ">=0.46.2"
|
||||
litellm = ">=1.74.3, <1.78.0, !=1.64.4, !=1.67.*"
|
||||
memory-profiler = "^0.61.0"
|
||||
numpy = "*"
|
||||
openai = "1.99.9"
|
||||
openhands-aci = "0.3.2"
|
||||
openhands-agent-server = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "08cf609a996523c0199c61c768d74417b7e96109", subdirectory = "openhands/agent_server"}
|
||||
openhands-sdk = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "08cf609a996523c0199c61c768d74417b7e96109", subdirectory = "openhands/sdk"}
|
||||
opentelemetry-api = "^1.33.1"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
|
||||
pathspec = "^0.12.1"
|
||||
pexpect = "*"
|
||||
pg8000 = "^1.31.5"
|
||||
pillow = "^11.3.0"
|
||||
playwright = "^1.55.0"
|
||||
poetry = "^2.1.2"
|
||||
prompt-toolkit = "^3.0.50"
|
||||
protobuf = "^5.0.0,<6.0.0"
|
||||
psutil = "*"
|
||||
pybase62 = "^1.0.0"
|
||||
pygithub = "^2.5.0"
|
||||
pyjwt = "^2.9.0"
|
||||
pylatexenc = "*"
|
||||
@@ -5488,6 +5604,7 @@ PyPDF2 = "*"
|
||||
python-docx = "*"
|
||||
python-dotenv = "*"
|
||||
python-frontmatter = "^1.1.0"
|
||||
python-jose = {version = ">=3.3", extras = ["cryptography"]}
|
||||
python-json-logger = "^3.2.1"
|
||||
python-multipart = "*"
|
||||
python-pptx = "*"
|
||||
@@ -5500,6 +5617,7 @@ redis = ">=5.2,<7.0"
|
||||
requests = "^2.32.5"
|
||||
setuptools = ">=78.1.1"
|
||||
shellingham = "^1.5.4"
|
||||
sqlalchemy = {version = "^2.0.40", extras = ["asyncio"]}
|
||||
sse-starlette = "^3.0.2"
|
||||
starlette = "^0.48.0"
|
||||
tenacity = ">=8.5,<10.0"
|
||||
@@ -5519,6 +5637,35 @@ third-party-runtimes = ["daytona (==0.24.2)", "e2b-code-interpreter (>=2.0.0,<3.
|
||||
type = "directory"
|
||||
url = ".."
|
||||
|
||||
[[package]]
|
||||
name = "openhands-sdk"
|
||||
version = "1.0.0"
|
||||
description = "OpenHands SDK - Core functionality for building AI agents"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
fastmcp = ">=2.11.3"
|
||||
litellm = {git = "https://github.com/BerriAI/litellm.git", rev = "v1.77.7.dev9"}
|
||||
pydantic = ">=2.11.7"
|
||||
python-frontmatter = ">=1.1.0"
|
||||
python-json-logger = ">=3.3.0"
|
||||
tenacity = ">=9.1.2"
|
||||
websockets = ">=12"
|
||||
|
||||
[package.extras]
|
||||
boto3 = ["boto3 (>=1.35.0)"]
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/All-Hands-AI/agent-sdk.git"
|
||||
reference = "08cf609a996523c0199c61c768d74417b7e96109"
|
||||
resolved_reference = "08cf609a996523c0199c61c768d74417b7e96109"
|
||||
subdirectory = "openhands/sdk"
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
@@ -5855,14 +6002,14 @@ ptyprocess = ">=0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pg8000"
|
||||
version = "1.31.4"
|
||||
version = "1.31.5"
|
||||
description = "PostgreSQL interface library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pg8000-1.31.4-py3-none-any.whl", hash = "sha256:d14fb2054642ee80f9a216721892e99e19db60a005358460ffa48872351423d4"},
|
||||
{file = "pg8000-1.31.4.tar.gz", hash = "sha256:e7ecce4339891f27b0b22e2f79eb9efe44118bd384207359fc18350f788ace00"},
|
||||
{file = "pg8000-1.31.5-py3-none-any.whl", hash = "sha256:0af2c1926b153307639868d2ee5cef6cd3a7d07448e12736989b10e1d491e201"},
|
||||
{file = "pg8000-1.31.5.tar.gz", hash = "sha256:46ebb03be52b7a77c03c725c79da2ca281d6e8f59577ca66b17c9009618cae78"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -6528,6 +6675,17 @@ files = [
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.6.1,<0.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pybase62"
|
||||
version = "1.0.0"
|
||||
description = "Python module for base62 encoding"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pybase62-1.0.0-py3-none-any.whl", hash = "sha256:60539ad956ec9e9de091bc7ae88c9550bc2fa17f503050cf34d021b75e73cb27"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.14.0"
|
||||
@@ -7122,6 +7280,30 @@ PyYAML = "*"
|
||||
docs = ["sphinx"]
|
||||
test = ["mypy", "pyaml", "pytest", "toml", "types-PyYAML", "types-toml"]
|
||||
|
||||
[[package]]
|
||||
name = "python-jose"
|
||||
version = "3.5.0"
|
||||
description = "JOSE implementation in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"},
|
||||
{file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""}
|
||||
ecdsa = "!=0.15"
|
||||
pyasn1 = ">=0.5.0"
|
||||
rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0"
|
||||
|
||||
[package.extras]
|
||||
cryptography = ["cryptography (>=3.4.0)"]
|
||||
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"]
|
||||
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "python-json-logger"
|
||||
version = "3.3.0"
|
||||
|
||||
@@ -13,7 +13,8 @@ from server.auth.auth_error import (
|
||||
ExpiredError,
|
||||
NoCredentialsError,
|
||||
)
|
||||
from server.auth.token_manager import TokenManager, get_config
|
||||
from server.auth.token_manager import TokenManager
|
||||
from server.config import get_config
|
||||
from server.logger import logger
|
||||
from server.rate_limit import RateLimiter, create_redis_rate_limiter
|
||||
from storage.api_key_store import ApiKeyStore
|
||||
@@ -223,6 +224,16 @@ class SaasUserAuth(UserAuth):
|
||||
await rate_limiter.hit('auth_uid', user_id)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
async def get_for_user(cls, user_id: str) -> UserAuth:
|
||||
offline_token = await token_manager.load_offline_token(user_id)
|
||||
assert offline_token is not None
|
||||
return SaasUserAuth(
|
||||
user_id=user_id,
|
||||
refresh_token=SecretStr(offline_token),
|
||||
auth_type=AuthType.BEARER,
|
||||
)
|
||||
|
||||
|
||||
def get_api_key_from_header(request: Request):
|
||||
auth_header = request.headers.get('Authorization')
|
||||
|
||||
@@ -26,6 +26,7 @@ from server.auth.constants import (
|
||||
KEYCLOAK_SERVER_URL_EXT,
|
||||
)
|
||||
from server.auth.keycloak_manager import get_keycloak_admin, get_keycloak_openid
|
||||
from server.config import get_config
|
||||
from server.logger import logger
|
||||
from sqlalchemy import String as SQLString
|
||||
from sqlalchemy import type_coerce
|
||||
@@ -35,19 +36,8 @@ from storage.github_app_installation import GithubAppInstallation
|
||||
from storage.offline_token_store import OfflineTokenStore
|
||||
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
|
||||
|
||||
from openhands.core.config import load_openhands_config
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
# Create a function to get config to avoid circular imports
|
||||
_config = None
|
||||
|
||||
|
||||
def get_config():
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = load_openhands_config()
|
||||
return _config
|
||||
|
||||
|
||||
def _before_sleep_callback(retry_state: RetryCallState) -> None:
|
||||
logger.info(f'Retry attempt {retry_state.attempt_number} for Keycloak operation')
|
||||
|
||||
@@ -19,10 +19,21 @@ from server.auth.constants import (
|
||||
GITLAB_APP_CLIENT_ID,
|
||||
)
|
||||
|
||||
from openhands.core.config.utils import load_openhands_config
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
# Create a function to get config to avoid circular imports
|
||||
_config = None
|
||||
|
||||
|
||||
def get_config():
|
||||
global _config
|
||||
if _config is None:
|
||||
_config = load_openhands_config()
|
||||
return _config
|
||||
|
||||
|
||||
def sign_token(payload: dict[str, object], jwt_secret: str, algorithm='HS256') -> str:
|
||||
"""Signs a JWT token."""
|
||||
|
||||
@@ -424,7 +424,7 @@ async def refresh_tokens(
|
||||
provider_handler = ProviderHandler(
|
||||
create_provider_tokens_object([provider]), external_auth_id=user_id
|
||||
)
|
||||
service = provider_handler._get_service(provider)
|
||||
service = provider_handler.get_service(provider)
|
||||
token = await service.get_latest_token()
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -784,6 +784,7 @@ class SaasNestedConversationManager(ConversationManager):
|
||||
env_vars['SKIP_DEPENDENCY_CHECK'] = '1'
|
||||
env_vars['INITIAL_NUM_WARM_SERVERS'] = '1'
|
||||
env_vars['INIT_GIT_IN_EMPTY_WORKSPACE'] = '1'
|
||||
env_vars['ENABLE_V1'] = '0'
|
||||
|
||||
# We need this for LLM traces tracking to identify the source of the LLM calls
|
||||
env_vars['WEB_HOST'] = WEB_HOST
|
||||
|
||||
@@ -195,14 +195,11 @@ def update_active_working_seconds(
|
||||
file_store: The FileStore instance for accessing conversation data
|
||||
"""
|
||||
try:
|
||||
# Get all events for the conversation
|
||||
events = list(event_store.get_events())
|
||||
|
||||
# Track agent state changes and calculate running time
|
||||
running_start_time = None
|
||||
total_running_seconds = 0.0
|
||||
|
||||
for event in events:
|
||||
for event in event_store.search_events():
|
||||
if isinstance(event, AgentStateChangedObservation) and event.timestamp:
|
||||
event_timestamp = datetime.fromisoformat(event.timestamp).timestamp()
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
Unified SQLAlchemy declarative base for all models.
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from openhands.app_server.utils.sql_utils import Base
|
||||
|
||||
Base = declarative_base()
|
||||
__all__ = ['Base']
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from google.cloud.sql.connector import Connector
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -26,6 +25,8 @@ def _get_db_engine():
|
||||
if GCP_DB_INSTANCE: # GCP environments
|
||||
|
||||
def get_db_connection():
|
||||
from google.cloud.sql.connector import Connector
|
||||
|
||||
connector = Connector()
|
||||
instance_string = f'{GCP_PROJECT}:{GCP_REGION}:{GCP_DB_INSTANCE}'
|
||||
return connector.connect(
|
||||
@@ -52,6 +53,8 @@ def _get_db_engine():
|
||||
|
||||
|
||||
async def async_creator():
|
||||
from google.cloud.sql.connector import Connector
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
async with Connector(loop=loop) as connector:
|
||||
conn = await connector.connect_async(
|
||||
|
||||
@@ -52,6 +52,14 @@ class SaasConversationStore(ConversationStore):
|
||||
# Convert string to ProviderType enum
|
||||
kwargs['git_provider'] = ProviderType(kwargs['git_provider'])
|
||||
|
||||
# Remove V1 attributes
|
||||
kwargs.pop('max_budget_per_task', None)
|
||||
kwargs.pop('cache_read_tokens', None)
|
||||
kwargs.pop('cache_write_tokens', None)
|
||||
kwargs.pop('reasoning_tokens', None)
|
||||
kwargs.pop('context_window', None)
|
||||
kwargs.pop('per_turn_token', None)
|
||||
|
||||
return ConversationMetadata(**kwargs)
|
||||
|
||||
async def save_metadata(self, metadata: ConversationMetadata):
|
||||
|
||||
@@ -1,41 +1,8 @@
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from openhands.app_server.app_conversation.sql_app_conversation_info_service import (
|
||||
StoredConversationMetadata as _StoredConversationMetadata,
|
||||
)
|
||||
|
||||
from sqlalchemy import JSON, Column, DateTime, Float, Integer, String
|
||||
from storage.base import Base
|
||||
StoredConversationMetadata = _StoredConversationMetadata
|
||||
|
||||
|
||||
class StoredConversationMetadata(Base): # type: ignore
|
||||
__tablename__ = 'conversation_metadata'
|
||||
conversation_id = Column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
github_user_id = Column(String, nullable=True) # The GitHub user ID
|
||||
user_id = Column(String, nullable=False) # The Keycloak User ID
|
||||
selected_repository = Column(String, nullable=True)
|
||||
selected_branch = Column(String, nullable=True)
|
||||
git_provider = Column(
|
||||
String, nullable=True
|
||||
) # The git provider (GitHub, GitLab, etc.)
|
||||
title = Column(String, nullable=True)
|
||||
last_updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC), # type: ignore[attr-defined]
|
||||
)
|
||||
created_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC), # type: ignore[attr-defined]
|
||||
)
|
||||
trigger = Column(String, nullable=True)
|
||||
pr_number = Column(
|
||||
JSON, nullable=True
|
||||
) # List of PR numbers associated with the conversation
|
||||
|
||||
# Cost and token metrics
|
||||
accumulated_cost = Column(Float, default=0.0)
|
||||
prompt_tokens = Column(Integer, default=0)
|
||||
completion_tokens = Column(Integer, default=0)
|
||||
total_tokens = Column(Integer, default=0)
|
||||
|
||||
# LLM model used for the conversation
|
||||
llm_model = Column(String, nullable=True)
|
||||
__all__ = ['StoredConversationMetadata']
|
||||
|
||||
@@ -80,7 +80,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
events.append(event6)
|
||||
|
||||
# Configure the mock event store to return our test events
|
||||
mock_event_store.get_events.return_value = events
|
||||
mock_event_store.search_events.return_value = events
|
||||
|
||||
# Call the function under test with mocked session_maker
|
||||
with patch(
|
||||
@@ -133,7 +133,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
|
||||
events = [event1, event2]
|
||||
|
||||
mock_event_store.get_events.return_value = events
|
||||
mock_event_store.search_events.return_value = events
|
||||
|
||||
# Call the function under test with mocked session_maker
|
||||
with patch(
|
||||
@@ -178,7 +178,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
events = [event1, event2, event3]
|
||||
# No final state change - agent still running
|
||||
|
||||
mock_event_store.get_events.return_value = events
|
||||
mock_event_store.search_events.return_value = events
|
||||
|
||||
# Call the function under test with mocked session_maker
|
||||
with patch(
|
||||
@@ -221,7 +221,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
|
||||
events = [event1, event2, event3]
|
||||
|
||||
mock_event_store.get_events.return_value = events
|
||||
mock_event_store.search_events.return_value = events
|
||||
|
||||
# Call the function under test with mocked session_maker
|
||||
with patch(
|
||||
@@ -267,7 +267,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
|
||||
events = [event1, event2, event3, event4]
|
||||
|
||||
mock_event_store.get_events.return_value = events
|
||||
mock_event_store.search_events.return_value = events
|
||||
|
||||
# Call the function under test with mocked session_maker
|
||||
with patch(
|
||||
@@ -297,7 +297,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
user_id = 'test_user_error'
|
||||
|
||||
# Configure the mock to raise an exception
|
||||
mock_event_store.get_events.side_effect = Exception('Test error')
|
||||
mock_event_store.search_events.side_effect = Exception('Test error')
|
||||
|
||||
# Call the function under test
|
||||
update_active_working_seconds(
|
||||
@@ -376,7 +376,7 @@ class TestUpdateActiveWorkingSeconds:
|
||||
event10.timestamp = '1970-01-01T00:00:37.000000'
|
||||
events.append(event10)
|
||||
|
||||
mock_event_store.get_events.return_value = events
|
||||
mock_event_store.search_events.return_value = events
|
||||
|
||||
# Call the function under test with mocked session_maker
|
||||
with patch(
|
||||
|
||||
@@ -20,7 +20,7 @@ def token_store(session_maker, mock_config):
|
||||
|
||||
@pytest.fixture
|
||||
def token_manager():
|
||||
with patch('server.auth.token_manager.get_config') as mock_get_config:
|
||||
with patch('server.config.get_config') as mock_get_config:
|
||||
mock_config = mock_get_config.return_value
|
||||
mock_config.jwt_secret.get_secret_value.return_value = 'test_secret'
|
||||
return TokenManager(external=False)
|
||||
|
||||
@@ -8,7 +8,7 @@ from openhands.integrations.service_types import ProviderType
|
||||
|
||||
@pytest.fixture
|
||||
def token_manager():
|
||||
with patch('server.auth.token_manager.get_config') as mock_get_config:
|
||||
with patch('server.config.get_config') as mock_get_config:
|
||||
mock_config = mock_get_config.return_value
|
||||
mock_config.jwt_secret.get_secret_value.return_value = 'test_secret'
|
||||
return TokenManager(external=False)
|
||||
|
||||
@@ -307,7 +307,7 @@ class TheoremqaTask(Task):
|
||||
|
||||
# Converting the string answer to a number/list/bool/option
|
||||
try:
|
||||
prediction = eval(prediction)
|
||||
prediction = ast.literal_eval(prediction)
|
||||
except Exception:
|
||||
LOGGER.warning(
|
||||
f'[TASK] Failed to convert the answer: {prediction}\n{traceback.format_exc()}'
|
||||
|
||||
@@ -111,15 +111,10 @@ for run_idx in $(seq 1 $N_RUNS); do
|
||||
echo "### Evaluating on $OUTPUT_FILE ... ###"
|
||||
OUTPUT_CONFIG_FILE="${OUTPUT_FILE%.jsonl}_config.json"
|
||||
export EVAL_SKIP_BUILD_ERRORS=true
|
||||
pip install multi-swe-bench --quiet --disable-pip-version-check > /dev/null 2>&1
|
||||
COMMAND="poetry run python ./evaluation/benchmarks/multi_swe_bench/scripts/eval/update_multi_swe_bench_config.py --input $OUTPUT_FILE --output $OUTPUT_CONFIG_FILE --dataset $EVAL_DATASET;
|
||||
python -m multi_swe_bench.harness.run_evaluation --config $OUTPUT_CONFIG_FILE
|
||||
poetry run python -m multi_swe_bench.harness.run_evaluation --config $OUTPUT_CONFIG_FILE
|
||||
"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
echo "Running command: $COMMAND"
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
|
||||
@@ -16,6 +16,10 @@ At the end, you must test your code rigorously using the tools provided, and do
|
||||
|
||||
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||
|
||||
## Issue Description
|
||||
|
||||
{{ instance.problem_statement }}
|
||||
|
||||
# Workflow
|
||||
|
||||
## High-Level Problem Solving Strategy
|
||||
@@ -73,6 +77,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
|
||||
## 8. Final Reflection and Additional Testing
|
||||
- Reflect carefully on the original intent of the user and the problem statement.
|
||||
- Compare your changes with the base commit {{ instance.base_commit }} to ensure minimal and focused modifications.
|
||||
- Think about potential edge cases or scenarios that may not be covered by existing tests.
|
||||
- Write additional tests that would need to pass to fully validate the correctness of your solution.
|
||||
- Run these new tests and ensure they all pass.
|
||||
|
||||
@@ -24,8 +24,8 @@ from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
OpenHandsConfig,
|
||||
get_evaluation_parser,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
@@ -166,7 +166,8 @@ def load_integration_tests() -> pd.DataFrame:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_arguments()
|
||||
parser = get_evaluation_parser()
|
||||
args, _ = parser.parse_known_args()
|
||||
integration_tests = load_integration_tests()
|
||||
|
||||
llm_config = None
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { act } from "@testing-library/react";
|
||||
import { vi, afterEach } from "vitest";
|
||||
import type * as ZustandExportedTypes from "zustand";
|
||||
|
||||
export * from "zustand";
|
||||
|
||||
const { create: actualCreate, createStore: actualCreateStore } =
|
||||
await vi.importActual<typeof ZustandExportedTypes>("zustand");
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
export const storeResetFns = new Set<() => void>();
|
||||
|
||||
const createUncurried = <T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) => {
|
||||
const store = actualCreate(stateCreator);
|
||||
const initialState = store.getInitialState();
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true);
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const create = (<T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) =>
|
||||
// to support curried version of create
|
||||
typeof stateCreator === "function"
|
||||
? createUncurried(stateCreator)
|
||||
: createUncurried) as typeof ZustandExportedTypes.create;
|
||||
|
||||
const createStoreUncurried = <T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) => {
|
||||
const store = actualCreateStore(stateCreator);
|
||||
const initialState = store.getInitialState();
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true);
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const createStore = (<T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) =>
|
||||
// to support curried version of createStore
|
||||
typeof stateCreator === "function"
|
||||
? createStoreUncurried(stateCreator)
|
||||
: createStoreUncurried) as typeof ZustandExportedTypes.createStore;
|
||||
|
||||
// reset all stores after each test run
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
storeResetFns.forEach((resetFn) => {
|
||||
resetFn();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
|
||||
// Mock the hooks
|
||||
vi.mock("#/context/ws-client-provider");
|
||||
@@ -176,7 +177,7 @@ describe("ChatInterface - Chat Suggestions", () => {
|
||||
});
|
||||
|
||||
test("should hide chat suggestions when there is a user message", () => {
|
||||
const userEvent: OpenHandsAction = {
|
||||
const mockUserEvent: OpenHandsAction = {
|
||||
id: 1,
|
||||
source: "user",
|
||||
action: "message",
|
||||
@@ -189,10 +190,11 @@ describe("ChatInterface - Chat Suggestions", () => {
|
||||
timestamp: "2025-07-01T00:00:00Z",
|
||||
};
|
||||
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: false,
|
||||
parsedEvents: [userEvent],
|
||||
useEventStore.setState({
|
||||
events: [mockUserEvent],
|
||||
uiEvents: [],
|
||||
addEvent: vi.fn(),
|
||||
clearEvents: vi.fn(),
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />, queryClient);
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
|
||||
import { screen, waitFor, render } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import {
|
||||
createMockMessageEvent,
|
||||
createMockUserMessageEvent,
|
||||
createMockAgentErrorEvent,
|
||||
} from "#/mocks/mock-ws-helpers";
|
||||
import {
|
||||
ConnectionStatusComponent,
|
||||
EventStoreComponent,
|
||||
OptimisticUserMessageStoreComponent,
|
||||
ErrorMessageStoreComponent,
|
||||
} from "./helpers/websocket-test-components";
|
||||
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
|
||||
import { conversationWebSocketTestSetup } from "./helpers/msw-websocket-setup";
|
||||
|
||||
// MSW WebSocket mock setup
|
||||
const { wsLink, server: mswServer } = conversationWebSocketTestSetup();
|
||||
|
||||
beforeAll(() => mswServer.listen());
|
||||
afterEach(() => {
|
||||
mswServer.resetHandlers();
|
||||
});
|
||||
afterAll(() => mswServer.close());
|
||||
|
||||
// Helper function to render components with ConversationWebSocketProvider
|
||||
function renderWithWebSocketContext(
|
||||
children: React.ReactNode,
|
||||
conversationId = "test-conversation-default",
|
||||
) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConversationWebSocketProvider conversationId={conversationId}>
|
||||
{children}
|
||||
</ConversationWebSocketProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("Conversation WebSocket Handler", () => {
|
||||
// 1. Connection Lifecycle Tests
|
||||
describe("Connection Management", () => {
|
||||
it("should establish WebSocket connection to /events/socket URL", async () => {
|
||||
// This will fail because we haven't created the context yet
|
||||
renderWithWebSocketContext(<ConnectionStatusComponent />);
|
||||
|
||||
// Initially should be CONNECTING
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"CONNECTING",
|
||||
);
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"OPEN",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.todo("should provide manual disconnect functionality");
|
||||
});
|
||||
|
||||
// 2. Event Processing Tests
|
||||
describe("Event Stream Processing", () => {
|
||||
it("should update event store with received WebSocket events", async () => {
|
||||
// Create a mock MessageEvent to send through WebSocket
|
||||
const mockMessageEvent = createMockMessageEvent();
|
||||
|
||||
// Set up MSW to send the event when connection is established
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
// Send the mock event after connection
|
||||
client.send(JSON.stringify(mockMessageEvent));
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and event store
|
||||
renderWithWebSocketContext(<EventStoreComponent />);
|
||||
|
||||
// Wait for connection and event processing
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("events-count")).toHaveTextContent("1");
|
||||
});
|
||||
|
||||
// Verify the event was added to the store
|
||||
expect(screen.getByTestId("latest-event-id")).toHaveTextContent(
|
||||
"test-event-123",
|
||||
);
|
||||
expect(screen.getByTestId("ui-events-count")).toHaveTextContent("1");
|
||||
});
|
||||
|
||||
it("should handle malformed/invalid event data gracefully", async () => {
|
||||
// Set up MSW to send various invalid events when connection is established
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
|
||||
// Send invalid JSON
|
||||
client.send("invalid json string");
|
||||
|
||||
// Send valid JSON but missing required fields
|
||||
client.send(JSON.stringify({ message: "missing required fields" }));
|
||||
|
||||
// Send valid JSON with wrong data types
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
id: 123, // should be string
|
||||
timestamp: "2023-01-01T00:00:00Z",
|
||||
source: "agent",
|
||||
}),
|
||||
);
|
||||
|
||||
// Send null values for required fields
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
id: null,
|
||||
timestamp: "2023-01-01T00:00:00Z",
|
||||
source: "agent",
|
||||
}),
|
||||
);
|
||||
|
||||
// Send a valid event after invalid ones to ensure processing continues
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
id: "valid-event-123",
|
||||
timestamp: new Date().toISOString(),
|
||||
source: "agent",
|
||||
llm_message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "Valid message after invalid ones" },
|
||||
],
|
||||
},
|
||||
activated_microagents: [],
|
||||
extended_content: [],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and event store
|
||||
renderWithWebSocketContext(<EventStoreComponent />);
|
||||
|
||||
// Wait for connection and event processing
|
||||
// Only the valid event should be added to the store
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("events-count")).toHaveTextContent("1");
|
||||
});
|
||||
|
||||
// Verify only the valid event was added
|
||||
expect(screen.getByTestId("latest-event-id")).toHaveTextContent(
|
||||
"valid-event-123",
|
||||
);
|
||||
expect(screen.getByTestId("ui-events-count")).toHaveTextContent("1");
|
||||
});
|
||||
});
|
||||
|
||||
// 3. State Management Tests
|
||||
describe("State Management Integration", () => {
|
||||
it("should clear optimistic user messages when confirmed", async () => {
|
||||
// First, set an optimistic user message
|
||||
const { setOptimisticUserMessage } =
|
||||
useOptimisticUserMessageStore.getState();
|
||||
setOptimisticUserMessage("This is an optimistic message");
|
||||
|
||||
// Create a mock user MessageEvent to send through WebSocket
|
||||
const mockUserMessageEvent = createMockUserMessageEvent();
|
||||
|
||||
// Set up MSW to send the user message event when connection is established
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
// Send the mock user message event after connection
|
||||
client.send(JSON.stringify(mockUserMessageEvent));
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and optimistic user message store
|
||||
renderWithWebSocketContext(<OptimisticUserMessageStoreComponent />);
|
||||
|
||||
// Initially should show the optimistic message
|
||||
expect(screen.getByTestId("optimistic-user-message")).toHaveTextContent(
|
||||
"This is an optimistic message",
|
||||
);
|
||||
|
||||
// Wait for connection and user message event processing
|
||||
// The optimistic message should be cleared when user message is confirmed
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("optimistic-user-message")).toHaveTextContent(
|
||||
"none",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Cache Management Tests
|
||||
describe("Cache Management", () => {
|
||||
it.todo(
|
||||
"should invalidate file changes cache on file edit/write/command events",
|
||||
);
|
||||
it.todo("should invalidate specific file diff cache on file modifications");
|
||||
it.todo("should prevent cache refetch during high message rates");
|
||||
it.todo("should not invalidate cache for non-file-related events");
|
||||
it.todo("should invalidate cache with correct conversation ID context");
|
||||
});
|
||||
|
||||
// 5. Error Handling Tests
|
||||
describe("Error Handling & Recovery", () => {
|
||||
it("should update error message store on AgentErrorEvent", async () => {
|
||||
// Create a mock AgentErrorEvent to send through WebSocket
|
||||
const mockAgentErrorEvent = createMockAgentErrorEvent();
|
||||
|
||||
// Set up MSW to send the error event when connection is established
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
// Send the mock error event after connection
|
||||
client.send(JSON.stringify(mockAgentErrorEvent));
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and error message store
|
||||
renderWithWebSocketContext(<ErrorMessageStoreComponent />);
|
||||
|
||||
// Initially should show "none"
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
|
||||
|
||||
// Wait for connection and error event processing
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent(
|
||||
"Failed to execute command: Permission denied",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set error message store on WebSocket connection errors", async () => {
|
||||
// Set up MSW to simulate connection error
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client }) => {
|
||||
// Simulate connection error by closing immediately
|
||||
client.close(1006, "Connection failed");
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and error message store
|
||||
renderWithWebSocketContext(
|
||||
<>
|
||||
<ErrorMessageStoreComponent />
|
||||
<ConnectionStatusComponent />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Initially should show "none"
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
|
||||
|
||||
// Wait for connection error and error message to be set
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"CLOSED",
|
||||
);
|
||||
});
|
||||
|
||||
// Should set error message on connection failure
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("error-message")).not.toHaveTextContent(
|
||||
"none",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set error message store on WebSocket disconnect with error", async () => {
|
||||
// Set up MSW to connect first, then disconnect with error
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
|
||||
// Simulate disconnect with error after a short delay
|
||||
setTimeout(() => {
|
||||
client.close(1006, "Unexpected disconnect");
|
||||
}, 100);
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and error message store
|
||||
renderWithWebSocketContext(
|
||||
<>
|
||||
<ErrorMessageStoreComponent />
|
||||
<ConnectionStatusComponent />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Initially should show "none"
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
|
||||
|
||||
// Wait for connection to be established first
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"OPEN",
|
||||
);
|
||||
});
|
||||
|
||||
// Wait for disconnect and error message to be set
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"CLOSED",
|
||||
);
|
||||
});
|
||||
|
||||
// Should set error message on unexpected disconnect
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("error-message")).not.toHaveTextContent(
|
||||
"none",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear error message store when connection is restored", async () => {
|
||||
let connectionAttempt = 0;
|
||||
|
||||
// Set up MSW to fail first connection, then succeed on retry
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
connectionAttempt += 1;
|
||||
|
||||
if (connectionAttempt === 1) {
|
||||
// First attempt fails
|
||||
client.close(1006, "Initial connection failed");
|
||||
} else {
|
||||
// Second attempt succeeds
|
||||
server.connect();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Render components that use both WebSocket and error message store
|
||||
renderWithWebSocketContext(
|
||||
<>
|
||||
<ErrorMessageStoreComponent />
|
||||
<ConnectionStatusComponent />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Initially should show "none"
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
|
||||
|
||||
// Wait for first connection failure and error message
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"CLOSED",
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("error-message")).not.toHaveTextContent(
|
||||
"none",
|
||||
);
|
||||
});
|
||||
|
||||
// Simulate reconnection attempt (this would normally be triggered by the WebSocket context)
|
||||
// For now, we'll just verify the pattern - when connection is restored, error should clear
|
||||
// This test will fail until the WebSocket handler implements the clear logic
|
||||
|
||||
// Note: This test demonstrates the expected behavior but may need adjustment
|
||||
// based on how the actual reconnection logic is implemented
|
||||
});
|
||||
|
||||
it.todo("should track and display errors with proper metadata");
|
||||
it.todo("should set appropriate error states on connection failures");
|
||||
it.todo(
|
||||
"should handle WebSocket close codes appropriately (1000, 1006, etc.)",
|
||||
);
|
||||
});
|
||||
|
||||
// 6. Connection State Validation Tests
|
||||
describe("Connection State Management", () => {
|
||||
it.todo("should only connect when conversation is in RUNNING status");
|
||||
it.todo("should handle STARTING conversation state appropriately");
|
||||
it.todo("should disconnect when conversation is STOPPED");
|
||||
it.todo("should validate runtime status before connecting");
|
||||
});
|
||||
|
||||
// 7. Message Sending Tests
|
||||
describe("Message Sending", () => {
|
||||
it.todo("should send user actions through WebSocket when connected");
|
||||
it.todo("should handle send attempts when disconnected");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
# Test Helpers
|
||||
|
||||
This directory contains reusable test utilities and components for the OpenHands frontend test suite.
|
||||
|
||||
## Files
|
||||
|
||||
### `websocket-test-components.tsx`
|
||||
Contains React test components for accessing and displaying WebSocket-related store values:
|
||||
- `ConnectionStatusComponent` - Displays WebSocket connection state
|
||||
- `EventStoreComponent` - Displays event store values (events count, UI events count, latest event ID)
|
||||
- `OptimisticUserMessageStoreComponent` - Displays optimistic user message store values
|
||||
- `ErrorMessageStoreComponent` - Displays error message store values
|
||||
|
||||
These components are designed to be used in tests to verify that WebSocket events are properly processed and stored.
|
||||
|
||||
|
||||
|
||||
### `msw-websocket-setup.ts`
|
||||
Contains MSW (Mock Service Worker) setup utilities for WebSocket testing:
|
||||
- `createWebSocketLink()` - Creates a WebSocket link for MSW testing
|
||||
- `createWebSocketMockServer()` - Creates and configures an MSW server for WebSocket testing
|
||||
- `createWebSocketTestSetup()` - Creates a complete WebSocket testing setup
|
||||
- `conversationWebSocketTestSetup()` - Standard setup for conversation WebSocket handler tests
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import {
|
||||
ConnectionStatusComponent,
|
||||
EventStoreComponent,
|
||||
} from "./__tests__/helpers/websocket-test-components";
|
||||
import { conversationWebSocketTestSetup } from "./__tests__/helpers/msw-websocket-setup";
|
||||
|
||||
// Set up MSW server
|
||||
const { wsLink, server } = conversationWebSocketTestSetup();
|
||||
|
||||
// Render components with WebSocket context (helper function defined in test file)
|
||||
renderWithWebSocketContext(<ConnectionStatusComponent />);
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Reusability**: Test components and utilities can be shared across multiple test files
|
||||
- **Maintainability**: Changes to test setup only need to be made in one place
|
||||
- **Consistency**: Ensures consistent test setup across different WebSocket-related tests
|
||||
- **Readability**: Test files are cleaner and focus on test logic rather than setup boilerplate
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ws } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
|
||||
/**
|
||||
* Creates a WebSocket link for MSW testing
|
||||
* @param url - WebSocket URL to mock (default: "ws://localhost/events/socket")
|
||||
* @returns MSW WebSocket link
|
||||
*/
|
||||
export const createWebSocketLink = (url = "ws://localhost/events/socket") =>
|
||||
ws.link(url);
|
||||
|
||||
/**
|
||||
* Creates and configures an MSW server for WebSocket testing
|
||||
* @param wsLink - WebSocket link to use for the server
|
||||
* @returns Configured MSW server
|
||||
*/
|
||||
export const createWebSocketMockServer = (wsLink: ReturnType<typeof ws.link>) =>
|
||||
setupServer(
|
||||
wsLink.addEventListener("connection", ({ server }) => {
|
||||
server.connect();
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates a complete WebSocket testing setup with server and link
|
||||
* @param url - WebSocket URL to mock (default: "ws://localhost/events/socket")
|
||||
* @returns Object containing the WebSocket link and configured server
|
||||
*/
|
||||
export const createWebSocketTestSetup = (
|
||||
url = "ws://localhost/events/socket",
|
||||
) => {
|
||||
const wsLink = createWebSocketLink(url);
|
||||
const server = createWebSocketMockServer(wsLink);
|
||||
|
||||
return { wsLink, server };
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard WebSocket test setup for conversation WebSocket handler tests
|
||||
*/
|
||||
export const conversationWebSocketTestSetup = () =>
|
||||
createWebSocketTestSetup("ws://localhost/events/socket");
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import { useConversationWebSocket } from "#/contexts/conversation-websocket-context";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { isV1Event } from "#/types/v1/type-guards";
|
||||
import { OpenHandsEvent } from "#/types/v1/core";
|
||||
|
||||
/**
|
||||
* Test component to access and display WebSocket connection state
|
||||
*/
|
||||
export function ConnectionStatusComponent() {
|
||||
const { connectionState } = useConversationWebSocket();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="connection-state">{connectionState}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test component to access and display event store values
|
||||
*/
|
||||
export function EventStoreComponent() {
|
||||
const { events, uiEvents } = useEventStore();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="events-count">{events.length}</div>
|
||||
<div data-testid="ui-events-count">{uiEvents.length}</div>
|
||||
<div data-testid="latest-event-id">
|
||||
{isV1Event(events[events.length - 1])
|
||||
? (events[events.length - 1] as OpenHandsEvent).id
|
||||
: "none"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test component to access and display optimistic user message store values
|
||||
*/
|
||||
export function OptimisticUserMessageStoreComponent() {
|
||||
const { optimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="optimistic-user-message">
|
||||
{optimisticUserMessage || "none"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test component to access and display error message store values
|
||||
*/
|
||||
export function ErrorMessageStoreComponent() {
|
||||
const { errorMessage } = useErrorMessageStore();
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="error-message">{errorMessage || "none"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach } from "node:test";
|
||||
import { beforeAll, describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
// Mock the WsClient context
|
||||
vi.mock("#/context/ws-client-provider", () => ({
|
||||
@@ -16,15 +13,7 @@ vi.mock("#/context/ws-client-provider", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
interface TestTerminalComponentProps {
|
||||
commands: Command[];
|
||||
}
|
||||
|
||||
function TestTerminalComponent({ commands }: TestTerminalComponentProps) {
|
||||
// Set commands in Zustand store
|
||||
useCommandStore.setState({ commands });
|
||||
// Set agent state in Zustand store
|
||||
useAgentStore.setState({ curAgentState: AgentState.RUNNING });
|
||||
function TestTerminalComponent() {
|
||||
const ref = useTerminal();
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
@@ -57,10 +46,12 @@ describe("useTerminal", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset command store between tests
|
||||
useCommandStore.setState({ commands: [] });
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<TestTerminalComponent commands={[]} />);
|
||||
renderWithProviders(<TestTerminalComponent />);
|
||||
});
|
||||
|
||||
it("should render the commands in the terminal", () => {
|
||||
@@ -69,26 +60,12 @@ describe("useTerminal", () => {
|
||||
{ content: "hello", type: "output" },
|
||||
];
|
||||
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />);
|
||||
// Set commands in store before rendering to ensure they're picked up during initialization
|
||||
useCommandStore.setState({ commands });
|
||||
|
||||
renderWithProviders(<TestTerminalComponent />);
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
|
||||
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", () => {
|
||||
const secret = "super_secret_github_token";
|
||||
const anotherSecret = "super_secret_another_token";
|
||||
const commands: Command[] = [
|
||||
{
|
||||
content: `export GITHUB_TOKEN=${secret},${anotherSecret},${secret}`,
|
||||
type: "input",
|
||||
},
|
||||
{ content: secret, type: "output" },
|
||||
];
|
||||
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />);
|
||||
|
||||
// This test is no longer relevant as secrets filtering has been removed
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
afterEach,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { ws } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
import { useWebSocket } from "#/hooks/use-websocket";
|
||||
|
||||
describe.skip("useWebSocket", () => {
|
||||
// MSW WebSocket mock setup
|
||||
const wsLink = ws.link("ws://acme.com/ws");
|
||||
|
||||
const mswServer = setupServer(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
// Establish the connection
|
||||
server.connect();
|
||||
|
||||
// Send a welcome message to confirm connection
|
||||
client.send("Welcome to the WebSocket!");
|
||||
}),
|
||||
);
|
||||
|
||||
beforeAll(() => mswServer.listen());
|
||||
afterEach(() => mswServer.resetHandlers());
|
||||
afterAll(() => mswServer.close());
|
||||
|
||||
it("should establish a WebSocket connection", async () => {
|
||||
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
|
||||
|
||||
// Initially should not be connected
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
expect(result.current.lastMessage).toBe(null);
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// Should receive the welcome message from our mock
|
||||
await waitFor(() => {
|
||||
expect(result.current.lastMessage).toBe("Welcome to the WebSocket!");
|
||||
});
|
||||
|
||||
// Confirm that the WebSocket connection is established when the hook is used
|
||||
expect(result.current.socket).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should handle incoming messages correctly", async () => {
|
||||
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// Should receive the welcome message from our mock
|
||||
await waitFor(() => {
|
||||
expect(result.current.lastMessage).toBe("Welcome to the WebSocket!");
|
||||
});
|
||||
|
||||
// Send another message from the mock server
|
||||
wsLink.broadcast("Hello from server!");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.lastMessage).toBe("Hello from server!");
|
||||
});
|
||||
|
||||
// Should have a messages array with all received messages
|
||||
expect(result.current.messages).toEqual([
|
||||
"Welcome to the WebSocket!",
|
||||
"Hello from server!",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle connection errors gracefully", async () => {
|
||||
// Create a mock that will simulate an error
|
||||
const errorLink = ws.link("ws://error-test.com/ws");
|
||||
mswServer.use(
|
||||
errorLink.addEventListener("connection", ({ client }) => {
|
||||
// Simulate an error by closing the connection immediately
|
||||
client.close(1006, "Connection failed");
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useWebSocket("ws://error-test.com/ws"));
|
||||
|
||||
// Initially should not be connected and no error
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
|
||||
// Wait for the connection to fail
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
});
|
||||
|
||||
// Should have error information (the close event should trigger error state)
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).not.toBe(null);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
// Should have meaningful error message (could be from onerror or onclose)
|
||||
expect(
|
||||
result.current.error?.message.includes("WebSocket closed with code 1006"),
|
||||
).toBe(true);
|
||||
|
||||
// Should not crash the application
|
||||
expect(result.current.socket).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should close the WebSocket connection on unmount", async () => {
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useWebSocket("ws://acme.com/ws"),
|
||||
);
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// Verify connection is active
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
expect(result.current.socket).toBeTruthy();
|
||||
|
||||
const closeSpy = vi.spyOn(result.current.socket!, "close");
|
||||
|
||||
// Unmount the component (this should trigger the useEffect cleanup)
|
||||
unmount();
|
||||
|
||||
// Verify that WebSocket close was called during cleanup
|
||||
expect(closeSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should support query parameters in WebSocket URL", async () => {
|
||||
const baseUrl = "ws://acme.com/ws";
|
||||
const queryParams = {
|
||||
token: "abc123",
|
||||
userId: "user456",
|
||||
version: "v1",
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useWebSocket(baseUrl, { queryParams }));
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// Verify that the WebSocket was created with query parameters
|
||||
expect(result.current.socket).toBeTruthy();
|
||||
expect(result.current.socket!.url).toBe(
|
||||
"ws://acme.com/ws?token=abc123&userId=user456&version=v1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should call onOpen handler when WebSocket connection opens", async () => {
|
||||
const onOpenSpy = vi.fn();
|
||||
const options = { onOpen: onOpenSpy };
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocket("ws://acme.com/ws", options),
|
||||
);
|
||||
|
||||
// Initially should not be connected
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
expect(onOpenSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// onOpen handler should have been called
|
||||
expect(onOpenSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should call onClose handler when WebSocket connection closes", async () => {
|
||||
const onCloseSpy = vi.fn();
|
||||
const options = { onClose: onCloseSpy };
|
||||
|
||||
const { result, unmount } = renderHook(() =>
|
||||
useWebSocket("ws://acme.com/ws", options),
|
||||
);
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
expect(onCloseSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Unmount to trigger close
|
||||
unmount();
|
||||
|
||||
// Wait for onClose handler to be called
|
||||
await waitFor(() => {
|
||||
expect(onCloseSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call onMessage handler when WebSocket receives a message", async () => {
|
||||
const onMessageSpy = vi.fn();
|
||||
const options = { onMessage: onMessageSpy };
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocket("ws://acme.com/ws", options),
|
||||
);
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// Should receive the welcome message from our mock
|
||||
await waitFor(() => {
|
||||
expect(result.current.lastMessage).toBe("Welcome to the WebSocket!");
|
||||
});
|
||||
|
||||
// onMessage handler should have been called for the welcome message
|
||||
expect(onMessageSpy).toHaveBeenCalledOnce();
|
||||
|
||||
// Send another message from the mock server
|
||||
wsLink.broadcast("Hello from server!");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.lastMessage).toBe("Hello from server!");
|
||||
});
|
||||
|
||||
// onMessage handler should have been called twice now
|
||||
expect(onMessageSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should call onError handler when WebSocket encounters an error", async () => {
|
||||
const onErrorSpy = vi.fn();
|
||||
const options = { onError: onErrorSpy };
|
||||
|
||||
// Create a mock that will simulate an error
|
||||
const errorLink = ws.link("ws://error-test.com/ws");
|
||||
mswServer.use(
|
||||
errorLink.addEventListener("connection", ({ client }) => {
|
||||
// Simulate an error by closing the connection immediately
|
||||
client.close(1006, "Connection failed");
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWebSocket("ws://error-test.com/ws", options),
|
||||
);
|
||||
|
||||
// Initially should not be connected and no error
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
expect(onErrorSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Wait for the connection to fail
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
});
|
||||
|
||||
// Should have error information
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).not.toBe(null);
|
||||
});
|
||||
|
||||
// onError handler should have been called
|
||||
expect(onErrorSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should provide sendMessage function to send messages to WebSocket", async () => {
|
||||
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
|
||||
|
||||
// Wait for connection to be established
|
||||
await waitFor(() => {
|
||||
expect(result.current.isConnected).toBe(true);
|
||||
});
|
||||
|
||||
// Should have a sendMessage function
|
||||
expect(result.current.sendMessage).toBeDefined();
|
||||
expect(typeof result.current.sendMessage).toBe("function");
|
||||
|
||||
// Mock the WebSocket send method
|
||||
const sendSpy = vi.spyOn(result.current.socket!, "send");
|
||||
|
||||
// Send a message
|
||||
result.current.sendMessage("Hello WebSocket!");
|
||||
|
||||
// Verify that WebSocket.send was called with the correct message
|
||||
expect(sendSpy).toHaveBeenCalledOnce();
|
||||
expect(sendSpy).toHaveBeenCalledWith("Hello WebSocket!");
|
||||
});
|
||||
|
||||
it("should not send message when WebSocket is not connected", () => {
|
||||
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
|
||||
|
||||
// Initially should not be connected
|
||||
expect(result.current.isConnected).toBe(false);
|
||||
expect(result.current.sendMessage).toBeDefined();
|
||||
|
||||
// Mock the WebSocket send method (even though socket might be null)
|
||||
const sendSpy = vi.fn();
|
||||
if (result.current.socket) {
|
||||
vi.spyOn(result.current.socket, "send").mockImplementation(sendSpy);
|
||||
}
|
||||
|
||||
// Try to send a message when not connected
|
||||
result.current.sendMessage("Hello WebSocket!");
|
||||
|
||||
// Verify that WebSocket.send was not called
|
||||
expect(sendSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
import {
|
||||
ActionEvent,
|
||||
MessageEvent,
|
||||
ObservationEvent,
|
||||
SecurityRisk,
|
||||
} from "#/types/v1/core";
|
||||
|
||||
const mockUserMessageEvent: MessageEvent = {
|
||||
id: "test-event-1",
|
||||
timestamp: Date.now().toString(),
|
||||
source: "user",
|
||||
llm_message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Hello, world!" }],
|
||||
},
|
||||
activated_microagents: [],
|
||||
extended_content: [],
|
||||
};
|
||||
|
||||
const mockActionEvent: ActionEvent = {
|
||||
id: "test-action-1",
|
||||
timestamp: Date.now().toString(),
|
||||
source: "agent",
|
||||
thought: [{ type: "text", text: "I need to execute a bash command" }],
|
||||
thinking_blocks: [],
|
||||
action: {
|
||||
kind: "ExecuteBashAction",
|
||||
command: "echo hello",
|
||||
is_input: false,
|
||||
timeout: null,
|
||||
reset: false,
|
||||
},
|
||||
tool_name: "execute_bash",
|
||||
tool_call_id: "call_123",
|
||||
tool_call: {
|
||||
id: "call_123",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "execute_bash",
|
||||
arguments: '{"command": "echo hello"}',
|
||||
},
|
||||
},
|
||||
llm_response_id: "response_123",
|
||||
security_risk: SecurityRisk.UNKNOWN,
|
||||
};
|
||||
|
||||
const mockObservationEvent: ObservationEvent = {
|
||||
id: "test-observation-1",
|
||||
timestamp: Date.now().toString(),
|
||||
source: "environment",
|
||||
tool_name: "execute_bash",
|
||||
tool_call_id: "call_123",
|
||||
observation: {
|
||||
kind: "ExecuteBashObservation",
|
||||
output: "hello\n",
|
||||
command: "echo hello",
|
||||
exit_code: 0,
|
||||
error: false,
|
||||
timeout: false,
|
||||
metadata: {
|
||||
exit_code: 0,
|
||||
pid: 12345,
|
||||
username: "user",
|
||||
hostname: "localhost",
|
||||
working_dir: "/home/user",
|
||||
py_interpreter_path: null,
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
},
|
||||
},
|
||||
action_id: "test-action-1",
|
||||
};
|
||||
|
||||
describe("useEventStore", () => {
|
||||
it("should render initial state correctly", () => {
|
||||
const { result } = renderHook(() => useEventStore());
|
||||
expect(result.current.events).toEqual([]);
|
||||
});
|
||||
|
||||
it("should add an event to the store", () => {
|
||||
const { result } = renderHook(() => useEventStore());
|
||||
|
||||
act(() => {
|
||||
result.current.addEvent(mockUserMessageEvent);
|
||||
});
|
||||
|
||||
expect(result.current.events).toEqual([mockUserMessageEvent]);
|
||||
});
|
||||
|
||||
it("should retrieve events whose actions are replaced by their observations", () => {
|
||||
const { result } = renderHook(() => useEventStore());
|
||||
|
||||
act(() => {
|
||||
result.current.addEvent(mockUserMessageEvent);
|
||||
result.current.addEvent(mockActionEvent);
|
||||
result.current.addEvent(mockObservationEvent);
|
||||
});
|
||||
|
||||
expect(result.current.uiEvents).toEqual([
|
||||
mockUserMessageEvent,
|
||||
mockObservationEvent,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should clear all events when clearEvents is called", () => {
|
||||
const { result } = renderHook(() => useEventStore());
|
||||
|
||||
// Add some events first
|
||||
act(() => {
|
||||
result.current.addEvent(mockUserMessageEvent);
|
||||
result.current.addEvent(mockActionEvent);
|
||||
});
|
||||
|
||||
// Verify events were added
|
||||
expect(result.current.events).toHaveLength(2);
|
||||
expect(result.current.uiEvents).toHaveLength(2);
|
||||
|
||||
// Clear events
|
||||
act(() => {
|
||||
result.current.clearEvents();
|
||||
});
|
||||
|
||||
// Verify events were cleared
|
||||
expect(result.current.events).toEqual([]);
|
||||
expect(result.current.uiEvents).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ActionEvent,
|
||||
ObservationEvent,
|
||||
MessageEvent,
|
||||
SecurityRisk,
|
||||
OpenHandsEvent,
|
||||
} from "#/types/v1/core";
|
||||
import { handleEventForUI } from "#/utils/handle-event-for-ui";
|
||||
|
||||
describe("handleEventForUI", () => {
|
||||
const mockObservationEvent: ObservationEvent = {
|
||||
id: "test-observation-1",
|
||||
timestamp: Date.now().toString(),
|
||||
source: "environment",
|
||||
tool_name: "execute_bash",
|
||||
tool_call_id: "call_123",
|
||||
observation: {
|
||||
kind: "ExecuteBashObservation",
|
||||
output: "hello\n",
|
||||
command: "echo hello",
|
||||
exit_code: 0,
|
||||
error: false,
|
||||
timeout: false,
|
||||
metadata: {
|
||||
exit_code: 0,
|
||||
pid: 12345,
|
||||
username: "user",
|
||||
hostname: "localhost",
|
||||
working_dir: "/home/user",
|
||||
py_interpreter_path: null,
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
},
|
||||
},
|
||||
action_id: "test-action-1",
|
||||
};
|
||||
|
||||
const mockActionEvent: ActionEvent = {
|
||||
id: "test-action-1",
|
||||
timestamp: Date.now().toString(),
|
||||
source: "agent",
|
||||
thought: [{ type: "text", text: "I need to execute a bash command" }],
|
||||
thinking_blocks: [],
|
||||
action: {
|
||||
kind: "ExecuteBashAction",
|
||||
command: "echo hello",
|
||||
is_input: false,
|
||||
timeout: null,
|
||||
reset: false,
|
||||
},
|
||||
tool_name: "execute_bash",
|
||||
tool_call_id: "call_123",
|
||||
tool_call: {
|
||||
id: "call_123",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "execute_bash",
|
||||
arguments: '{"command": "echo hello"}',
|
||||
},
|
||||
},
|
||||
llm_response_id: "response_123",
|
||||
security_risk: SecurityRisk.UNKNOWN,
|
||||
};
|
||||
|
||||
const mockMessageEvent: MessageEvent = {
|
||||
id: "test-event-1",
|
||||
timestamp: Date.now().toString(),
|
||||
source: "user",
|
||||
llm_message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Hello, world!" }],
|
||||
},
|
||||
activated_microagents: [],
|
||||
extended_content: [],
|
||||
};
|
||||
|
||||
it("should add non-observation events to the end of uiEvents", () => {
|
||||
const initialUiEvents = [mockMessageEvent];
|
||||
const result = handleEventForUI(mockActionEvent, initialUiEvents);
|
||||
|
||||
expect(result).toEqual([mockMessageEvent, mockActionEvent]);
|
||||
expect(result).not.toBe(initialUiEvents); // Should return a new array
|
||||
});
|
||||
|
||||
it("should replace corresponding action with observation when action exists", () => {
|
||||
const initialUiEvents = [mockMessageEvent, mockActionEvent];
|
||||
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
|
||||
|
||||
expect(result).toEqual([mockMessageEvent, mockObservationEvent]);
|
||||
expect(result).not.toBe(initialUiEvents); // Should return a new array
|
||||
});
|
||||
|
||||
it("should add observation to end when corresponding action is not found", () => {
|
||||
const initialUiEvents = [mockMessageEvent];
|
||||
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
|
||||
|
||||
expect(result).toEqual([mockMessageEvent, mockObservationEvent]);
|
||||
expect(result).not.toBe(initialUiEvents); // Should return a new array
|
||||
});
|
||||
|
||||
it("should handle empty uiEvents array", () => {
|
||||
const initialUiEvents: OpenHandsEvent[] = [];
|
||||
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
|
||||
|
||||
expect(result).toEqual([mockObservationEvent]);
|
||||
expect(result).not.toBe(initialUiEvents); // Should return a new array
|
||||
});
|
||||
|
||||
it("should not mutate the original uiEvents array", () => {
|
||||
const initialUiEvents = [mockMessageEvent, mockActionEvent];
|
||||
const originalLength = initialUiEvents.length;
|
||||
const originalFirstEvent = initialUiEvents[0];
|
||||
|
||||
handleEventForUI(mockObservationEvent, initialUiEvents);
|
||||
|
||||
expect(initialUiEvents).toHaveLength(originalLength);
|
||||
expect(initialUiEvents[0]).toBe(originalFirstEvent);
|
||||
expect(initialUiEvents[1]).toBe(mockActionEvent); // Should not be replaced
|
||||
});
|
||||
|
||||
it("should replace the correct action when multiple actions exist", () => {
|
||||
const anotherActionEvent: ActionEvent = {
|
||||
...mockActionEvent,
|
||||
id: "test-action-2",
|
||||
};
|
||||
|
||||
const initialUiEvents = [
|
||||
mockMessageEvent,
|
||||
mockActionEvent,
|
||||
anotherActionEvent,
|
||||
];
|
||||
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
|
||||
|
||||
expect(result).toEqual([
|
||||
mockMessageEvent,
|
||||
mockObservationEvent,
|
||||
anotherActionEvent,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -24,4 +24,5 @@ test("mapProvider", () => {
|
||||
expect(mapProvider("replicate")).toBe("Replicate");
|
||||
expect(mapProvider("voyage")).toBe("Voyage AI");
|
||||
expect(mapProvider("openrouter")).toBe("OpenRouter");
|
||||
expect(mapProvider("clarifai")).toBe("Clarifai");
|
||||
});
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.58.0",
|
||||
"version": "0.59.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.58.0",
|
||||
"version": "0.59.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.58.0",
|
||||
"version": "0.59.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -121,7 +121,7 @@ class ConversationService {
|
||||
reason?: string;
|
||||
}>(url);
|
||||
return data;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Error checking if feedback exists
|
||||
return { exists: false };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TrajectoryActions } from "../trajectory/trajectory-actions";
|
||||
import { createChatMessage } from "#/services/chat-service";
|
||||
import { InteractiveChatBox } from "./interactive-chat-box";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { isOpenHandsAction, isActionOrObservation } from "#/types/core/guards";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { FeedbackModal } from "../feedback/feedback-modal";
|
||||
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
||||
@@ -24,6 +24,7 @@ import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
import { ErrorMessageBanner } from "./error-message-banner";
|
||||
import {
|
||||
hasUserEvent,
|
||||
@@ -34,6 +35,7 @@ import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
|
||||
import { isV0Event } from "#/types/v1/type-guards";
|
||||
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
@@ -47,7 +49,8 @@ function getEntryPoint(
|
||||
export function ChatInterface() {
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const { errorMessage } = useErrorMessageStore();
|
||||
const { send, isLoadingMessages, parsedEvents } = useWsClient();
|
||||
const { send, isLoadingMessages } = useWsClient();
|
||||
const storeEvents = useEventStore((state) => state.events);
|
||||
const { setOptimisticUserMessage, getOptimisticUserMessage } =
|
||||
useOptimisticUserMessageStore();
|
||||
const { t } = useTranslation();
|
||||
@@ -74,18 +77,24 @@ export function ChatInterface() {
|
||||
|
||||
const optimisticUserMessage = getOptimisticUserMessage();
|
||||
|
||||
const events = parsedEvents.filter(shouldRenderEvent);
|
||||
const events = storeEvents
|
||||
.filter(isV0Event)
|
||||
.filter(isActionOrObservation)
|
||||
.filter(shouldRenderEvent);
|
||||
|
||||
// Check if there are any substantive agent actions (not just system messages)
|
||||
const hasSubstantiveAgentActions = React.useMemo(
|
||||
() =>
|
||||
parsedEvents.some(
|
||||
(event) =>
|
||||
isOpenHandsAction(event) &&
|
||||
event.source === "agent" &&
|
||||
event.action !== "system",
|
||||
),
|
||||
[parsedEvents],
|
||||
storeEvents
|
||||
.filter(isV0Event)
|
||||
.filter(isActionOrObservation)
|
||||
.some(
|
||||
(event) =>
|
||||
isOpenHandsAction(event) &&
|
||||
event.source === "agent" &&
|
||||
event.action !== "system",
|
||||
),
|
||||
[storeEvents],
|
||||
);
|
||||
|
||||
const handleSendMessage = async (
|
||||
|
||||
@@ -120,7 +120,7 @@ export function InteractiveChatBox({
|
||||
|
||||
// Step 5: Handle failed results
|
||||
handleFailedFiles(fileResults, imageResults);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Clear loading states and show error
|
||||
clearLoadingStates(validFiles, validImages);
|
||||
displayErrorToast("An unexpected error occurred while processing files");
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export function ConversationCard({
|
||||
}
|
||||
}
|
||||
// VS Code URL not available
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Failed to fetch VS Code URL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useUrlSearch(inputValue: string, provider: Provider) {
|
||||
);
|
||||
|
||||
setUrlSearchResults(repositories);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setUrlSearchResults([]);
|
||||
} finally {
|
||||
setIsUrlSearchLoading(false);
|
||||
|
||||
@@ -28,7 +28,7 @@ export function CancelSubscriptionModal({
|
||||
await cancelSubscriptionMutation.mutateAsync();
|
||||
displaySuccessToast(t(I18nKey.PAYMENT$SUBSCRIPTION_CANCELLED));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
displayErrorToast(t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export function CreateApiKeyModal({
|
||||
onKeyCreated(newKey);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_CREATED));
|
||||
setNewKeyName("");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
displayErrorToast(t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export function DeleteApiKeyModal({
|
||||
await deleteApiKeyMutation.mutateAsync(keyToDelete.id);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_DELETED));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
displayErrorToast(t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { useGitUser } from "#/hooks/query/use-git-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
|
||||
import { OpenHandsLogoButton } from "#/components/shared/buttons/openhands-logo-button";
|
||||
import { NewProjectButton } from "#/components/shared/buttons/new-project-button";
|
||||
import { ConversationPanelButton } from "#/components/shared/buttons/conversation-panel-button";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
@@ -74,7 +74,7 @@ export function Sidebar() {
|
||||
<nav className="flex flex-row md:flex-col items-center justify-between w-full h-auto md:w-auto md:h-full">
|
||||
<div className="flex flex-row md:flex-col items-center gap-[26px]">
|
||||
<div className="flex items-center justify-center">
|
||||
<AllHandsLogoButton />
|
||||
<OpenHandsLogoButton />
|
||||
</div>
|
||||
<div>
|
||||
<NewProjectButton disabled={settings?.EMAIL_VERIFIED === false} />
|
||||
|
||||
@@ -5,11 +5,13 @@ import { AgentState } from "#/types/agent-state";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { ActionTooltip } from "../action-tooltip";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { isOpenHandsAction, isActionOrObservation } from "#/types/core/guards";
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import { RiskAlert } from "#/components/shared/risk-alert";
|
||||
import WarningIcon from "#/icons/u-warning.svg?react";
|
||||
import { useEventMessageStore } from "#/stores/event-message-store";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
import { isV0Event } from "#/types/v1/type-guards";
|
||||
|
||||
export function ConfirmationButtons() {
|
||||
const submittedEventIds = useEventMessageStore(
|
||||
@@ -21,10 +23,13 @@ export function ConfirmationButtons() {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { send, parsedEvents } = useWsClient();
|
||||
const { send } = useWsClient();
|
||||
const events = useEventStore((state) => state.events);
|
||||
|
||||
// Find the most recent action awaiting confirmation
|
||||
const awaitingAction = parsedEvents
|
||||
const awaitingAction = events
|
||||
.filter(isV0Event)
|
||||
.filter(isActionOrObservation)
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((ev) => {
|
||||
|
||||
+3
-3
@@ -3,13 +3,13 @@ import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
export function AllHandsLogoButton() {
|
||||
export function OpenHandsLogoButton() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<TooltipButton
|
||||
tooltip={t(I18nKey.BRANDING$ALL_HANDS_AI)}
|
||||
ariaLabel={t(I18nKey.BRANDING$ALL_HANDS_LOGO)}
|
||||
tooltip={t(I18nKey.BRANDING$OPENHANDS)}
|
||||
ariaLabel={t(I18nKey.BRANDING$OPENHANDS_LOGO)}
|
||||
navLinkTo="/"
|
||||
>
|
||||
<AllHandsLogo width={46} height={30} />
|
||||
@@ -20,7 +20,7 @@ export function ModalBackdrop({ children, onClose }: ModalBackdropProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center z-20">
|
||||
<div className="fixed inset-0 flex items-center justify-center z-60">
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className="fixed inset-0 bg-black opacity-60"
|
||||
|
||||
@@ -275,7 +275,7 @@ export function ConversationSubscriptionsProvider({
|
||||
setActiveConversationIds((prev) =>
|
||||
prev.includes(conversationId) ? prev : [...prev, conversationId],
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Clean up the event handler if there was an error
|
||||
delete eventHandlersRef.current[conversationId];
|
||||
}
|
||||
|
||||
@@ -11,13 +11,11 @@ import {
|
||||
CommandAction,
|
||||
FileEditAction,
|
||||
FileWriteAction,
|
||||
OpenHandsAction,
|
||||
UserMessageAction,
|
||||
} from "#/types/core/actions";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import {
|
||||
isAgentStateChangeObservation,
|
||||
isErrorObservation,
|
||||
@@ -28,6 +26,7 @@ import {
|
||||
} from "#/types/core/guards";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
|
||||
export type WebSocketStatus = "CONNECTING" | "CONNECTED" | "DISCONNECTED";
|
||||
|
||||
@@ -72,16 +71,12 @@ const isMessageAction = (
|
||||
interface UseWsClient {
|
||||
webSocketStatus: WebSocketStatus;
|
||||
isLoadingMessages: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
|
||||
send: (event: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
webSocketStatus: "DISCONNECTED",
|
||||
isLoadingMessages: true,
|
||||
events: [],
|
||||
parsedEvents: [],
|
||||
send: () => {
|
||||
throw new Error("not connected");
|
||||
},
|
||||
@@ -133,14 +128,11 @@ export function WsClientProvider({
|
||||
}: React.PropsWithChildren<WsClientProviderProps>) {
|
||||
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
|
||||
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
const { addEvent, clearEvents } = useEventStore();
|
||||
const queryClient = useQueryClient();
|
||||
const sioRef = React.useRef<Socket | null>(null);
|
||||
const [webSocketStatus, setWebSocketStatus] =
|
||||
React.useState<WebSocketStatus>("DISCONNECTED");
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const [parsedEvents, setParsedEvents] = React.useState<
|
||||
(OpenHandsAction | OpenHandsObservation)[]
|
||||
>([]);
|
||||
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
@@ -188,7 +180,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
if (isOpenHandsAction(event) || isOpenHandsObservation(event)) {
|
||||
setParsedEvents((prevEvents) => [...prevEvents, event]);
|
||||
addEvent(event); // Event is already OpenHandsParsedEvent
|
||||
}
|
||||
|
||||
if (isErrorObservation(event)) {
|
||||
@@ -249,7 +241,6 @@ export function WsClientProvider({
|
||||
}
|
||||
}
|
||||
|
||||
setEvents((prevEvents) => [...prevEvents, event]);
|
||||
if (!Number.isNaN(parseInt(event.id as string, 10))) {
|
||||
lastEventRef.current = event;
|
||||
}
|
||||
@@ -286,9 +277,7 @@ export function WsClientProvider({
|
||||
React.useEffect(() => {
|
||||
lastEventRef.current = null;
|
||||
|
||||
// reset events when conversationId changes
|
||||
setEvents([]);
|
||||
setParsedEvents([]);
|
||||
clearEvents();
|
||||
setWebSocketStatus("CONNECTING");
|
||||
}, [conversationId]);
|
||||
|
||||
@@ -397,16 +386,9 @@ export function WsClientProvider({
|
||||
() => ({
|
||||
webSocketStatus,
|
||||
isLoadingMessages: messageRateHandler.isUnderThreshold,
|
||||
events,
|
||||
parsedEvents,
|
||||
send,
|
||||
}),
|
||||
[
|
||||
webSocketStatus,
|
||||
messageRateHandler.isUnderThreshold,
|
||||
events,
|
||||
parsedEvents,
|
||||
],
|
||||
[webSocketStatus, messageRateHandler.isUnderThreshold],
|
||||
);
|
||||
|
||||
return <WsClientContext value={value}>{children}</WsClientContext>;
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useWebSocket } from "#/hooks/use-websocket";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import {
|
||||
isV1Event,
|
||||
isAgentErrorEvent,
|
||||
isUserMessageEvent,
|
||||
isActionEvent,
|
||||
} from "#/types/v1/type-guards";
|
||||
import { handleActionEventCacheInvalidation } from "#/utils/cache-utils";
|
||||
|
||||
interface ConversationWebSocketContextType {
|
||||
connectionState: "CONNECTING" | "OPEN" | "CLOSED" | "CLOSING";
|
||||
}
|
||||
|
||||
const ConversationWebSocketContext = createContext<
|
||||
ConversationWebSocketContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export function ConversationWebSocketProvider({
|
||||
children,
|
||||
conversationId,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
conversationId?: string;
|
||||
}) {
|
||||
const [connectionState, setConnectionState] = useState<
|
||||
"CONNECTING" | "OPEN" | "CLOSED" | "CLOSING"
|
||||
>("CONNECTING");
|
||||
const queryClient = useQueryClient();
|
||||
const { addEvent } = useEventStore();
|
||||
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
|
||||
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(messageEvent: MessageEvent) => {
|
||||
try {
|
||||
const event = JSON.parse(messageEvent.data);
|
||||
// Use type guard to validate v1 event structure
|
||||
if (isV1Event(event)) {
|
||||
addEvent(event);
|
||||
|
||||
// Handle AgentErrorEvent specifically
|
||||
if (isAgentErrorEvent(event)) {
|
||||
setErrorMessage(event.error);
|
||||
}
|
||||
|
||||
// Clear optimistic user message when a user message is confirmed
|
||||
if (isUserMessageEvent(event)) {
|
||||
removeOptimisticUserMessage();
|
||||
}
|
||||
|
||||
// Handle cache invalidation for ActionEvent
|
||||
if (isActionEvent(event)) {
|
||||
const currentConversationId =
|
||||
conversationId || "test-conversation-id"; // TODO: Get from context
|
||||
handleActionEventCacheInvalidation(
|
||||
event,
|
||||
currentConversationId,
|
||||
queryClient,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to parse WebSocket message as JSON:", error);
|
||||
}
|
||||
},
|
||||
[addEvent, setErrorMessage, removeOptimisticUserMessage, queryClient],
|
||||
);
|
||||
|
||||
const websocketOptions = useMemo(
|
||||
() => ({
|
||||
onOpen: () => {
|
||||
setConnectionState("OPEN");
|
||||
removeErrorMessage(); // Clear any previous error messages on successful connection
|
||||
},
|
||||
onClose: (event: CloseEvent) => {
|
||||
setConnectionState("CLOSED");
|
||||
// Set error message for unexpected disconnects (not normal closure)
|
||||
if (event.code !== 1000) {
|
||||
setErrorMessage(
|
||||
`Connection lost: ${event.reason || "Unexpected disconnect"}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
setConnectionState("CLOSED");
|
||||
setErrorMessage("Failed to connect to server");
|
||||
},
|
||||
onMessage: handleMessage,
|
||||
}),
|
||||
[handleMessage, setErrorMessage, removeErrorMessage],
|
||||
);
|
||||
|
||||
const { socket } = useWebSocket(
|
||||
"ws://localhost/events/socket",
|
||||
websocketOptions,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
// Update state based on socket readyState
|
||||
const updateState = () => {
|
||||
switch (socket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
setConnectionState("CONNECTING");
|
||||
break;
|
||||
case WebSocket.OPEN:
|
||||
setConnectionState("OPEN");
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
setConnectionState("CLOSING");
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
setConnectionState("CLOSED");
|
||||
break;
|
||||
default:
|
||||
setConnectionState("CLOSED");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
updateState();
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const contextValue = useMemo(() => ({ connectionState }), [connectionState]);
|
||||
|
||||
return (
|
||||
<ConversationWebSocketContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ConversationWebSocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useConversationWebSocket =
|
||||
(): ConversationWebSocketContextType => {
|
||||
const context = useContext(ConversationWebSocketContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useConversationWebSocket must be used within a ConversationWebSocketProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
|
||||
|
||||
interface WebSocketProviderWrapperProps {
|
||||
children: React.ReactNode;
|
||||
conversationId: string;
|
||||
version: 0 | 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component that conditionally renders either the old v0 WebSocket provider
|
||||
* or the new v1 WebSocket provider based on the version prop.
|
||||
*
|
||||
* @param version - 0 for old WsClientProvider, 1 for new ConversationWebSocketProvider
|
||||
* @param conversationId - The conversation ID to pass to the provider
|
||||
* @param children - The child components to wrap
|
||||
*
|
||||
* @example
|
||||
* // Use the old v0 provider
|
||||
* <WebSocketProviderWrapper version={0} conversationId="conv-123">
|
||||
* <ChatComponent />
|
||||
* </WebSocketProviderWrapper>
|
||||
*
|
||||
* @example
|
||||
* // Use the new v1 provider
|
||||
* <WebSocketProviderWrapper version={1} conversationId="conv-123">
|
||||
* <ChatComponent />
|
||||
* </WebSocketProviderWrapper>
|
||||
*/
|
||||
export function WebSocketProviderWrapper({
|
||||
children,
|
||||
conversationId,
|
||||
version,
|
||||
}: WebSocketProviderWrapperProps) {
|
||||
if (version === 0) {
|
||||
return (
|
||||
<WsClientProvider conversationId={conversationId}>
|
||||
{children}
|
||||
</WsClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (version === 1) {
|
||||
return (
|
||||
<ConversationWebSocketProvider conversationId={conversationId}>
|
||||
{children}
|
||||
</ConversationWebSocketProvider>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported WebSocket provider version: ${version}. Supported versions are 0 and 1.`,
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ function PosthogInit() {
|
||||
try {
|
||||
const config = await OptionService.getConfig();
|
||||
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
displayErrorToast("Error fetching PostHog client key");
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
import useMetricsStore from "#/stores/metrics-store";
|
||||
import { isSystemMessage } from "#/types/core/guards";
|
||||
import { isSystemMessage, isActionOrObservation } from "#/types/core/guards";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { useDeleteConversation } from "./mutation/use-delete-conversation";
|
||||
@@ -14,6 +13,8 @@ import { useGetTrajectory } from "./mutation/use-get-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-trajectory";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
import { isV0Event } from "#/types/v1/type-guards";
|
||||
|
||||
interface UseConversationNameContextMenuProps {
|
||||
conversationId?: string;
|
||||
@@ -31,7 +32,7 @@ export function useConversationNameContextMenu({
|
||||
const { t } = useTranslation();
|
||||
const { conversationId: currentConversationId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { parsedEvents } = useWsClient();
|
||||
const events = useEventStore((state) => state.events);
|
||||
const { mutate: deleteConversation } = useDeleteConversation();
|
||||
const { mutate: stopConversation } = useStopConversation();
|
||||
const { mutate: getTrajectory } = useGetTrajectory();
|
||||
@@ -46,7 +47,10 @@ export function useConversationNameContextMenu({
|
||||
const [confirmStopModalVisible, setConfirmStopModalVisible] =
|
||||
React.useState(false);
|
||||
|
||||
const systemMessage = parsedEvents.find(isSystemMessage);
|
||||
const systemMessage = events
|
||||
.filter(isV0Event)
|
||||
.filter(isActionOrObservation)
|
||||
.find(isSystemMessage);
|
||||
|
||||
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -136,7 +140,7 @@ export function useConversationNameContextMenu({
|
||||
}
|
||||
}
|
||||
// VS Code URL not available
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Failed to fetch VS Code URL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useEventStore } from "#/stores/use-event-store";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
@@ -13,7 +14,8 @@ interface ServerError {
|
||||
const isServerError = (data: object): data is ServerError => "error" in data;
|
||||
|
||||
export const useHandleWSEvents = () => {
|
||||
const { events, send } = useWsClient();
|
||||
const { send } = useWsClient();
|
||||
const events = useEventStore((state) => state.events);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!events.length) {
|
||||
@@ -35,7 +37,7 @@ export const useHandleWSEvents = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
if ("type" in event && event.type === "error") {
|
||||
const message: string = `${event.message}`;
|
||||
if (message.startsWith("Agent reached maximum")) {
|
||||
// We set the agent state to paused here - if the user clicks resume, it auto updates the max iterations
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useConfig } from "./query/use-config";
|
||||
import { useGitUser } from "./query/use-git-user";
|
||||
import { getLoginMethod, LoginMethod } from "#/utils/local-storage";
|
||||
import reoService, { ReoIdentity } from "#/utils/reo";
|
||||
import { isProductionDomain } from "#/utils/utils";
|
||||
|
||||
/**
|
||||
* Maps login method to Reo identity type
|
||||
@@ -92,10 +93,14 @@ export const useReoTracking = () => {
|
||||
const { data: user } = useGitUser();
|
||||
const [hasIdentified, setHasIdentified] = React.useState(false);
|
||||
|
||||
// Initialize Reo.dev when in SaaS mode
|
||||
// Initialize Reo.dev when in SaaS mode and on the correct domain
|
||||
React.useEffect(() => {
|
||||
const initReo = async () => {
|
||||
if (config?.APP_MODE === "saas" && !reoService.isInitialized()) {
|
||||
if (
|
||||
config?.APP_MODE === "saas" &&
|
||||
isProductionDomain() &&
|
||||
!reoService.isInitialized()
|
||||
) {
|
||||
await reoService.init();
|
||||
}
|
||||
};
|
||||
@@ -103,10 +108,11 @@ export const useReoTracking = () => {
|
||||
initReo();
|
||||
}, [config?.APP_MODE]);
|
||||
|
||||
// Identify user when user data is available and we're in SaaS mode
|
||||
// Identify user when user data is available and we're in SaaS mode on correct domain
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
config?.APP_MODE !== "saas" ||
|
||||
!isProductionDomain() ||
|
||||
!user ||
|
||||
hasIdentified ||
|
||||
!reoService.isInitialized()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from "react";
|
||||
|
||||
export const useWebSocket = <T = string>(
|
||||
url: string,
|
||||
options?: {
|
||||
queryParams?: Record<string, string>;
|
||||
onOpen?: (event: Event) => void;
|
||||
onClose?: (event: CloseEvent) => void;
|
||||
onMessage?: (event: MessageEvent) => void;
|
||||
onError?: (event: Event) => void;
|
||||
},
|
||||
) => {
|
||||
const [isConnected, setIsConnected] = React.useState(false);
|
||||
const [lastMessage, setLastMessage] = React.useState<T | null>(null);
|
||||
const [messages, setMessages] = React.useState<T[]>([]);
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
const wsRef = React.useRef<WebSocket | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Build URL with query parameters if provided
|
||||
let wsUrl = url;
|
||||
if (options?.queryParams) {
|
||||
const params = new URLSearchParams(options.queryParams);
|
||||
wsUrl = `${url}?${params.toString()}`;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = (event) => {
|
||||
setIsConnected(true);
|
||||
setError(null); // Clear any previous errors
|
||||
options?.onOpen?.(event);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
setLastMessage(event.data);
|
||||
setMessages((prev) => [...prev, event.data]);
|
||||
options?.onMessage?.(event);
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
setIsConnected(false);
|
||||
// If the connection closes with an error code, treat it as an error
|
||||
if (event.code !== 1000) {
|
||||
// 1000 is normal closure
|
||||
setError(
|
||||
new Error(
|
||||
`WebSocket closed with code ${event.code}: ${event.reason || "Connection closed unexpectedly"}`,
|
||||
),
|
||||
);
|
||||
// Also call onError handler for error closures
|
||||
options?.onError?.(event);
|
||||
}
|
||||
options?.onClose?.(event);
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
setIsConnected(false);
|
||||
options?.onError?.(event);
|
||||
};
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, [url, options]);
|
||||
|
||||
const sendMessage = React.useCallback(
|
||||
(data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(data);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
lastMessage,
|
||||
messages,
|
||||
error,
|
||||
socket: wsRef.current,
|
||||
sendMessage,
|
||||
};
|
||||
};
|
||||
@@ -168,8 +168,8 @@ export enum I18nKey {
|
||||
GITHUB$CODE_NOT_IN_GITHUB = "GITHUB$CODE_NOT_IN_GITHUB",
|
||||
GITHUB$START_FROM_SCRATCH = "GITHUB$START_FROM_SCRATCH",
|
||||
AVATAR$ALT_TEXT = "AVATAR$ALT_TEXT",
|
||||
BRANDING$ALL_HANDS_AI = "BRANDING$ALL_HANDS_AI",
|
||||
BRANDING$ALL_HANDS_LOGO = "BRANDING$ALL_HANDS_LOGO",
|
||||
BRANDING$OPENHANDS = "BRANDING$OPENHANDS",
|
||||
BRANDING$OPENHANDS_LOGO = "BRANDING$OPENHANDS_LOGO",
|
||||
ERROR$GENERIC = "ERROR$GENERIC",
|
||||
GITHUB$AUTH_SCOPE = "GITHUB$AUTH_SCOPE",
|
||||
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
|
||||
|
||||
@@ -2687,37 +2687,37 @@
|
||||
"tr": "Kullanıcı avatarı",
|
||||
"uk": "аватар користувача"
|
||||
},
|
||||
"BRANDING$ALL_HANDS_AI": {
|
||||
"en": "All Hands AI",
|
||||
"ja": "All Hands AI",
|
||||
"zh-CN": "All Hands AI",
|
||||
"zh-TW": "All Hands AI",
|
||||
"ko-KR": "All Hands AI",
|
||||
"de": "All Hands AI",
|
||||
"no": "All Hands AI",
|
||||
"it": "All Hands AI",
|
||||
"pt": "All Hands AI",
|
||||
"es": "All Hands AI",
|
||||
"ar": "All Hands AI",
|
||||
"fr": "All Hands AI",
|
||||
"tr": "All Hands AI",
|
||||
"uk": "All Hands AI"
|
||||
"BRANDING$OPENHANDS": {
|
||||
"en": "OpenHands",
|
||||
"ja": "OpenHands",
|
||||
"zh-CN": "OpenHands",
|
||||
"zh-TW": "OpenHands",
|
||||
"ko-KR": "OpenHands",
|
||||
"de": "OpenHands",
|
||||
"no": "OpenHands",
|
||||
"it": "OpenHands",
|
||||
"pt": "OpenHands",
|
||||
"es": "OpenHands",
|
||||
"ar": "OpenHands",
|
||||
"fr": "OpenHands",
|
||||
"tr": "OpenHands",
|
||||
"uk": "OpenHands"
|
||||
},
|
||||
"BRANDING$ALL_HANDS_LOGO": {
|
||||
"en": "All Hands Logo",
|
||||
"ja": "All Handsロゴ",
|
||||
"zh-CN": "All Hands标志",
|
||||
"zh-TW": "All Hands標誌",
|
||||
"ko-KR": "All Hands 로고",
|
||||
"de": "All Hands Logo",
|
||||
"no": "All Hands Logo",
|
||||
"it": "Logo All Hands",
|
||||
"pt": "Logo All Hands",
|
||||
"es": "Logo de All Hands",
|
||||
"ar": "شعار All Hands",
|
||||
"fr": "Logo All Hands",
|
||||
"tr": "All Hands Logosu",
|
||||
"uk": "All Hands лого"
|
||||
"BRANDING$OPENHANDS_LOGO": {
|
||||
"en": "OpenHands Logo",
|
||||
"ja": "OpenHandsロゴ",
|
||||
"zh-CN": "OpenHands标志",
|
||||
"zh-TW": "OpenHands標誌",
|
||||
"ko-KR": "OpenHands 로고",
|
||||
"de": "OpenHands Logo",
|
||||
"no": "OpenHands Logo",
|
||||
"it": "Logo OpenHands",
|
||||
"pt": "Logo OpenHands",
|
||||
"es": "Logo de OpenHands",
|
||||
"ar": "شعار OpenHands",
|
||||
"fr": "Logo OpenHands",
|
||||
"tr": "OpenHands Logosu",
|
||||
"uk": "OpenHands лого"
|
||||
},
|
||||
"ERROR$GENERIC": {
|
||||
"en": "An error occurred",
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
UserMessageAction,
|
||||
} from "#/types/core/actions";
|
||||
import { AgentStateChangeObservation } from "#/types/core/observations";
|
||||
import { MessageEvent } from "#/types/v1/core";
|
||||
import { AgentErrorEvent } from "#/types/v1/core/events/observation-event";
|
||||
import { MockSessionMessaage } from "./session-history.mock";
|
||||
|
||||
export const generateAgentStateChangeObservation = (
|
||||
@@ -73,3 +75,56 @@ export const emitMessages = (
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// V1 Event Mock Factories for WebSocket Testing
|
||||
|
||||
/**
|
||||
* Creates a mock MessageEvent for testing WebSocket event handling
|
||||
*/
|
||||
export const createMockMessageEvent = (
|
||||
overrides: Partial<MessageEvent> = {},
|
||||
): MessageEvent => ({
|
||||
id: "test-event-123",
|
||||
timestamp: new Date().toISOString(),
|
||||
source: "agent",
|
||||
llm_message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Hello from agent" }],
|
||||
},
|
||||
activated_microagents: [],
|
||||
extended_content: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a mock user MessageEvent for testing WebSocket event handling
|
||||
*/
|
||||
export const createMockUserMessageEvent = (
|
||||
overrides: Partial<MessageEvent> = {},
|
||||
): MessageEvent => ({
|
||||
id: "user-message-123",
|
||||
timestamp: new Date().toISOString(),
|
||||
source: "user",
|
||||
llm_message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Hello from user" }],
|
||||
},
|
||||
activated_microagents: [],
|
||||
extended_content: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a mock AgentErrorEvent for testing error handling
|
||||
*/
|
||||
export const createMockAgentErrorEvent = (
|
||||
overrides: Partial<AgentErrorEvent> = {},
|
||||
): AgentErrorEvent => ({
|
||||
id: "error-event-123",
|
||||
timestamp: new Date().toISOString(),
|
||||
source: "agent",
|
||||
tool_name: "str_replace_editor",
|
||||
tool_call_id: "tool-call-456",
|
||||
error: "Failed to execute command: Permission denied",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useAgentStore } from "#/stores/agent-store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
import { useBatchFeedback } from "#/hooks/query/use-batch-feedback";
|
||||
import { WsClientProvider } from "#/context/ws-client-provider";
|
||||
import { EventHandler } from "../wrapper/event-handler";
|
||||
import { useConversationConfig } from "#/hooks/query/use-conversation-config";
|
||||
|
||||
@@ -28,6 +27,7 @@ import { ConversationName } from "#/components/features/conversation/conversatio
|
||||
|
||||
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs/conversation-tabs";
|
||||
import { useStartConversation } from "#/hooks/mutation/use-start-conversation";
|
||||
import { WebSocketProviderWrapper } from "#/contexts/websocket-provider-wrapper";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
@@ -106,7 +106,7 @@ function AppContent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<WsClientProvider conversationId={conversationId}>
|
||||
<WebSocketProviderWrapper version={0} conversationId={conversationId}>
|
||||
<ConversationSubscriptionsProvider>
|
||||
<EventHandler>
|
||||
<div
|
||||
@@ -122,7 +122,7 @@ function AppContent() {
|
||||
</div>
|
||||
</EventHandler>
|
||||
</ConversationSubscriptionsProvider>
|
||||
</WsClientProvider>
|
||||
</WebSocketProviderWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { create } from "zustand";
|
||||
import { OpenHandsEvent } from "#/types/v1/core";
|
||||
import { handleEventForUI } from "#/utils/handle-event-for-ui";
|
||||
import { OpenHandsParsedEvent } from "#/types/core";
|
||||
import { isV1Event } from "#/types/v1/type-guards";
|
||||
|
||||
// While we transition to v1 events, our store can handle both v0 and v1 events
|
||||
type OHEvent = OpenHandsEvent | OpenHandsParsedEvent;
|
||||
|
||||
interface EventState {
|
||||
events: OHEvent[];
|
||||
uiEvents: OHEvent[];
|
||||
addEvent: (event: OHEvent) => void;
|
||||
clearEvents: () => void;
|
||||
}
|
||||
|
||||
export const useEventStore = create<EventState>()((set) => ({
|
||||
events: [],
|
||||
uiEvents: [],
|
||||
addEvent: (event: OHEvent) =>
|
||||
set((state) => {
|
||||
const newEvents = [...state.events, event];
|
||||
const newUiEvents = isV1Event(event)
|
||||
? // @ts-expect-error - temporary, needs proper typing
|
||||
handleEventForUI(event, state.uiEvents)
|
||||
: [...state.uiEvents, event];
|
||||
|
||||
return {
|
||||
events: newEvents,
|
||||
uiEvents: newUiEvents,
|
||||
};
|
||||
}),
|
||||
clearEvents: () =>
|
||||
set(() => ({
|
||||
events: [],
|
||||
uiEvents: [],
|
||||
})),
|
||||
}));
|
||||
@@ -105,3 +105,8 @@ export const isStatusUpdate = (event: unknown): event is StatusUpdate =>
|
||||
"status_update" in event &&
|
||||
"type" in event &&
|
||||
"id" in event;
|
||||
|
||||
export const isActionOrObservation = (
|
||||
event: OpenHandsParsedEvent,
|
||||
): event is OpenHandsAction | OpenHandsObservation =>
|
||||
isOpenHandsAction(event) || isOpenHandsObservation(event);
|
||||
|
||||
@@ -2,6 +2,9 @@ import { OpenHandsAction } from "./actions";
|
||||
import { OpenHandsObservation } from "./observations";
|
||||
import { OpenHandsVariance } from "./variances";
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed once we fully transition to v1 events
|
||||
*/
|
||||
export type OpenHandsParsedEvent =
|
||||
| OpenHandsAction
|
||||
| OpenHandsObservation
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { OpenHandsEvent, ObservationEvent, BaseEvent } from "./core";
|
||||
import { AgentErrorEvent } from "./core/events/observation-event";
|
||||
import { MessageEvent } from "./core/events/message-event";
|
||||
import { ActionEvent } from "./core/events/action-event";
|
||||
import type { OpenHandsParsedEvent } from "../core/index";
|
||||
|
||||
/**
|
||||
* Type guard to check if an unknown value is a valid BaseEvent
|
||||
* @param value - The value to check
|
||||
* @returns true if the value is a valid BaseEvent
|
||||
*/
|
||||
export function isBaseEvent(value: unknown): value is BaseEvent {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
"id" in value &&
|
||||
"timestamp" in value &&
|
||||
"source" in value &&
|
||||
typeof value.id === "string" &&
|
||||
value.id.length > 0 &&
|
||||
typeof value.timestamp === "string" &&
|
||||
value.timestamp.length > 0 &&
|
||||
typeof value.source === "string" &&
|
||||
(value.source === "agent" ||
|
||||
value.source === "user" ||
|
||||
value.source === "environment")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard function to check if an event is an observation event
|
||||
*/
|
||||
export const isObservationEvent = (
|
||||
event: OpenHandsEvent,
|
||||
): event is ObservationEvent =>
|
||||
event.source === "environment" &&
|
||||
"action_id" in event &&
|
||||
"observation" in event;
|
||||
|
||||
/**
|
||||
* Type guard function to check if an event is an agent error event
|
||||
*/
|
||||
export const isAgentErrorEvent = (
|
||||
event: OpenHandsEvent,
|
||||
): event is AgentErrorEvent =>
|
||||
event.source === "agent" &&
|
||||
"tool_name" in event &&
|
||||
"tool_call_id" in event &&
|
||||
"error" in event &&
|
||||
typeof event.tool_name === "string" &&
|
||||
typeof event.tool_call_id === "string" &&
|
||||
typeof event.error === "string";
|
||||
|
||||
/**
|
||||
* Type guard function to check if an event is a user message event
|
||||
*/
|
||||
export const isUserMessageEvent = (
|
||||
event: OpenHandsEvent,
|
||||
): event is MessageEvent =>
|
||||
"llm_message" in event &&
|
||||
typeof event.llm_message === "object" &&
|
||||
event.llm_message !== null &&
|
||||
"role" in event.llm_message &&
|
||||
event.llm_message.role === "user";
|
||||
|
||||
/**
|
||||
* Type guard function to check if an event is an action event
|
||||
*/
|
||||
export const isActionEvent = (event: OpenHandsEvent): event is ActionEvent =>
|
||||
event.source === "agent" &&
|
||||
"action" in event &&
|
||||
"tool_name" in event &&
|
||||
"tool_call_id" in event &&
|
||||
typeof event.tool_name === "string" &&
|
||||
typeof event.tool_call_id === "string";
|
||||
|
||||
// =============================================================================
|
||||
// TEMPORARY COMPATIBILITY TYPE GUARDS
|
||||
// These will be removed once we fully migrate to V1 events
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* TEMPORARY: Type guard to check if an event is a V1 OpenHandsEvent
|
||||
* Uses isBaseEvent to validate the complete event structure
|
||||
*
|
||||
* @deprecated This is temporary until full V1 migration is complete
|
||||
*/
|
||||
export function isV1Event(
|
||||
event: OpenHandsEvent | OpenHandsParsedEvent,
|
||||
): event is OpenHandsEvent {
|
||||
// Use isBaseEvent to validate the complete BaseEvent structure
|
||||
// This ensures the event has all required properties with correct types
|
||||
return isBaseEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* TEMPORARY: Type guard to check if an event is a V0 OpenHandsParsedEvent
|
||||
*
|
||||
* @deprecated This is temporary until full V1 migration is complete
|
||||
*/
|
||||
export function isV0Event(
|
||||
event: OpenHandsEvent | OpenHandsParsedEvent,
|
||||
): event is OpenHandsParsedEvent {
|
||||
// Handle null/undefined cases
|
||||
if (!event || typeof event !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// V0 events have numeric IDs and either 'action' or 'observation' properties
|
||||
return (
|
||||
"id" in event &&
|
||||
typeof event.id === "number" &&
|
||||
("action" in event || "observation" in event)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import type { ActionEvent } from "#/types/v1/core/events/action-event";
|
||||
import { stripWorkspacePrefix } from "./path-utils";
|
||||
|
||||
/**
|
||||
* Cache invalidation utilities for TanStack Query
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle cache invalidation for ActionEvent
|
||||
* Invalidates relevant query caches based on the action type
|
||||
*
|
||||
* @param event - The ActionEvent to process
|
||||
* @param conversationId - The conversation ID for cache keys
|
||||
* @param queryClient - The TanStack Query client instance
|
||||
*/
|
||||
export const handleActionEventCacheInvalidation = (
|
||||
event: ActionEvent,
|
||||
conversationId: string,
|
||||
queryClient: QueryClient,
|
||||
) => {
|
||||
const { action } = event;
|
||||
|
||||
// Invalidate file_changes cache for file-related actions
|
||||
if (
|
||||
action.kind === "StrReplaceEditorAction" ||
|
||||
action.kind === "ExecuteBashAction"
|
||||
) {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: ["file_changes", conversationId],
|
||||
},
|
||||
{ cancelRefetch: false },
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate specific file diff cache for file modifications
|
||||
if (action.kind === "StrReplaceEditorAction" && action.path) {
|
||||
const strippedPath = stripWorkspacePrefix(action.path);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["file_diff", conversationId, strippedPath],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { OpenHandsEvent } from "#/types/v1/core";
|
||||
import { isObservationEvent } from "#/types/v1/type-guards";
|
||||
|
||||
/**
|
||||
* Handles adding an event to the UI events array, with special logic for observation events
|
||||
*/
|
||||
export const handleEventForUI = (
|
||||
event: OpenHandsEvent,
|
||||
uiEvents: OpenHandsEvent[],
|
||||
): OpenHandsEvent[] => {
|
||||
const newUiEvents = [...uiEvents];
|
||||
|
||||
if (isObservationEvent(event)) {
|
||||
// Find and replace the corresponding action from uiEvents
|
||||
const actionIndex = newUiEvents.findIndex(
|
||||
(uiEvent) => uiEvent.id === event.action_id,
|
||||
);
|
||||
if (actionIndex !== -1) {
|
||||
// Replace the action with the observation
|
||||
newUiEvents[actionIndex] = event;
|
||||
} else {
|
||||
// Action not found in uiEvents, just add the observation
|
||||
newUiEvents.push(event);
|
||||
}
|
||||
} else {
|
||||
// For non-observation events, just add them to uiEvents
|
||||
newUiEvents.push(event);
|
||||
}
|
||||
|
||||
return newUiEvents;
|
||||
};
|
||||
@@ -25,6 +25,7 @@ export const MAP_PROVIDER = {
|
||||
openrouter: "OpenRouter",
|
||||
openhands: "OpenHands",
|
||||
lemonade: "Lemonade",
|
||||
clarifai: "Clarifai",
|
||||
};
|
||||
|
||||
export const mapProvider = (provider: string) =>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Path manipulation utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Strip workspace prefix from file paths
|
||||
* Removes /workspace/ and the next directory level from paths
|
||||
*
|
||||
* @param path - The file path to process
|
||||
* @returns The path with workspace prefix removed
|
||||
*
|
||||
* @example
|
||||
* stripWorkspacePrefix("/workspace/repo/src/file.py") // returns "src/file.py"
|
||||
* stripWorkspacePrefix("/workspace/my-project/components/Button.tsx") // returns "components/Button.tsx"
|
||||
*/
|
||||
export const stripWorkspacePrefix = (path: string): string => {
|
||||
// Strip /workspace/ and the next directory level
|
||||
const workspaceMatch = path.match(/^\/workspace\/[^/]+\/(.*)$/);
|
||||
return workspaceMatch ? workspaceMatch[1] : path;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { SuggestedTaskGroup } from "#/utils/types";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { PRODUCT_URL } from "#/utils/constants";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -49,6 +50,13 @@ export const isMobileDevice = (): boolean =>
|
||||
"ontouchstart" in window ||
|
||||
navigator.maxTouchPoints > 0;
|
||||
|
||||
/**
|
||||
* Checks if the current domain is the production domain
|
||||
* @returns True if the current domain matches the production URL
|
||||
*/
|
||||
export const isProductionDomain = (): boolean =>
|
||||
window.location.origin === PRODUCT_URL.PRODUCTION;
|
||||
|
||||
interface EventActionHistory {
|
||||
args?: {
|
||||
LLM_API_KEY?: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ export const VERIFIED_PROVIDERS = [
|
||||
"openai",
|
||||
"mistral",
|
||||
"lemonade",
|
||||
"clarifai",
|
||||
];
|
||||
export const VERIFIED_MODELS = [
|
||||
"o3-mini-2025-01-31",
|
||||
|
||||
@@ -24,7 +24,7 @@ export function transformVSCodeUrl(vsCodeUrl: string | null): string | null {
|
||||
}
|
||||
|
||||
return vsCodeUrl;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Silently handle the error and return the original URL
|
||||
return vsCodeUrl;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ vi.mock("#/hooks/use-is-on-tos-page", () => ({
|
||||
useIsOnTosPage: () => false,
|
||||
}));
|
||||
|
||||
// Import the Zustand mock to enable automatic store resets
|
||||
vi.mock("zustand");
|
||||
|
||||
// Mock requests during tests
|
||||
beforeAll(() => {
|
||||
server.listen({ onUnhandledRequest: "bypass" });
|
||||
|
||||
@@ -27,6 +27,7 @@ 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.
|
||||
* Do NOT mark a pull request as ready to review unless the user explicitly says so
|
||||
* IMPORTANT: When making commits, NEVER use the `--no-verify` flag. Pre-commit hooks (if configured in `.openhands/pre-commit.sh`) must be executed to ensure code quality and enforce project standards.
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
|
||||
@@ -28,6 +28,7 @@ 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.
|
||||
* Do NOT mark a pull request as ready to review unless the user explicitly says so
|
||||
* IMPORTANT: When making commits, NEVER use the `--no-verify` flag. Pre-commit hooks (if configured in `.openhands/pre-commit.sh`) must be executed to ensure code quality and enforce project standards.
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
|
||||
@@ -27,6 +27,7 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a merge request, send the user a short message with a link to the merge request.
|
||||
* IMPORTANT: When making commits, NEVER use the `--no-verify` flag. Pre-commit hooks (if configured in `.openhands/pre-commit.sh`) must be executed to ensure code quality and enforce project standards.
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
|
||||
@@ -164,7 +164,7 @@ def test_executable() -> bool:
|
||||
)
|
||||
|
||||
# --- Wait for welcome ---
|
||||
deadline = boot_start + 30
|
||||
deadline = boot_start + 60
|
||||
saw_welcome = False
|
||||
captured = []
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""OpenHands CLI package."""
|
||||
"""OpenHands package."""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
from importlib.metadata import version, PackageNotFoundError
|
||||
|
||||
try:
|
||||
__version__ = version("openhands")
|
||||
except PackageNotFoundError:
|
||||
__version__ = "0.0.0"
|
||||
|
||||
@@ -54,6 +54,7 @@ def _print_exit_hint(conversation_id: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
|
||||
def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
||||
"""Run the agent chat session using the agent SDK.
|
||||
|
||||
|
||||
@@ -113,21 +113,12 @@ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
|
||||
pull_cmd = ['docker', 'pull', runtime_image]
|
||||
print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
|
||||
try:
|
||||
subprocess.run(
|
||||
pull_cmd,
|
||||
check=True,
|
||||
timeout=300, # 5 minutes timeout
|
||||
)
|
||||
subprocess.run(pull_cmd, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print_formatted_text(
|
||||
HTML('<ansired>❌ Failed to pull runtime image.</ansired>')
|
||||
)
|
||||
sys.exit(1)
|
||||
except subprocess.TimeoutExpired:
|
||||
print_formatted_text(
|
||||
HTML('<ansired>❌ Timeout while pulling runtime image.</ansired>')
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print_formatted_text('')
|
||||
print_formatted_text(
|
||||
|
||||
@@ -57,8 +57,6 @@ def display_banner(conversation_id: str, resume: bool = False) -> None:
|
||||
style=DEFAULT_STYLE,
|
||||
)
|
||||
|
||||
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
|
||||
|
||||
print_formatted_text('')
|
||||
if not resume:
|
||||
print_formatted_text(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
@@ -37,7 +38,7 @@ def ask_user_confirmation(
|
||||
or '[unknown action]'
|
||||
)
|
||||
print_formatted_text(
|
||||
HTML(f'<grey> {i}. {tool_name}: {action_content}...</grey>')
|
||||
HTML(f'<grey> {i}. {tool_name}: {html.escape(action_content)}...</grey>')
|
||||
)
|
||||
|
||||
question = 'Choose an option:'
|
||||
|
||||
@@ -123,9 +123,15 @@ def prompt_api_key(
|
||||
validator = NonEmptyValueValidator()
|
||||
|
||||
question = helper_text + step_counter.next_step(question)
|
||||
return cli_text_input(
|
||||
user_input = cli_text_input(
|
||||
question, escapable=escapable, validator=validator, is_password=True
|
||||
)
|
||||
|
||||
# If user pressed ENTER with existing key (empty input), return the existing key
|
||||
if existing_api_key and not user_input.strip():
|
||||
return existing_api_key.get_secret_value()
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
# Advanced settings functions
|
||||
|
||||
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
|
||||
|
||||
[project]
|
||||
name = "openhands"
|
||||
version = "0.1.0"
|
||||
version = "1.0.1"
|
||||
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
@@ -15,15 +15,16 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
|
||||
# TODO: pin package versions once agent-sdk has published PyPI packages
|
||||
dependencies = [
|
||||
"openhands-sdk",
|
||||
"openhands-tools",
|
||||
"openhands-sdk @ git+https://github.com/All-Hands-AI/agent-sdk.git@50b094a92817e448ec4352d2950df4f19edd5a9f#subdirectory=openhands/sdk",
|
||||
"openhands-tools @ git+https://github.com/All-Hands-AI/agent-sdk.git@50b094a92817e448ec4352d2950df4f19edd5a9f#subdirectory=openhands/tools",
|
||||
"prompt-toolkit>=3",
|
||||
"typer>=0.17.4",
|
||||
]
|
||||
|
||||
# Dev-only tools with uv groups: `uv sync --group dev`
|
||||
scripts.openhands = "openhands_cli.simple_main:main"
|
||||
scripts = { openhands = "openhands_cli.simple_main:main" }
|
||||
|
||||
[dependency-groups]
|
||||
# Hatchling wheel target: include the package directory
|
||||
@@ -42,6 +43,9 @@ dev = [
|
||||
"ruff>=0.11.8",
|
||||
]
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = [ "openhands_cli" ]
|
||||
|
||||
@@ -96,5 +100,5 @@ disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.uv.sources]
|
||||
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "189979a5013751aa86852ab41afe9a79555e62ac" }
|
||||
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "189979a5013751aa86852ab41afe9a79555e62ac" }
|
||||
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "50b094a92817e448ec4352d2950df4f19edd5a9f" }
|
||||
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "50b094a92817e448ec4352d2950df4f19edd5a9f" }
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Test for API key preservation bug when updating settings."""
|
||||
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands_cli.user_actions.settings_action import prompt_api_key
|
||||
from openhands_cli.tui.utils import StepCounter
|
||||
|
||||
|
||||
def test_api_key_preservation_when_user_presses_enter():
|
||||
"""Test that API key is preserved when user presses ENTER to keep current key.
|
||||
|
||||
This test replicates the bug where API keys disappear when updating settings.
|
||||
When a user presses ENTER to keep the current API key, the function should
|
||||
return the existing API key, not an empty string.
|
||||
"""
|
||||
step_counter = StepCounter(1)
|
||||
existing_api_key = SecretStr("sk-existing-key-123")
|
||||
|
||||
# Mock cli_text_input to return empty string (simulating user pressing ENTER)
|
||||
with patch('openhands_cli.user_actions.settings_action.cli_text_input', return_value=''):
|
||||
result = prompt_api_key(
|
||||
step_counter=step_counter,
|
||||
provider='openai',
|
||||
existing_api_key=existing_api_key,
|
||||
escapable=True
|
||||
)
|
||||
|
||||
# The bug: result is empty string instead of the existing key
|
||||
# This test will fail initially, demonstrating the bug
|
||||
assert result == existing_api_key.get_secret_value(), (
|
||||
f"Expected existing API key '{existing_api_key.get_secret_value()}' "
|
||||
f"but got '{result}'. API key should be preserved when user presses ENTER."
|
||||
)
|
||||
|
||||
|
||||
def test_api_key_update_when_user_enters_new_key():
|
||||
"""Test that API key is updated when user enters a new key."""
|
||||
step_counter = StepCounter(1)
|
||||
existing_api_key = SecretStr("sk-existing-key-123")
|
||||
new_api_key = "sk-new-key-456"
|
||||
|
||||
# Mock cli_text_input to return new API key
|
||||
with patch('openhands_cli.user_actions.settings_action.cli_text_input', return_value=new_api_key):
|
||||
result = prompt_api_key(
|
||||
step_counter=step_counter,
|
||||
provider='openai',
|
||||
existing_api_key=existing_api_key,
|
||||
escapable=True
|
||||
)
|
||||
|
||||
# Should return the new API key
|
||||
assert result == new_api_key
|
||||
|
||||
|
||||
@@ -111,8 +111,6 @@ class TestLaunchGuiServer:
|
||||
[
|
||||
# Docker pull failure
|
||||
(subprocess.CalledProcessError(1, 'docker pull'), None, 1, False, False),
|
||||
# Docker pull timeout
|
||||
(subprocess.TimeoutExpired('docker pull', 300), None, 1, False, False),
|
||||
# Docker run failure
|
||||
(MagicMock(returncode=0), subprocess.CalledProcessError(1, 'docker run'), 1, False, False),
|
||||
# KeyboardInterrupt during run
|
||||
|
||||
Generated
+32
-21
@@ -660,18 +660,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastuuid"
|
||||
version = "0.12.0"
|
||||
version = "0.13.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/17/13146a1e916bd2971d0a58db5e0a4ad23efdd49f78f33ac871c161f8007b/fastuuid-0.12.0.tar.gz", hash = "sha256:d0bd4e5b35aad2826403f4411937c89e7c88857b1513fe10f696544c03e9bd8e", size = 19180, upload-time = "2025-01-27T18:04:14.387Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/80/3c16a1edad2e6cd82fbd15ac998cc1b881f478bf1f80ca717d941c441874/fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1", size = 18255, upload-time = "2025-09-26T09:05:38.281Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/28/442e79d6219b90208cb243ac01db05d89cc4fdf8ecd563fb89476baf7122/fastuuid-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:328694a573fe9dce556b0b70c9d03776786801e028d82f0b6d9db1cb0521b4d1", size = 247372, upload-time = "2025-01-27T18:03:40.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/eb/e0fd56890970ca7a9ec0d116844580988b692b1a749ac38e0c39e1dbdf23/fastuuid-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02acaea2c955bb2035a7d8e7b3fba8bd623b03746ae278e5fa932ef54c702f9f", size = 258200, upload-time = "2025-01-27T18:04:12.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/3c/4b30e376e65597a51a3dc929461a0dec77c8aec5d41d930f482b8f43e781/fastuuid-0.12.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ed9f449cba8cf16cced252521aee06e633d50ec48c807683f21cc1d89e193eb0", size = 278446, upload-time = "2025-01-27T18:04:15.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/96/cc5975fd23d2197b3e29f650a7a9beddce8993eaf934fa4ac595b77bb71f/fastuuid-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:0df2ea4c9db96fd8f4fa38d0e88e309b3e56f8fd03675a2f6958a5b082a0c1e4", size = 157185, upload-time = "2025-01-27T18:06:19.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/e8/d2bb4f19e5ee15f6f8e3192a54a897678314151aa17d0fb766d2c2cbc03d/fastuuid-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fe2407316a04ee8f06d3dbc7eae396d0a86591d92bafe2ca32fce23b1145786", size = 247512, upload-time = "2025-01-27T18:04:08.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/53/25e811d92fd60f5c65e098c3b68bd8f1a35e4abb6b77a153025115b680de/fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c", size = 258257, upload-time = "2025-01-27T18:03:56.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/23/73618e7793ea0b619caae2accd9e93e60da38dd78dd425002d319152ef2f/fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37", size = 278559, upload-time = "2025-01-27T18:03:58.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/41/6317ecfc4757d5f2a604e5d3993f353ba7aee85fa75ad8b86fce6fc2fa40/fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9", size = 157276, upload-time = "2025-01-27T18:06:39.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/36/434f137c5970cac19e57834e1f7680e85301619d49891618c00666700c61/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a", size = 494638, upload-time = "2025-09-26T09:14:58.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/3c/083de2ac007b2b305523b9c006dba5051e5afd87a626ef1a39f76e2c6b82/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7", size = 253138, upload-time = "2025-09-26T09:13:33.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/5e/630cffa1c8775db526e39e9e4c5c7db0c27be0786bb21ba82c912ae19f63/fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202", size = 244521, upload-time = "2025-09-26T09:14:40.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/51/55d78705f4fbdadf88fb40f382f508d6c7a4941ceddd7825fafebb4cc778/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e", size = 271557, upload-time = "2025-09-26T09:15:09.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/2b/1b89e90a8635e5587ccdbbeb169c590672ce7637880f2c047482a0359950/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854", size = 272334, upload-time = "2025-09-26T09:07:48.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/06/4c8207894eeb30414999e5c3f66ac039bc4003437eb4060d8a1bceb4cc6f/fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86", size = 290594, upload-time = "2025-09-26T09:12:54.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/69/96d221931a31d77a47cc2487bdfacfb3091edfc2e7a04b1795df1aec05df/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952", size = 452835, upload-time = "2025-09-26T09:14:00.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/ef/bf045f0a47dcec96247497ef3f7a31d86ebc074330e2dccc34b8dbc0468a/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9", size = 468225, upload-time = "2025-09-26T09:13:38.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/46/4817ab5a3778927155a4bde92540d4c4fa996161ec8b8e080c8928b0984e/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3", size = 444907, upload-time = "2025-09-26T09:14:30.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/27/ab284117ce4dc9b356a7196bdbf220510285f201d27f1f078592cdc8187b/fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85", size = 145415, upload-time = "2025-09-26T09:08:59.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/0c/f970a4222773b248931819f8940800b760283216ca3dda173ed027e94bdd/fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a", size = 150840, upload-time = "2025-09-26T09:13:46.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/62/74fc53f6e04a4dc5b36c34e4e679f85a4c14eec800dcdb0f2c14b5442217/fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ea17dfd35e0e91920a35d91e65e5f9c9d1985db55ac4ff2f1667a0f61189cefa", size = 494678, upload-time = "2025-09-26T09:14:30.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/ba/f28b9b7045738a8bfccfb9cd6aff4b91fce2669e6b383a48b0694ee9b3ff/fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be6ad91e5fefbcc2a4b478858a2715e386d405834ea3ae337c3b6b95cc0e47d6", size = 253162, upload-time = "2025-09-26T09:13:35.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/18/13fac89cb4c9f0cd7e81a9154a77ecebcc95d2b03477aa91d4d50f7227ee/fastuuid-0.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea6df13a306aab3e0439d58c312ff1e6f4f07f09f667579679239b4a6121f64a", size = 244546, upload-time = "2025-09-26T09:14:58.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/9691167804d59411cc4269841df949f6dd5e76452ab10dcfcd1dbe04c5bc/fastuuid-0.13.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2354c1996d3cf12dc2ba3752e2c4d6edc46e1a38c63893146777b1939f3062d4", size = 271528, upload-time = "2025-09-26T09:14:48.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/b5/7a75a03d1c7aa0b6d573032fcca39391f0aef7f2caabeeb45a672bc0bd3c/fastuuid-0.13.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cf9b7469fc26d1f9b1c43ac4b192e219e85b88fdf81d71aa755a6c08c8a817", size = 272292, upload-time = "2025-09-26T09:14:42.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/db/fa0f16cbf76e6880599533af4ef01bb586949c5320612e9d884eff13e603/fastuuid-0.13.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92ba539170097b9047551375f1ca09d8d2b4aefcc79eeae3e1c43fe49b42072e", size = 290466, upload-time = "2025-09-26T09:08:33.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/02/6b8c45bfbc8500994dd94edba7f59555f9683c4d8c9a164ae1d25d03c7c7/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:dbb81d05617bc2970765c1ad82db7e8716f6a2b7a361a14b83de5b9240ade448", size = 452838, upload-time = "2025-09-26T09:13:44.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/12/85d95a84f265b888e8eb9f9e2b5aaf331e8be60c0a7060146364b3544b6a/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:d973bd6bf9d754d3cca874714ac0a6b22a47f239fb3d3c8687569db05aac3471", size = 468149, upload-time = "2025-09-26T09:13:18.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/da/dd9a137e9ea707e883c92470113a432233482ec9ad3e9b99c4defc4904e6/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e725ceef79486423f05ee657634d4b4c1ca5fb2c8a94e0708f5d6356a83f2a83", size = 444933, upload-time = "2025-09-26T09:14:09.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/f4/ab363d7f4ac3989691e2dc5ae2d8391cfb0b4169e52ef7fa0ac363e936f0/fastuuid-0.13.5-cp313-cp313-win32.whl", hash = "sha256:a1c430a332ead0b2674f1ef71b17f43b8139ec5a4201182766a21f131a31e021", size = 145462, upload-time = "2025-09-26T09:14:15.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/8a/52eb77d9c294a54caa0d2d8cc9f906207aa6d916a22de963687ab6db8b86/fastuuid-0.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:241fdd362fd96e6b337db62a65dd7cb3dfac20adf854573247a47510e192db6f", size = 150923, upload-time = "2025-09-26T09:13:03.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1266,8 +1280,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.76.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
version = "1.77.7"
|
||||
source = { git = "https://github.com/BerriAI/litellm.git?rev=v1.77.7.dev9#763d2f8ccdd8412dbe6d4ac0e136d9ac34dcd4c0" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "click" },
|
||||
@@ -1282,10 +1296,6 @@ dependencies = [
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tokenizers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/a3/f7c00c660972eed1ba5ed53771ac9b4235e7fb1dc410e91d35aff2778ae7/litellm-1.76.2.tar.gz", hash = "sha256:fc7af111fa0f06943d8dbebed73f88000f9902f0d0ee0882c57d0bd5c1a37ecb", size = 10189238, upload-time = "2025-09-04T00:25:09.472Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/f4/980cc81c21424026dcb48a541654fd6f4286891825a3d0dd51f02b65cbc3/litellm-1.76.2-py3-none-any.whl", hash = "sha256:a9a2ef64a598b5b4ae245f1de6afc400856477cd6f708ff633d95e2275605a45", size = 8973847, upload-time = "2025-09-04T00:25:05.353Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
@@ -1615,7 +1625,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openhands"
|
||||
version = "0.1.0"
|
||||
version = "1.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "openhands-sdk" },
|
||||
@@ -1642,8 +1652,8 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openhands-sdk", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=189979a5013751aa86852ab41afe9a79555e62ac" },
|
||||
{ name = "openhands-tools", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=189979a5013751aa86852ab41afe9a79555e62ac" },
|
||||
{ name = "openhands-sdk", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=50b094a92817e448ec4352d2950df4f19edd5a9f" },
|
||||
{ name = "openhands-tools", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=50b094a92817e448ec4352d2950df4f19edd5a9f" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3" },
|
||||
{ name = "typer", specifier = ">=0.17.4" },
|
||||
]
|
||||
@@ -1667,9 +1677,10 @@ dev = [
|
||||
[[package]]
|
||||
name = "openhands-sdk"
|
||||
version = "1.0.0"
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=189979a5013751aa86852ab41afe9a79555e62ac#189979a5013751aa86852ab41afe9a79555e62ac" }
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=50b094a92817e448ec4352d2950df4f19edd5a9f#50b094a92817e448ec4352d2950df4f19edd5a9f" }
|
||||
dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
{ name = "httpx" },
|
||||
{ name = "litellm" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-frontmatter" },
|
||||
@@ -1681,7 +1692,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "openhands-tools"
|
||||
version = "1.0.0"
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=189979a5013751aa86852ab41afe9a79555e62ac#189979a5013751aa86852ab41afe9a79555e62ac" }
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=50b094a92817e448ec4352d2950df4f19edd5a9f#50b094a92817e448ec4352d2950df4f19edd5a9f" }
|
||||
dependencies = [
|
||||
{ name = "bashlex" },
|
||||
{ name = "binaryornot" },
|
||||
|
||||
+7
-42
@@ -1,44 +1,9 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
# This is a namespace package - extend the path to include installed packages
|
||||
# (We need to do this to support dependencies openhands-sdk, openhands-tools and openhands-agent-server
|
||||
# which all have a top level `openhands`` package.)
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
|
||||
__package_name__ = 'openhands_ai'
|
||||
# Import version information for backward compatibility
|
||||
from openhands.version import __version__, get_version
|
||||
|
||||
|
||||
def get_version():
|
||||
# Try getting the version from pyproject.toml
|
||||
try:
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
candidate_paths = [
|
||||
Path(root_dir) / 'pyproject.toml',
|
||||
Path(root_dir) / 'openhands' / 'pyproject.toml',
|
||||
]
|
||||
for file_path in candidate_paths:
|
||||
if file_path.is_file():
|
||||
with open(file_path, 'r') as f:
|
||||
for line in f:
|
||||
if line.strip().startswith('version ='):
|
||||
return line.split('=', 1)[1].strip().strip('"').strip("'")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
return version(__package_name__)
|
||||
except (ImportError, PackageNotFoundError):
|
||||
pass
|
||||
|
||||
try:
|
||||
from pkg_resources import DistributionNotFound, get_distribution # type: ignore
|
||||
|
||||
return get_distribution(__package_name__).version
|
||||
except (ImportError, DistributionNotFound):
|
||||
pass
|
||||
|
||||
return 'unknown'
|
||||
|
||||
|
||||
try:
|
||||
__version__ = get_version()
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
__all__ = ['__version__', 'get_version']
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
At the user's request, repository {{ repository_info.repo_name }} has been cloned to {{ repository_info.repo_directory }} in the current working directory.
|
||||
{% if repository_info.branch_name %}The repository has been checked out to branch "{{ repository_info.branch_name }}".
|
||||
|
||||
IMPORTANT: You should work within the current branch "{{ repository_info.branch_name }}" unless
|
||||
IMPORTANT: You should work within the current branch "{{ repository_info.branch_name }}" unless:
|
||||
1. the user explicitly instructs otherwise
|
||||
2. if the current branch is "main", "master", or another default branch where direct pushes may be unsafe
|
||||
2. the current branch is "main", "master", or another default branch where direct pushes may be unsafe
|
||||
{% endif %}
|
||||
</REPOSITORY_INFO>
|
||||
{% endif %}
|
||||
@@ -35,9 +35,9 @@ For example, if you are using vite.config.js, you should set server.host and ser
|
||||
{% endif %}
|
||||
{% if runtime_info.custom_secrets_descriptions %}
|
||||
<CUSTOM_SECRETS>
|
||||
You are have access to the following environment variables
|
||||
You have access to the following environment variables
|
||||
{% for secret_name, secret_description in runtime_info.custom_secrets_descriptions.items() %}
|
||||
* $**{{ secret_name }}**: {{ secret_description }}
|
||||
* **${{ secret_name }}**: {{ secret_description }}
|
||||
{% endfor %}
|
||||
</CUSTOM_SECRETS>
|
||||
{% endif %}
|
||||
|
||||
@@ -35,6 +35,7 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
* If there are existing git user credentials already configured, use them and add Co-authored-by: openhands <openhands@all-hands.dev> to any commits messages you make. if a git config doesn't exist use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
|
||||
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
|
||||
* NEVER use the `--no-verify` flag when making commits. Pre-commit hooks must be allowed to run to enforce code quality standards and project-specific checks configured in `.openhands/pre-commit.sh`.
|
||||
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
|
||||
* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
|
||||
</VERSION_CONTROL>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user