mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Merge branch 'main' into ht/support-kimi-k2-thinking
This commit is contained in:
commit
f45ab114dd
1
.devcontainer/README.md
Normal file
1
.devcontainer/README.md
Normal file
@ -0,0 +1 @@
|
||||
This way of running OpenHands is not officially supported. It is maintained by the community.
|
||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@ -13,6 +13,7 @@
|
||||
- [ ] Other (dependency update, docs, typo fixes, etc.)
|
||||
|
||||
## Checklist
|
||||
<!-- AI/LLM AGENTS: This checklist is for a human author to complete. Do NOT check either of the two boxes below. Leave them unchecked until a human has personally reviewed and tested the changes. -->
|
||||
|
||||
- [ ] I have read and reviewed the code and I understand what the code is doing.
|
||||
- [ ] I have tested the code to the best of my ability and ensured it works as expected.
|
||||
|
||||
65
.github/workflows/check-package-versions.yml
vendored
Normal file
65
.github/workflows/check-package-versions.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
name: Check Package Versions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-package-versions:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Check for any 'rev' fields in pyproject.toml
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import sys, tomllib, pathlib
|
||||
|
||||
path = pathlib.Path("pyproject.toml")
|
||||
if not path.exists():
|
||||
print("❌ ERROR: pyproject.toml not found")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR: Failed to parse pyproject.toml: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
poetry = data.get("tool", {}).get("poetry", {})
|
||||
sections = {
|
||||
"dependencies": poetry.get("dependencies", {}),
|
||||
}
|
||||
|
||||
errors = []
|
||||
|
||||
print("🔍 Checking for any dependencies with 'rev' fields...\n")
|
||||
for section_name, deps in sections.items():
|
||||
if not isinstance(deps, dict):
|
||||
continue
|
||||
|
||||
for pkg_name, cfg in deps.items():
|
||||
if isinstance(cfg, dict) and "rev" in cfg:
|
||||
msg = f" ✖ {pkg_name} in [{section_name}] uses rev='{cfg['rev']}' (NOT ALLOWED)"
|
||||
print(msg)
|
||||
errors.append(msg)
|
||||
else:
|
||||
print(f" • {pkg_name}: OK")
|
||||
|
||||
if errors:
|
||||
print("\n❌ FAILED: Found dependencies using 'rev' fields:\n" + "\n".join(errors))
|
||||
print("\nPlease use versioned releases instead, e.g.:")
|
||||
print(' my-package = "1.0.0"')
|
||||
sys.exit(1)
|
||||
|
||||
print("\n✅ SUCCESS: No 'rev' fields found. All dependencies are using proper versioned releases.")
|
||||
PY
|
||||
199
.github/workflows/integration-runner.yml
vendored
199
.github/workflows/integration-runner.yml
vendored
@ -1,199 +0,0 @@
|
||||
name: Run Integration Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
schedule:
|
||||
- cron: '30 22 * * *' # Runs at 10:30pm UTC every day
|
||||
|
||||
env:
|
||||
N_PROCESSES: 10 # Global configuration for number of parallel processes for evaluation
|
||||
|
||||
jobs:
|
||||
run-integration-tests:
|
||||
if: github.event.label.name == 'integration-test' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
contents: "read"
|
||||
id-token: "write"
|
||||
pull-requests: "write"
|
||||
issues: "write"
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: useblacksmith/setup-node@v5
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- name: Comment on PR if 'integration-test' label is present
|
||||
if: github.event_name == 'pull_request' && github.event.label.name == 'integration-test'
|
||||
uses: KeisukeYamashita/create-comment@v1
|
||||
with:
|
||||
unique: false
|
||||
comment: |
|
||||
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
|
||||
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --with dev,test,runtime,evaluation
|
||||
|
||||
- name: Configure config.toml for testing with Haiku
|
||||
env:
|
||||
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
MAX_ITERATIONS: 10
|
||||
run: |
|
||||
echo "[llm.eval]" > config.toml
|
||||
echo "model = \"$LLM_MODEL\"" >> config.toml
|
||||
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
|
||||
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
|
||||
echo "temperature = 0.0" >> config.toml
|
||||
|
||||
- name: Build environment
|
||||
run: make build
|
||||
|
||||
- name: Run integration test evaluation for Haiku
|
||||
env:
|
||||
SANDBOX_FORCE_REBUILD_RUNTIME: True
|
||||
run: |
|
||||
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'haiku_run'
|
||||
|
||||
# get integration tests report
|
||||
REPORT_FILE_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/*haiku*_maxiter_10_N* -name "report.md" -type f | head -n 1)
|
||||
echo "REPORT_FILE: $REPORT_FILE_HAIKU"
|
||||
echo "INTEGRATION_TEST_REPORT_HAIKU<<EOF" >> $GITHUB_ENV
|
||||
cat $REPORT_FILE_HAIKU >> $GITHUB_ENV
|
||||
echo >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Wait a little bit
|
||||
run: sleep 10
|
||||
|
||||
- name: Configure config.toml for testing with DeepSeek
|
||||
env:
|
||||
LLM_MODEL: "litellm_proxy/deepseek-chat"
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
MAX_ITERATIONS: 10
|
||||
run: |
|
||||
echo "[llm.eval]" > config.toml
|
||||
echo "model = \"$LLM_MODEL\"" >> config.toml
|
||||
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
|
||||
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
|
||||
echo "temperature = 0.0" >> config.toml
|
||||
|
||||
- name: Run integration test evaluation for DeepSeek
|
||||
env:
|
||||
SANDBOX_FORCE_REBUILD_RUNTIME: True
|
||||
run: |
|
||||
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'deepseek_run'
|
||||
|
||||
# get integration tests report
|
||||
REPORT_FILE_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/deepseek*_maxiter_10_N* -name "report.md" -type f | head -n 1)
|
||||
echo "REPORT_FILE: $REPORT_FILE_DEEPSEEK"
|
||||
echo "INTEGRATION_TEST_REPORT_DEEPSEEK<<EOF" >> $GITHUB_ENV
|
||||
cat $REPORT_FILE_DEEPSEEK >> $GITHUB_ENV
|
||||
echo >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
|
||||
- name: Wait a little bit (again)
|
||||
run: sleep 5
|
||||
|
||||
- name: Configure config.toml for testing VisualBrowsingAgent (DeepSeek)
|
||||
env:
|
||||
LLM_MODEL: "litellm_proxy/deepseek-chat"
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
MAX_ITERATIONS: 15
|
||||
run: |
|
||||
echo "[llm.eval]" > config.toml
|
||||
echo "model = \"$LLM_MODEL\"" >> config.toml
|
||||
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
|
||||
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
|
||||
echo "temperature = 0.0" >> config.toml
|
||||
- name: Run integration test evaluation for VisualBrowsingAgent (DeepSeek)
|
||||
env:
|
||||
SANDBOX_FORCE_REBUILD_RUNTIME: True
|
||||
run: |
|
||||
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD VisualBrowsingAgent '' 15 $N_PROCESSES "t05_simple_browsing,t06_github_pr_browsing.py" 'visualbrowsing_deepseek_run'
|
||||
|
||||
# Find and export the visual browsing agent test results
|
||||
REPORT_FILE_VISUALBROWSING_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/VisualBrowsingAgent/deepseek*_maxiter_15_N* -name "report.md" -type f | head -n 1)
|
||||
echo "REPORT_FILE_VISUALBROWSING_DEEPSEEK: $REPORT_FILE_VISUALBROWSING_DEEPSEEK"
|
||||
echo "INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK<<EOF" >> $GITHUB_ENV
|
||||
cat $REPORT_FILE_VISUALBROWSING_DEEPSEEK >> $GITHUB_ENV
|
||||
echo >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create archive of evaluation outputs
|
||||
run: |
|
||||
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
|
||||
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
|
||||
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
|
||||
|
||||
- name: Upload evaluation results as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
id: upload_results_artifact
|
||||
with:
|
||||
name: integration-test-outputs-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: integration_tests_*.tar.gz
|
||||
|
||||
- name: Get artifact URLs
|
||||
run: |
|
||||
echo "ARTIFACT_URL=${{ steps.upload_results_artifact.outputs.artifact-url }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set timestamp and trigger reason
|
||||
run: |
|
||||
echo "TIMESTAMP=$(date +'%Y-%m-%d-%H-%M')" >> $GITHUB_ENV
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "TRIGGER_REASON=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
echo "TRIGGER_REASON=manual-${{ github.event.inputs.reason }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TRIGGER_REASON=nightly-scheduled" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Comment with results and artifact link
|
||||
id: create_comment
|
||||
uses: KeisukeYamashita/create-comment@v1
|
||||
with:
|
||||
# if triggered by PR, use PR number, otherwise use 9745 as fallback issue number for manual triggers
|
||||
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || 9745 }}
|
||||
unique: false
|
||||
comment: |
|
||||
Trigger by: ${{ github.event_name == 'pull_request' && format('Pull Request (integration-test label on PR #{0})', github.event.pull_request.number) || (github.event_name == 'workflow_dispatch' && format('Manual Trigger: {0}', github.event.inputs.reason)) || 'Nightly Scheduled Run' }}
|
||||
Commit: ${{ github.sha }}
|
||||
**Integration Tests Report (Haiku)**
|
||||
Haiku LLM Test Results:
|
||||
${{ env.INTEGRATION_TEST_REPORT_HAIKU }}
|
||||
---
|
||||
**Integration Tests Report (DeepSeek)**
|
||||
DeepSeek LLM Test Results:
|
||||
${{ env.INTEGRATION_TEST_REPORT_DEEPSEEK }}
|
||||
---
|
||||
**Integration Tests Report VisualBrowsing (DeepSeek)**
|
||||
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}
|
||||
---
|
||||
Download testing outputs (includes both Haiku and DeepSeek results): [Download](${{ steps.upload_results_artifact.outputs.artifact-url }})
|
||||
32
.github/workflows/py-tests.yml
vendored
32
.github/workflows/py-tests.yml
vendored
@ -70,37 +70,7 @@ jobs:
|
||||
.coverage.${{ matrix.python_version }}
|
||||
.coverage.runtime.${{ matrix.python_version }}
|
||||
include-hidden-files: true
|
||||
# Run specific Windows python tests
|
||||
test-on-windows:
|
||||
name: Python Tests on Windows
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pipx
|
||||
run: pip install pipx
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --with dev,test,runtime
|
||||
- name: Run Windows unit tests
|
||||
run: poetry run pytest -svv tests/runtime//test_windows_bash.py
|
||||
env:
|
||||
PYTHONPATH: ".;$env:PYTHONPATH"
|
||||
DEBUG: "1"
|
||||
- name: Run Windows runtime tests with LocalRuntime
|
||||
run: $env:TEST_RUNTIME="local"; poetry run pytest -svv tests/runtime/test_bash.py
|
||||
env:
|
||||
PYTHONPATH: ".;$env:PYTHONPATH"
|
||||
TEST_RUNTIME: local
|
||||
DEBUG: "1"
|
||||
|
||||
test-enterprise:
|
||||
name: Enterprise Python Unit Tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -185,6 +185,9 @@ cython_debug/
|
||||
.repomix
|
||||
repomix-output.txt
|
||||
|
||||
# Emacs backup
|
||||
*~
|
||||
|
||||
# evaluation
|
||||
evaluation/evaluation_outputs
|
||||
evaluation/outputs
|
||||
|
||||
@ -58,7 +58,7 @@ by implementing the [interface specified here](https://github.com/OpenHands/Open
|
||||
|
||||
#### Testing
|
||||
When you write code, it is also good to write tests. Please navigate to the [`./tests`](./tests) folder to see existing test suites.
|
||||
At the moment, we have two kinds of tests: [`unit`](./tests/unit) and [`integration`](./evaluation/integration_tests). Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
|
||||
At the moment, we have these kinds of tests: [`unit`](./tests/unit), [`runtime`](./tests/runtime), and [`end-to-end (e2e)`](./tests/e2e). Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
|
||||
|
||||
## Sending Pull Requests to OpenHands
|
||||
|
||||
|
||||
@ -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/openhands/runtime:0.61-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:0.62-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@ -82,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.openhands.dev/openhands/runtime:0.61-nikolaik
|
||||
docker pull docker.openhands.dev/openhands/runtime:0.62-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.61-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.62-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.openhands.dev/openhands/openhands:0.61
|
||||
docker.openhands.dev/openhands/openhands:0.62
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Develop in Docker
|
||||
|
||||
> [!WARNING]
|
||||
> This is not officially supported and may not work.
|
||||
> This way of running OpenHands is not officially supported. It is maintained by the community and may not work.
|
||||
|
||||
Install [Docker](https://docs.docker.com/engine/install/) on your host machine and run:
|
||||
|
||||
|
||||
@ -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/openhands/runtime:0.61-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/openhands/runtime:0.62-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.61-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.62-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:
|
||||
|
||||
53
enterprise/poetry.lock
generated
53
enterprise/poetry.lock
generated
@ -5820,13 +5820,15 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
|
||||
|
||||
[[package]]
|
||||
name = "openhands-agent-server"
|
||||
version = "1.0.0a5"
|
||||
version = "1.1.0"
|
||||
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
files = [
|
||||
{file = "openhands_agent_server-1.1.0-py3-none-any.whl", hash = "sha256:59a856883df23488c0723e47655ef21649a321fcd4709a25a4690866eff6ac88"},
|
||||
{file = "openhands_agent_server-1.1.0.tar.gz", hash = "sha256:e39bebd39afd45cfcfd765005e7c4e5409e46678bd7612ae20bae79f7057b935"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiosqlite = ">=0.19"
|
||||
@ -5839,16 +5841,9 @@ uvicorn = ">=0.31.1"
|
||||
websockets = ">=12"
|
||||
wsproto = ">=1.2.0"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/OpenHands/software-agent-sdk.git"
|
||||
reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
resolved_reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
subdirectory = "openhands-agent-server"
|
||||
|
||||
[[package]]
|
||||
name = "openhands-ai"
|
||||
version = "0.0.0-post.5514+7c9e66194"
|
||||
version = "0.0.0-post.5525+0b6631523"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
optional = false
|
||||
python-versions = "^3.12,<3.14"
|
||||
@ -5889,9 +5884,9 @@ memory-profiler = "^0.61.0"
|
||||
numpy = "*"
|
||||
openai = "1.99.9"
|
||||
openhands-aci = "0.3.2"
|
||||
openhands-agent-server = {git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "d5995c31c55e488d4ab0372d292973bc6fad71f1", subdirectory = "openhands-agent-server"}
|
||||
openhands-sdk = {git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "d5995c31c55e488d4ab0372d292973bc6fad71f1", subdirectory = "openhands-sdk"}
|
||||
openhands-tools = {git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "d5995c31c55e488d4ab0372d292973bc6fad71f1", subdirectory = "openhands-tools"}
|
||||
openhands-agent-server = "1.1.0"
|
||||
openhands-sdk = "1.1.0"
|
||||
openhands-tools = "1.1.0"
|
||||
opentelemetry-api = "^1.33.1"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
|
||||
pathspec = "^0.12.1"
|
||||
@ -5947,13 +5942,15 @@ url = ".."
|
||||
|
||||
[[package]]
|
||||
name = "openhands-sdk"
|
||||
version = "1.0.0a5"
|
||||
version = "1.1.0"
|
||||
description = "OpenHands SDK - Core functionality for building AI agents"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
files = [
|
||||
{file = "openhands_sdk-1.1.0-py3-none-any.whl", hash = "sha256:4a984ce1687a48cf99a67fdf3d37b116f8b2840743d4807810b5024af6a1d57e"},
|
||||
{file = "openhands_sdk-1.1.0.tar.gz", hash = "sha256:855e0d8f3657205e4119e50520c17e65b3358b1a923f7a051a82512a54bf426c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
fastmcp = ">=2.11.3"
|
||||
@ -5969,22 +5966,17 @@ websockets = ">=12"
|
||||
[package.extras]
|
||||
boto3 = ["boto3 (>=1.35.0)"]
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/OpenHands/software-agent-sdk.git"
|
||||
reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
resolved_reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
subdirectory = "openhands-sdk"
|
||||
|
||||
[[package]]
|
||||
name = "openhands-tools"
|
||||
version = "1.0.0a5"
|
||||
version = "1.1.0"
|
||||
description = "OpenHands Tools - Runtime tools for AI agents"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
files = [
|
||||
{file = "openhands_tools-1.1.0-py3-none-any.whl", hash = "sha256:767d6746f05edade49263aa24450a037485a3dc23379f56917ef19aad22033f9"},
|
||||
{file = "openhands_tools-1.1.0.tar.gz", hash = "sha256:c2fadaa4f4e16e9a3df5781ea847565dcae7171584f09ef7c0e1d97c8dfc83f6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
bashlex = ">=0.18"
|
||||
@ -5996,13 +5988,6 @@ libtmux = ">=0.46.2"
|
||||
openhands-sdk = "*"
|
||||
pydantic = ">=2.11.7"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/OpenHands/software-agent-sdk.git"
|
||||
reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
resolved_reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
subdirectory = "openhands-tools"
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
|
||||
@ -15,7 +15,7 @@ python evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py
|
||||
|
||||
## Docker image download
|
||||
|
||||
Please download the multi-swe-bench dokcer images from [here](https://github.com/multi-swe-bench/multi-swe-bench?tab=readme-ov-file#run-evaluation).
|
||||
Please download the multi-swe-bench docker images from [here](https://github.com/multi-swe-bench/multi-swe-bench?tab=readme-ov-file#run-evaluation).
|
||||
|
||||
## Generate patch
|
||||
|
||||
@ -47,7 +47,7 @@ For debugging purposes, you can set `export EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=t
|
||||
|
||||
The results will be generated in evaluation/evaluation_outputs/outputs/XXX/CodeActAgent/YYY/output.jsonl, you can refer to the [example](examples/output.jsonl).
|
||||
|
||||
## Runing evaluation
|
||||
## Running evaluation
|
||||
|
||||
First, install [multi-swe-bench](https://github.com/multi-swe-bench/multi-swe-bench).
|
||||
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
# Integration tests
|
||||
|
||||
This directory implements integration tests that [was running in CI](https://github.com/OpenHands/OpenHands/tree/23d3becf1d6f5d07e592f7345750c314a826b4e9/tests/integration).
|
||||
|
||||
[PR 3985](https://github.com/OpenHands/OpenHands/pull/3985) introduce LLM-based editing, which requires access to LLM to perform edit. Hence, we remove integration tests from CI and intend to run them as nightly evaluation to ensure the quality of OpenHands softwares.
|
||||
|
||||
## To add new tests
|
||||
|
||||
Each test is a file named like `tXX_testname.py` where `XX` is a number.
|
||||
Make sure to name the file for each test to start with `t` and ends with `.py`.
|
||||
|
||||
Each test should be structured as a subclass of [`BaseIntegrationTest`](./tests/base.py), where you need to implement `initialize_runtime` that setup the runtime enviornment before test, and `verify_result` that takes in a `Runtime` and history of `Event` and return a `TestResult`. See [t01_fix_simple_typo.py](./tests/t01_fix_simple_typo.py) and [t05_simple_browsing.py](./tests/t05_simple_browsing.py) for two representative examples.
|
||||
|
||||
```python
|
||||
class TestResult(BaseModel):
|
||||
success: bool
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class BaseIntegrationTest(ABC):
|
||||
"""Base class for integration tests."""
|
||||
|
||||
INSTRUCTION: str
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
"""Initialize the runtime for the test to run."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
"""Verify the result of the test.
|
||||
|
||||
This method will be called after the agent performs the task on the runtime.
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
## Setup Environment and LLM Configuration
|
||||
|
||||
Please follow instruction [here](../README.md#setup) to setup your local
|
||||
development environment and LLM.
|
||||
|
||||
## Start the evaluation
|
||||
|
||||
```bash
|
||||
./evaluation/integration_tests/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [eval-num-workers] [eval_ids]
|
||||
```
|
||||
|
||||
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for
|
||||
your LLM settings, as defined in your `config.toml`.
|
||||
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version
|
||||
you would like to evaluate. It could also be a release tag like `0.9.0`.
|
||||
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks,
|
||||
defaulting to `CodeActAgent`.
|
||||
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit`
|
||||
instances. By default, the script evaluates the entire Exercism test set
|
||||
(133 issues). Note: in order to use `eval_limit`, you must also set `agent`.
|
||||
- `eval-num-workers`: the number of workers to use for evaluation. Default: `1`.
|
||||
- `eval_ids`, e.g. `"1,3,10"`, limits the evaluation to instances with the
|
||||
given IDs (comma separated).
|
||||
|
||||
Example:
|
||||
```bash
|
||||
./evaluation/integration_tests/scripts/run_infer.sh llm.claude-35-sonnet-eval HEAD CodeActAgent
|
||||
```
|
||||
@ -1,251 +0,0 @@
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
get_openhands_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
update_llm_config_for_completions_logging,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
codeact_user_response as fake_user_response,
|
||||
)
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
OpenHandsConfig,
|
||||
get_evaluation_parser,
|
||||
get_llm_config_arg,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.serialization.event import event_to_dict
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
|
||||
FAKE_RESPONSES = {
|
||||
'CodeActAgent': fake_user_response,
|
||||
'VisualBrowsingAgent': fake_user_response,
|
||||
}
|
||||
|
||||
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
instance_id: str,
|
||||
) -> OpenHandsConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.platform = 'linux/amd64'
|
||||
config = get_openhands_config_for_eval(
|
||||
metadata=metadata,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox_config=sandbox_config,
|
||||
)
|
||||
config.debug = True
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
metadata.llm_config, metadata.eval_output_dir, instance_id
|
||||
)
|
||||
)
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=True,
|
||||
enable_browsing=True,
|
||||
enable_llm_editor=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
reset_logger: bool = True,
|
||||
) -> EvalOutput:
|
||||
config = get_config(metadata, instance.instance_id)
|
||||
|
||||
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
|
||||
if reset_logger:
|
||||
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
|
||||
reset_logger_for_multiprocessing(logger, str(instance.instance_id), log_dir)
|
||||
else:
|
||||
logger.info(
|
||||
f'\nStarting evaluation for instance {str(instance.instance_id)}.\n'
|
||||
)
|
||||
|
||||
# =============================================
|
||||
# import test instance
|
||||
# =============================================
|
||||
instance_id = instance.instance_id
|
||||
spec = importlib.util.spec_from_file_location(instance_id, instance.file_path)
|
||||
test_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(test_module)
|
||||
assert hasattr(test_module, 'Test'), (
|
||||
f'Test module {instance_id} does not have a Test class'
|
||||
)
|
||||
|
||||
test_class: type[BaseIntegrationTest] = test_module.Test
|
||||
assert issubclass(test_class, BaseIntegrationTest), (
|
||||
f'Test class {instance_id} does not inherit from BaseIntegrationTest'
|
||||
)
|
||||
|
||||
instruction = test_class.INSTRUCTION
|
||||
|
||||
# =============================================
|
||||
# create sandbox and run the agent
|
||||
# =============================================
|
||||
runtime: Runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
try:
|
||||
test_class.initialize_runtime(runtime)
|
||||
|
||||
# Here's how you can run the agent (similar to the `main` function) and get the final task state
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=MessageAction(content=instruction),
|
||||
runtime=runtime,
|
||||
fake_user_response_fn=FAKE_RESPONSES[metadata.agent_class],
|
||||
)
|
||||
)
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
# # =============================================
|
||||
# # result evaluation
|
||||
# # =============================================
|
||||
|
||||
histories = state.history
|
||||
|
||||
# some basic check
|
||||
logger.info(f'Total events in history: {len(histories)}')
|
||||
assert len(histories) > 0, 'History should not be empty'
|
||||
|
||||
test_result: TestResult = test_class.verify_result(runtime, histories)
|
||||
metrics = get_metrics(state)
|
||||
finally:
|
||||
runtime.close()
|
||||
|
||||
# Save the output
|
||||
output = EvalOutput(
|
||||
instance_id=str(instance.instance_id),
|
||||
instance=instance.to_dict(),
|
||||
instruction=instruction,
|
||||
metadata=metadata,
|
||||
history=[event_to_dict(event) for event in histories],
|
||||
metrics=metrics,
|
||||
error=state.last_error if state and state.last_error else None,
|
||||
test_result=test_result.model_dump(),
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def load_integration_tests() -> pd.DataFrame:
|
||||
"""Load tests from python files under ./tests"""
|
||||
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
test_dir = os.path.join(cur_dir, 'tests')
|
||||
test_files = [
|
||||
os.path.join(test_dir, f)
|
||||
for f in os.listdir(test_dir)
|
||||
if f.startswith('t') and f.endswith('.py')
|
||||
]
|
||||
df = pd.DataFrame(test_files, columns=['file_path'])
|
||||
df['instance_id'] = df['file_path'].apply(
|
||||
lambda x: os.path.basename(x).rstrip('.py')
|
||||
)
|
||||
return df
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_evaluation_parser()
|
||||
args, _ = parser.parse_known_args()
|
||||
integration_tests = load_integration_tests()
|
||||
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
metadata = make_metadata(
|
||||
llm_config,
|
||||
'integration_tests',
|
||||
args.agent_cls,
|
||||
args.max_iterations,
|
||||
args.eval_note,
|
||||
args.eval_output_dir,
|
||||
)
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
|
||||
# Parse dataset IDs if provided
|
||||
eval_ids = None
|
||||
if args.eval_ids:
|
||||
eval_ids = str(args.eval_ids).split(',')
|
||||
logger.info(f'\nUsing specific dataset IDs: {eval_ids}\n')
|
||||
|
||||
instances = prepare_dataset(
|
||||
integration_tests,
|
||||
output_file,
|
||||
args.eval_n_limit,
|
||||
eval_ids=eval_ids,
|
||||
)
|
||||
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
)
|
||||
|
||||
df = pd.read_json(output_file, lines=True, orient='records')
|
||||
|
||||
# record success and reason
|
||||
df['success'] = df['test_result'].apply(lambda x: x['success'])
|
||||
df['reason'] = df['test_result'].apply(lambda x: x['reason'])
|
||||
logger.info('-' * 100)
|
||||
logger.info(
|
||||
f'Success rate: {df["success"].mean():.2%} ({df["success"].sum()}/{len(df)})'
|
||||
)
|
||||
logger.info(
|
||||
'\nEvaluation Results:'
|
||||
+ '\n'
|
||||
+ df[['instance_id', 'success', 'reason']].to_string(index=False)
|
||||
)
|
||||
logger.info('-' * 100)
|
||||
|
||||
# record cost for each instance, with 3 decimal places
|
||||
# we sum up all the "costs" from the metrics array
|
||||
df['cost'] = df['metrics'].apply(
|
||||
lambda m: round(sum(c['cost'] for c in m['costs']), 3)
|
||||
if m and 'costs' in m
|
||||
else 0.0
|
||||
)
|
||||
|
||||
# capture the top-level error if present, per instance
|
||||
df['error_message'] = df.get('error', None)
|
||||
|
||||
logger.info(f'Total cost: USD {df["cost"].sum():.2f}')
|
||||
|
||||
report_file = os.path.join(metadata.eval_output_dir, 'report.md')
|
||||
with open(report_file, 'w') as f:
|
||||
f.write(
|
||||
f'Success rate: {df["success"].mean():.2%}'
|
||||
f' ({df["success"].sum()}/{len(df)})\n'
|
||||
)
|
||||
f.write(f'\nTotal cost: USD {df["cost"].sum():.2f}\n')
|
||||
f.write(
|
||||
df[
|
||||
['instance_id', 'success', 'reason', 'cost', 'error_message']
|
||||
].to_markdown(index=False)
|
||||
)
|
||||
@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
MODEL_CONFIG=$1
|
||||
COMMIT_HASH=$2
|
||||
AGENT=$3
|
||||
EVAL_LIMIT=$4
|
||||
MAX_ITERATIONS=$5
|
||||
NUM_WORKERS=$6
|
||||
EVAL_IDS=$7
|
||||
|
||||
if [ -z "$NUM_WORKERS" ]; then
|
||||
NUM_WORKERS=1
|
||||
echo "Number of workers not specified, use default $NUM_WORKERS"
|
||||
fi
|
||||
checkout_eval_branch
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
echo "Agent not specified, use default CodeActAgent"
|
||||
AGENT="CodeActAgent"
|
||||
fi
|
||||
|
||||
get_openhands_version
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
|
||||
EVAL_NOTE=$OPENHANDS_VERSION
|
||||
|
||||
# Default to NOT use unit tests.
|
||||
if [ -z "$USE_UNIT_TESTS" ]; then
|
||||
export USE_UNIT_TESTS=false
|
||||
fi
|
||||
echo "USE_UNIT_TESTS: $USE_UNIT_TESTS"
|
||||
# If use unit tests, set EVAL_NOTE to the commit hash
|
||||
if [ "$USE_UNIT_TESTS" = true ]; then
|
||||
EVAL_NOTE=$EVAL_NOTE-w-test
|
||||
fi
|
||||
|
||||
# export PYTHONPATH=evaluation/integration_tests:\$PYTHONPATH
|
||||
COMMAND="poetry run python evaluation/integration_tests/run_infer.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--max-iterations ${MAX_ITERATIONS:-10} \
|
||||
--eval-num-workers $NUM_WORKERS \
|
||||
--eval-note $EVAL_NOTE"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
if [ -n "$EVAL_IDS" ]; then
|
||||
echo "EVAL_IDS: $EVAL_IDS"
|
||||
COMMAND="$COMMAND --eval-ids $EVAL_IDS"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
@ -1,32 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.events.event import Event
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class TestResult(BaseModel):
|
||||
success: bool
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
class BaseIntegrationTest(ABC):
|
||||
"""Base class for integration tests."""
|
||||
|
||||
INSTRUCTION: str
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
"""Initialize the runtime for the test to run."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
"""Verify the result of the test.
|
||||
|
||||
This method will be called after the agent performs the task on the runtime.
|
||||
"""
|
||||
pass
|
||||
@ -1,39 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from openhands.events.action import CmdRunAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = 'Fix typos in bad.txt.'
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
# create a file with a typo in /workspace/bad.txt
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_file_path = os.path.join(temp_dir, 'bad.txt')
|
||||
with open(temp_file_path, 'w') as f:
|
||||
f.write('This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!')
|
||||
|
||||
# Copy the file to the desired location
|
||||
runtime.copy_to(temp_file_path, '/workspace')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/bad.txt has been fixed
|
||||
action = CmdRunAction(command='cat /workspace/bad.txt')
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False, reason=f'Failed to run command: {obs.content}'
|
||||
)
|
||||
# check if the file /workspace/bad.txt has been fixed
|
||||
if (
|
||||
obs.content.strip().replace('\r\n', '\n')
|
||||
== 'This is a stupid typo.\nReally?\nNo more typos!\nEnjoy!'
|
||||
):
|
||||
return TestResult(success=True)
|
||||
return TestResult(success=False, reason=f'File not fixed: {obs.content}')
|
||||
@ -1,40 +0,0 @@
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from evaluation.utils.shared import assert_and_raise
|
||||
from openhands.events.action import CmdRunAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = "Write a shell script '/workspace/hello.sh' that prints 'hello'."
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/hello.sh exists
|
||||
action = CmdRunAction(command='cat /workspace/hello.sh')
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'Failed to cat /workspace/hello.sh: {obs.content}.',
|
||||
)
|
||||
|
||||
# execute the script
|
||||
action = CmdRunAction(command='bash /workspace/hello.sh')
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'Failed to execute /workspace/hello.sh: {obs.content}.',
|
||||
)
|
||||
if obs.content.strip() != 'hello':
|
||||
return TestResult(
|
||||
success=False, reason=f'Script did not print "hello": {obs.content}.'
|
||||
)
|
||||
return TestResult(success=True)
|
||||
@ -1,43 +0,0 @@
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from evaluation.utils.shared import assert_and_raise
|
||||
from openhands.events.action import CmdRunAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = "Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'."
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/hello.sh exists
|
||||
action = CmdRunAction(command='cat /workspace/test.txt')
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'Failed to cat /workspace/test.txt: {obs.content}.',
|
||||
)
|
||||
|
||||
# execute the script
|
||||
action = CmdRunAction(command='cat /workspace/test.txt')
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'Failed to cat /workspace/test.txt: {obs.content}.',
|
||||
)
|
||||
|
||||
if 'hello world' not in obs.content.strip():
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'File did not contain "hello world": {obs.content}.',
|
||||
)
|
||||
return TestResult(success=True)
|
||||
@ -1,57 +0,0 @@
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from evaluation.utils.shared import assert_and_raise
|
||||
from openhands.events.action import CmdRunAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = 'Write a git commit message for the current staging area and commit the changes.'
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# git init
|
||||
action = CmdRunAction(command='git init')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# create file
|
||||
action = CmdRunAction(command='echo \'print("hello world")\' > hello.py')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# git add
|
||||
cmd_str = 'git add hello.py'
|
||||
action = CmdRunAction(command=cmd_str)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/hello.py exists
|
||||
action = CmdRunAction(command='cat /workspace/hello.py')
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'Failed to cat /workspace/hello.py: {obs.content}.',
|
||||
)
|
||||
|
||||
# check if the staging area is empty
|
||||
action = CmdRunAction(command='git status')
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
success=False, reason=f'Failed to git status: {obs.content}.'
|
||||
)
|
||||
if 'nothing to commit, working tree clean' in obs.content.strip():
|
||||
return TestResult(success=True)
|
||||
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'Failed to check for "nothing to commit, working tree clean": {obs.content}.',
|
||||
)
|
||||
@ -1,145 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from evaluation.utils.shared import assert_and_raise
|
||||
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import AgentDelegateObservation
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
HTML_FILE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Ultimate Answer</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(to right, #1e3c72, #2a5298);
|
||||
color: #fff;
|
||||
font-family: 'Arial', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
#showButton {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
color: #1e3c72;
|
||||
background: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
#showButton:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
#result {
|
||||
margin-top: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>The Ultimate Answer</h1>
|
||||
<p>Click the button to reveal the answer to life, the universe, and everything.</p>
|
||||
<button id="showButton">Click me</button>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('showButton').addEventListener('click', function() {
|
||||
document.getElementById('result').innerText = 'The answer is OpenHands is all you need!';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = 'Browse localhost:8000, and tell me the ultimate answer to life.'
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
action = CmdRunAction(command='mkdir -p /tmp/server')
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# create a file with a typo in /workspace/bad.txt
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_file_path = os.path.join(temp_dir, 'index.html')
|
||||
with open(temp_file_path, 'w') as f:
|
||||
f.write(HTML_FILE)
|
||||
# Copy the file to the desired location
|
||||
runtime.copy_to(temp_file_path, '/tmp/server')
|
||||
|
||||
# create README.md
|
||||
action = CmdRunAction(
|
||||
command='cd /tmp/server && nohup python3 -m http.server 8000 &'
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
# check if the "The answer is OpenHands is all you need!" is in any message
|
||||
message_actions = [
|
||||
event
|
||||
for event in histories
|
||||
if isinstance(
|
||||
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
|
||||
)
|
||||
]
|
||||
logger.debug(f'Total message-like events: {len(message_actions)}')
|
||||
|
||||
for event in message_actions:
|
||||
try:
|
||||
if isinstance(event, AgentDelegateObservation):
|
||||
content = event.content
|
||||
elif isinstance(event, AgentFinishAction):
|
||||
content = event.outputs.get('content', '')
|
||||
elif isinstance(event, MessageAction):
|
||||
content = event.content
|
||||
else:
|
||||
logger.warning(f'Unexpected event type: {type(event)}')
|
||||
continue
|
||||
|
||||
if 'OpenHands is all you need!' in content:
|
||||
return TestResult(success=True)
|
||||
except Exception as e:
|
||||
logger.error(f'Error processing event: {e}')
|
||||
|
||||
logger.debug(
|
||||
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
|
||||
)
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
|
||||
)
|
||||
@ -1,58 +0,0 @@
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from openhands.events.action import AgentFinishAction, MessageAction
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import AgentDelegateObservation
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = 'Look at https://github.com/OpenHands/OpenHands/pull/8, and tell me what is happening there and what did @asadm suggest.'
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
# check if the license information is in any message
|
||||
message_actions = [
|
||||
event
|
||||
for event in histories
|
||||
if isinstance(
|
||||
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
|
||||
)
|
||||
]
|
||||
logger.info(f'Total message-like events: {len(message_actions)}')
|
||||
|
||||
for event in message_actions:
|
||||
try:
|
||||
if isinstance(event, AgentDelegateObservation):
|
||||
content = event.content
|
||||
elif isinstance(event, AgentFinishAction):
|
||||
content = event.outputs.get('content', '')
|
||||
if event.thought:
|
||||
content += f'\n\n{event.thought}'
|
||||
elif isinstance(event, MessageAction):
|
||||
content = event.content
|
||||
else:
|
||||
logger.warning(f'Unexpected event type: {type(event)}')
|
||||
continue
|
||||
|
||||
if (
|
||||
'non-commercial' in content
|
||||
or 'MIT' in content
|
||||
or 'Apache 2.0' in content
|
||||
):
|
||||
return TestResult(success=True)
|
||||
except Exception as e:
|
||||
logger.error(f'Error processing event: {e}')
|
||||
|
||||
logger.debug(
|
||||
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
|
||||
)
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
|
||||
)
|
||||
@ -1,73 +0,0 @@
|
||||
import hashlib
|
||||
|
||||
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
|
||||
from openhands.events.action import (
|
||||
AgentFinishAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import AgentDelegateObservation
|
||||
from openhands.runtime.base import Runtime
|
||||
|
||||
|
||||
class Test(BaseIntegrationTest):
|
||||
INSTRUCTION = 'Execute the python script /workspace/python_script.py with input "John" and "25" and tell me the secret number.'
|
||||
SECRET_NUMBER = int(hashlib.sha256(str(25).encode()).hexdigest()[:8], 16) % 1000
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
action = FileWriteAction(
|
||||
path='/workspace/python_script.py',
|
||||
content=(
|
||||
'name = input("Enter your name: "); age = input("Enter your age: "); '
|
||||
'import hashlib; secret = int(hashlib.sha256(str(age).encode()).hexdigest()[:8], 16) % 1000; '
|
||||
'print(f"Hello {name}, you are {age} years old. Tell you a secret number: {secret}")'
|
||||
),
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
observation = runtime.run_action(action)
|
||||
logger.info(observation, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
# check if the license information is in any message
|
||||
message_actions = [
|
||||
event
|
||||
for event in histories
|
||||
if isinstance(
|
||||
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
|
||||
)
|
||||
]
|
||||
logger.info(f'Total message-like events: {len(message_actions)}')
|
||||
|
||||
for event in message_actions:
|
||||
try:
|
||||
if isinstance(event, AgentDelegateObservation):
|
||||
content = event.content
|
||||
elif isinstance(event, AgentFinishAction):
|
||||
content = event.outputs.get('content', '')
|
||||
if event.thought:
|
||||
content += f'\n\n{event.thought}'
|
||||
elif isinstance(event, MessageAction):
|
||||
content = event.content
|
||||
else:
|
||||
logger.warning(f'Unexpected event type: {type(event)}')
|
||||
continue
|
||||
|
||||
if str(cls.SECRET_NUMBER) in content:
|
||||
return TestResult(success=True)
|
||||
except Exception as e:
|
||||
logger.error(f'Error processing event: {e}')
|
||||
|
||||
logger.debug(
|
||||
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
|
||||
)
|
||||
return TestResult(
|
||||
success=False,
|
||||
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
|
||||
)
|
||||
@ -33,9 +33,24 @@ describe("AccountSettingsContextMenu", () => {
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("SIDEBAR$DOCS")).toBeInTheDocument();
|
||||
expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render Documentation link with correct attributes", () => {
|
||||
renderWithRouter(
|
||||
<AccountSettingsContextMenu
|
||||
onLogout={onLogoutMock}
|
||||
onClose={onCloseMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const documentationLink = screen.getByText("SIDEBAR$DOCS").closest("a");
|
||||
expect(documentationLink).toHaveAttribute("href", "https://docs.openhands.dev");
|
||||
expect(documentationLink).toHaveAttribute("target", "_blank");
|
||||
expect(documentationLink).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("should call onLogout when the logout option is clicked", async () => {
|
||||
renderWithRouter(
|
||||
<AccountSettingsContextMenu
|
||||
|
||||
@ -8,6 +8,13 @@ vi.mock("#/hooks/use-auth-url", () => ({
|
||||
useAuthUrl: () => "https://gitlab.com/oauth/authorize",
|
||||
}));
|
||||
|
||||
// Mock the useTracking hook
|
||||
vi.mock("#/hooks/use-tracking", () => ({
|
||||
useTracking: () => ({
|
||||
trackLoginButtonClick: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("AuthModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
|
||||
@ -21,6 +21,7 @@ const mockUseConfig = vi.fn();
|
||||
const mockUseRepositoryMicroagents = vi.fn();
|
||||
const mockUseMicroagentManagementConversations = vi.fn();
|
||||
const mockUseSearchRepositories = vi.fn();
|
||||
const mockUseCreateConversationAndSubscribeMultiple = vi.fn();
|
||||
|
||||
vi.mock("#/hooks/use-user-providers", () => ({
|
||||
useUserProviders: () => mockUseUserProviders(),
|
||||
@ -47,6 +48,17 @@ vi.mock("#/hooks/query/use-search-repositories", () => ({
|
||||
useSearchRepositories: () => mockUseSearchRepositories(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-tracking", () => ({
|
||||
useTracking: () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-create-conversation-and-subscribe-multiple", () => ({
|
||||
useCreateConversationAndSubscribeMultiple: () =>
|
||||
mockUseCreateConversationAndSubscribeMultiple(),
|
||||
}));
|
||||
|
||||
describe("MicroagentManagement", () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
@ -309,6 +321,16 @@ describe("MicroagentManagement", () => {
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseCreateConversationAndSubscribeMultiple.mockReturnValue({
|
||||
createConversationAndSubscribe: vi.fn(({ onSuccessCallback }) => {
|
||||
// Immediately call the success callback to close the modal
|
||||
if (onSuccessCallback) {
|
||||
onSuccessCallback();
|
||||
}
|
||||
}),
|
||||
isPending: false,
|
||||
});
|
||||
|
||||
// Mock the search repositories hook to return repositories with OpenHands suffixes
|
||||
const mockSearchResults =
|
||||
getRepositoriesWithOpenHandsSuffix(mockRepositories);
|
||||
|
||||
@ -30,7 +30,7 @@ describe("ImagePreview", () => {
|
||||
expect(onRemoveMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("shoud not display the close button when onRemove is not provided", () => {
|
||||
it("should not display the close button when onRemove is not provided", () => {
|
||||
render(<ImagePreview src="https://example.com/image.jpg" />);
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import AcceptTOS from "#/routes/accept-tos";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
|
||||
// Mock the react-router hooks
|
||||
@ -44,9 +43,13 @@ const createWrapper = () => {
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
describe("AcceptTOS", () => {
|
||||
@ -106,7 +109,10 @@ describe("AcceptTOS", () => {
|
||||
// Wait for the mutation to complete
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
true,
|
||||
);
|
||||
expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", {
|
||||
redirect_url: "/dashboard",
|
||||
});
|
||||
|
||||
@ -46,6 +46,21 @@ describe("Content", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render analytics toggle as enabled when server returns null (opt-in by default)", async () => {
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
user_consents_to_analytics: null,
|
||||
});
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const analytics = screen.getByTestId("enable-analytics-switch");
|
||||
expect(analytics).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the language options", async () => {
|
||||
renderAppSettingsScreen();
|
||||
|
||||
@ -163,7 +178,10 @@ describe("Form submission", () => {
|
||||
await userEvent.click(submit);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(true),
|
||||
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@ -188,7 +206,10 @@ describe("Form submission", () => {
|
||||
await userEvent.click(submit);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(false),
|
||||
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
false,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ describe("Error Handler", () => {
|
||||
const error = {
|
||||
message: "Test error",
|
||||
source: "test",
|
||||
posthog,
|
||||
};
|
||||
|
||||
trackError(error);
|
||||
@ -52,6 +53,7 @@ describe("Error Handler", () => {
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
},
|
||||
posthog,
|
||||
};
|
||||
|
||||
trackError(error);
|
||||
@ -73,6 +75,7 @@ describe("Error Handler", () => {
|
||||
const error = {
|
||||
message: "Toast error",
|
||||
source: "toast-test",
|
||||
posthog,
|
||||
};
|
||||
|
||||
showErrorToast(error);
|
||||
@ -94,6 +97,7 @@ describe("Error Handler", () => {
|
||||
message: "Toast error",
|
||||
source: "toast-test",
|
||||
metadata: { context: "testing" },
|
||||
posthog,
|
||||
};
|
||||
|
||||
showErrorToast(error);
|
||||
@ -113,6 +117,7 @@ describe("Error Handler", () => {
|
||||
message: "Agent error",
|
||||
source: "agent-status",
|
||||
metadata: { id: "error.agent" },
|
||||
posthog,
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
@ -127,6 +132,7 @@ describe("Error Handler", () => {
|
||||
message: "Server error",
|
||||
source: "server",
|
||||
metadata: { error_code: 500, details: "Internal error" },
|
||||
posthog,
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
@ -145,6 +151,7 @@ describe("Error Handler", () => {
|
||||
message: error.message,
|
||||
source: "feedback",
|
||||
metadata: { conversationId: "123", error },
|
||||
posthog,
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
@ -164,6 +171,7 @@ describe("Error Handler", () => {
|
||||
message: "Chat error",
|
||||
source: "chat-test",
|
||||
msgId: "123",
|
||||
posthog,
|
||||
};
|
||||
|
||||
showChatError(error);
|
||||
|
||||
@ -13,14 +13,14 @@ describe("handleCaptureConsent", () => {
|
||||
});
|
||||
|
||||
it("should opt out of of capturing", () => {
|
||||
handleCaptureConsent(false);
|
||||
handleCaptureConsent(posthog, false);
|
||||
|
||||
expect(optOutSpy).toHaveBeenCalled();
|
||||
expect(optInSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should opt in to capturing if the user consents", () => {
|
||||
handleCaptureConsent(true);
|
||||
handleCaptureConsent(posthog, true);
|
||||
|
||||
expect(optInSpy).toHaveBeenCalled();
|
||||
expect(optOutSpy).not.toHaveBeenCalled();
|
||||
@ -28,7 +28,7 @@ describe("handleCaptureConsent", () => {
|
||||
|
||||
it("should not opt in to capturing if the user is already opted in", () => {
|
||||
hasOptedInSpy.mockReturnValueOnce(true);
|
||||
handleCaptureConsent(true);
|
||||
handleCaptureConsent(posthog, true);
|
||||
|
||||
expect(optInSpy).not.toHaveBeenCalled();
|
||||
expect(optOutSpy).not.toHaveBeenCalled();
|
||||
@ -36,7 +36,7 @@ describe("handleCaptureConsent", () => {
|
||||
|
||||
it("should not opt out of capturing if the user is already opted out", () => {
|
||||
hasOptedOutSpy.mockReturnValueOnce(true);
|
||||
handleCaptureConsent(false);
|
||||
handleCaptureConsent(posthog, false);
|
||||
|
||||
expect(optOutSpy).not.toHaveBeenCalled();
|
||||
expect(optInSpy).not.toHaveBeenCalled();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getStatusCode, getIndicatorColor, IndicatorColor } from "../status";
|
||||
import { getStatusCode, getIndicatorColor, IndicatorColor } from "#/utils/status";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
@ -87,6 +87,36 @@ describe("getStatusCode", () => {
|
||||
// Should return runtime status since no agent state
|
||||
expect(result).toBe("STATUS$STARTING_RUNTIME");
|
||||
});
|
||||
|
||||
it("should prioritize task ERROR status over websocket CONNECTING state", () => {
|
||||
// Test case: Task has errored but websocket is still trying to connect
|
||||
const result = getStatusCode(
|
||||
{ id: "", message: "", type: "info", status_update: true }, // statusMessage
|
||||
"CONNECTING", // webSocketStatus (stuck connecting)
|
||||
null, // conversationStatus
|
||||
null, // runtimeStatus
|
||||
AgentState.LOADING, // agentState
|
||||
"ERROR", // taskStatus (ERROR)
|
||||
);
|
||||
|
||||
// Should return error message, not "Connecting..."
|
||||
expect(result).toBe(I18nKey.AGENT_STATUS$ERROR_OCCURRED);
|
||||
});
|
||||
|
||||
it("should show Connecting when task is working and websocket is connecting", () => {
|
||||
// Test case: Task is in progress and websocket is connecting normally
|
||||
const result = getStatusCode(
|
||||
{ id: "", message: "", type: "info", status_update: true }, // statusMessage
|
||||
"CONNECTING", // webSocketStatus
|
||||
null, // conversationStatus
|
||||
null, // runtimeStatus
|
||||
AgentState.LOADING, // agentState
|
||||
"WORKING", // taskStatus (in progress)
|
||||
);
|
||||
|
||||
// Should show connecting message since task hasn't errored
|
||||
expect(result).toBe(I18nKey.CHAT_INTERFACE$CONNECTING);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIndicatorColor", () => {
|
||||
60
frontend/package-lock.json
generated
60
frontend/package-lock.json
generated
@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.61.0",
|
||||
"version": "0.62.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.61.0",
|
||||
"version": "0.62.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@posthog/react": "^1.4.0",
|
||||
"@react-router/node": "^7.9.3",
|
||||
"@react-router/serve": "^7.9.3",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
@ -38,7 +39,7 @@
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"posthog-js": "^1.268.8",
|
||||
"posthog-js": "^1.290.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@ -3511,9 +3512,29 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.2.tgz",
|
||||
"integrity": "sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg=="
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.5.2.tgz",
|
||||
"integrity": "sha512-iedUP3EnOPPxTA2VaIrsrd29lSZnUV+ZrMnvY56timRVeZAXoYCkmjfIs3KBAsF8OUT5h1GXLSkoQdrV0r31OQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/react": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/react/-/react-1.4.0.tgz",
|
||||
"integrity": "sha512-xzPeZ753fQ0deZzdgY/0YavZvNpmdaxUzLYJYu5XjONNcZ8PwJnNLEK+7D/Cj8UM4Q8nWI7QC5mjum0uLWa4FA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8.0",
|
||||
"posthog-js": ">=1.257.2",
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/breadcrumbs": {
|
||||
"version": "3.5.28",
|
||||
@ -8183,7 +8204,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@ -8198,7 +8218,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@ -11403,7 +11422,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
@ -14073,7 +14091,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -14264,27 +14281,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.268.8",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.8.tgz",
|
||||
"integrity": "sha512-BJiKK4MlUvs7ybnQcy1KkwAz+SZkE/wRLotetIoank5kbqZs8FLbeyozFvmmgx4aoMmaVymYBSmYphYjYQeidw==",
|
||||
"version": "1.290.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.290.0.tgz",
|
||||
"integrity": "sha512-zavBwZkf+3JeiSDVE7ZDXBfzva/iOljicdhdJH+cZoqp0LsxjKxjnNhGOd3KpAhw0wqdwjhd7Lp1aJuI7DXyaw==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.2.2",
|
||||
"@posthog/core": "1.5.2",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rrweb/types": "2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "2.0.0-alpha.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rrweb/types": {
|
||||
"optional": true
|
||||
},
|
||||
"rrweb-snapshot": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js/node_modules/web-vitals": {
|
||||
@ -15547,7 +15553,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@ -15560,7 +15565,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.61.0",
|
||||
"version": "0.62.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@ -11,6 +11,7 @@
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@posthog/react": "^1.4.0",
|
||||
"@react-router/node": "^7.9.3",
|
||||
"@react-router/serve": "^7.9.3",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
@ -37,7 +38,7 @@
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"posthog-js": "^1.268.8",
|
||||
"posthog-js": "^1.290.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import {
|
||||
BaseModalTitle,
|
||||
BaseModalDescription,
|
||||
@ -17,6 +18,7 @@ interface AnalyticsConsentFormModalProps {
|
||||
export function AnalyticsConsentFormModal({
|
||||
onClose,
|
||||
}: AnalyticsConsentFormModalProps) {
|
||||
const posthog = usePostHog();
|
||||
const { t } = useTranslation();
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
|
||||
@ -29,7 +31,7 @@ export function AnalyticsConsentFormModal({
|
||||
{ user_consents_to_analytics: analytics },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(analytics);
|
||||
handleCaptureConsent(posthog, analytics);
|
||||
onClose();
|
||||
},
|
||||
},
|
||||
|
||||
109
frontend/src/components/features/chat/change-agent-button.tsx
Normal file
109
frontend/src/components/features/chat/change-agent-button.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React, { useMemo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import CodeTagIcon from "#/icons/code-tag.svg?react";
|
||||
import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { ChangeAgentContextMenu } from "./change-agent-context-menu";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
export function ChangeAgentButton() {
|
||||
const { t } = useTranslation();
|
||||
const [contextMenuOpen, setContextMenuOpen] = React.useState(false);
|
||||
|
||||
const conversationMode = useConversationStore(
|
||||
(state) => state.conversationMode,
|
||||
);
|
||||
|
||||
const setConversationMode = useConversationStore(
|
||||
(state) => state.setConversationMode,
|
||||
);
|
||||
|
||||
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
|
||||
|
||||
const { curAgentState } = useAgentState();
|
||||
|
||||
const isAgentRunning = curAgentState === AgentState.RUNNING;
|
||||
|
||||
// Close context menu when agent starts running
|
||||
useEffect(() => {
|
||||
if (isAgentRunning && contextMenuOpen) {
|
||||
setContextMenuOpen(false);
|
||||
}
|
||||
}, [isAgentRunning, contextMenuOpen]);
|
||||
|
||||
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenuOpen(!contextMenuOpen);
|
||||
};
|
||||
|
||||
const handleCodeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setConversationMode("code");
|
||||
};
|
||||
|
||||
const handlePlanClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setConversationMode("plan");
|
||||
};
|
||||
|
||||
const isExecutionAgent = conversationMode === "code";
|
||||
|
||||
const buttonLabel = useMemo(() => {
|
||||
if (isExecutionAgent) {
|
||||
return t(I18nKey.COMMON$CODE);
|
||||
}
|
||||
return t(I18nKey.COMMON$PLAN);
|
||||
}, [isExecutionAgent, t]);
|
||||
|
||||
const buttonIcon = useMemo(() => {
|
||||
if (isExecutionAgent) {
|
||||
return <CodeTagIcon width={18} height={18} color="#737373" />;
|
||||
}
|
||||
return <LessonPlanIcon width={18} height={18} color="#ffffff" />;
|
||||
}, [isExecutionAgent]);
|
||||
|
||||
if (!shouldUsePlanningAgent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleButtonClick}
|
||||
disabled={isAgentRunning}
|
||||
className={cn(
|
||||
"flex items-center border border-[#4B505F] rounded-[100px] transition-opacity",
|
||||
!isExecutionAgent && "border-[#597FF4] bg-[#4A67BD]",
|
||||
isAgentRunning
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "cursor-pointer hover:opacity-80",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1 pl-1.5">
|
||||
{buttonIcon}
|
||||
<Typography.Text className="text-white text-2.75 not-italic font-normal leading-5">
|
||||
{buttonLabel}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<ChevronDownSmallIcon width={24} height={24} color="#ffffff" />
|
||||
</button>
|
||||
{contextMenuOpen && (
|
||||
<ChangeAgentContextMenu
|
||||
onClose={() => setContextMenuOpen(false)}
|
||||
onCodeClick={handleCodeClick}
|
||||
onPlanClick={handlePlanClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import CodeTagIcon from "#/icons/code-tag.svg?react";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ContextMenuIconText } from "../context-menu/context-menu-icon-text";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||
|
||||
const contextMenuListItemClassName = cn(
|
||||
"cursor-pointer p-0 h-auto hover:bg-transparent",
|
||||
CONTEXT_MENU_ICON_TEXT_CLASSNAME,
|
||||
);
|
||||
|
||||
const contextMenuIconTextClassName =
|
||||
"gap-2 p-2 hover:bg-[#5C5D62] rounded h-[30px]";
|
||||
|
||||
interface ChangeAgentContextMenuProps {
|
||||
onClose: () => void;
|
||||
onCodeClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onPlanClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export function ChangeAgentContextMenu({
|
||||
onClose,
|
||||
onCodeClick,
|
||||
onPlanClick,
|
||||
}: ChangeAgentContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const menuRef = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
|
||||
const handleCodeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCodeClick?.(event);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handlePlanClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onPlanClick?.(event);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
ref={menuRef}
|
||||
testId="change-agent-context-menu"
|
||||
position="top"
|
||||
alignment="left"
|
||||
className="min-h-fit min-w-[195px] mb-2"
|
||||
>
|
||||
<ContextMenuListItem
|
||||
testId="code-option"
|
||||
onClick={handleCodeClick}
|
||||
className={contextMenuListItemClassName}
|
||||
>
|
||||
<ContextMenuIconText
|
||||
icon={CodeTagIcon}
|
||||
text={t(I18nKey.COMMON$CODE)}
|
||||
className={contextMenuIconTextClassName}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
<ContextMenuListItem
|
||||
testId="plan-option"
|
||||
onClick={handlePlanClick}
|
||||
className={contextMenuListItemClassName}
|
||||
>
|
||||
<ContextMenuIconText
|
||||
icon={LessonPlanIcon}
|
||||
text={t(I18nKey.COMMON$PLAN)}
|
||||
className={contextMenuIconTextClassName}
|
||||
/>
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useParams } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
@ -60,6 +60,7 @@ function getEntryPoint(
|
||||
}
|
||||
|
||||
export function ChatInterface() {
|
||||
const posthog = usePostHog();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { errorMessage } = useErrorMessageStore();
|
||||
|
||||
@ -8,6 +8,7 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useV1PauseConversation } from "#/hooks/mutation/use-v1-pause-conversation";
|
||||
import { useV1ResumeConversation } from "#/hooks/mutation/use-v1-resume-conversation";
|
||||
import { ChangeAgentButton } from "../change-agent-button";
|
||||
|
||||
interface ChatInputActionsProps {
|
||||
disabled: boolean;
|
||||
@ -56,7 +57,10 @@ export function ChatInputActions({
|
||||
return (
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tools />
|
||||
<div className="flex items-center gap-4">
|
||||
<Tools />
|
||||
<ChangeAgentButton />
|
||||
</div>
|
||||
</div>
|
||||
<AgentStatus
|
||||
className="ml-2 md:ml-3"
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
interface ChatInputFieldProps {
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>;
|
||||
@ -20,6 +22,12 @@ export function ChatInputField({
|
||||
}: ChatInputFieldProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const conversationMode = useConversationStore(
|
||||
(state) => state.conversationMode,
|
||||
);
|
||||
|
||||
const isPlanMode = conversationMode === "plan";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="box-border content-stretch flex flex-row items-center justify-start min-h-6 p-0 relative shrink-0 flex-1"
|
||||
@ -30,7 +38,11 @@ export function ChatInputField({
|
||||
ref={chatInputRef}
|
||||
className="chat-input bg-transparent text-white text-[16px] font-normal leading-[20px] outline-none resize-none custom-scrollbar min-h-[20px] max-h-[400px] [text-overflow:inherit] [text-wrap-mode:inherit] [white-space-collapse:inherit] block whitespace-pre-wrap"
|
||||
contentEditable
|
||||
data-placeholder={t("SUGGESTIONS$WHAT_TO_BUILD")}
|
||||
data-placeholder={
|
||||
isPlanMode
|
||||
? t(I18nKey.COMMON$LET_S_WORK_ON_A_PLAN)
|
||||
: t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)
|
||||
}
|
||||
data-testid="chat-input"
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import PRIcon from "#/icons/u-pr.svg?react";
|
||||
import { cn, getCreatePRPrompt } from "#/utils/utils";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface GitControlBarPrButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@ -20,6 +20,7 @@ export function GitControlBarPrButton({
|
||||
isConversationReady = true,
|
||||
}: GitControlBarPrButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackCreatePrButtonClick } = useTracking();
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
@ -28,7 +29,7 @@ export function GitControlBarPrButton({
|
||||
providersAreSet && hasRepository && isConversationReady;
|
||||
|
||||
const handlePrClick = () => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
trackCreatePrButtonClick();
|
||||
onSuggestionsClick(getCreatePRPrompt(currentGitProvider));
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
import { cn, getGitPullPrompt } from "#/utils/utils";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface GitControlBarPullButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@ -16,6 +16,7 @@ export function GitControlBarPullButton({
|
||||
isConversationReady = true,
|
||||
}: GitControlBarPullButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackPullButtonClick } = useTracking();
|
||||
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { providers } = useUserProviders();
|
||||
@ -26,7 +27,7 @@ export function GitControlBarPullButton({
|
||||
providersAreSet && hasRepository && isConversationReady;
|
||||
|
||||
const handlePullClick = () => {
|
||||
posthog.capture("pull_button_clicked");
|
||||
trackPullButtonClick();
|
||||
onSuggestionsClick(getGitPullPrompt());
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import { cn, getGitPushPrompt } from "#/utils/utils";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface GitControlBarPushButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@ -20,6 +20,7 @@ export function GitControlBarPushButton({
|
||||
isConversationReady = true,
|
||||
}: GitControlBarPushButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackPushButtonClick } = useTracking();
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
@ -28,7 +29,7 @@ export function GitControlBarPushButton({
|
||||
providersAreSet && hasRepository && isConversationReady;
|
||||
|
||||
const handlePushClick = () => {
|
||||
posthog.capture("push_button_clicked");
|
||||
trackPushButtonClick();
|
||||
onSuggestionsClick(getGitPushPrompt(currentGitProvider));
|
||||
};
|
||||
|
||||
|
||||
82
frontend/src/components/features/chat/plan-preview.tsx
Normal file
82
frontend/src/components/features/chat/plan-preview.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface PlanPreviewProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
onViewClick?: () => void;
|
||||
onBuildClick?: () => void;
|
||||
}
|
||||
|
||||
// TODO: Remove the hardcoded values and use the plan content from the conversation store
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
export function PlanPreview({
|
||||
title = "Improve Developer Onboarding and Examples",
|
||||
description = "Based on the analysis of Browser-Use's current documentation and examples, this plan addresses gaps in developer onboarding by creating a progressive learning path, troubleshooting resources, and practical examples that address real-world scenarios (like the LM Studio/local LLM integration issues encountered...",
|
||||
onViewClick,
|
||||
onBuildClick,
|
||||
}: PlanPreviewProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
|
||||
|
||||
if (!shouldUsePlanningAgent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[#25272d] border border-[#597FF4] rounded-[12px] w-full mb-4 mt-2">
|
||||
{/* Header */}
|
||||
<div className="border-b border-[#525252] flex h-[41px] items-center px-2 gap-1">
|
||||
<LessonPlanIcon width={18} height={18} color="#9299aa" />
|
||||
<Typography.Text className="font-medium text-[11px] text-white tracking-[0.11px] leading-4">
|
||||
{t(I18nKey.COMMON$PLAN_MD)}
|
||||
</Typography.Text>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={onViewClick}
|
||||
className="flex items-center gap-1 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Typography.Text className="font-medium text-[11px] text-white tracking-[0.11px] leading-4">
|
||||
{t(I18nKey.COMMON$VIEW)}
|
||||
</Typography.Text>
|
||||
<ArrowUpRight className="text-white" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col gap-[10px] p-4">
|
||||
<h3 className="font-bold text-[19px] text-white leading-[29px]">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-[15px] text-white leading-[29px]">
|
||||
{description}
|
||||
<Typography.Text className="text-[#4a67bd] cursor-pointer hover:underline ml-1">
|
||||
{t(I18nKey.COMMON$READ_MORE)}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-[#525252] flex h-[54px] items-center justify-start px-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBuildClick}
|
||||
className="bg-white flex items-center justify-center h-[26px] px-2 rounded-[4px] w-[93px] hover:opacity-90 transition-opacity cursor-pointer"
|
||||
>
|
||||
<Typography.Text className="font-medium text-[14px] text-black leading-5">
|
||||
{t(I18nKey.COMMON$BUILD)}{" "}
|
||||
<Typography.Text className="font-medium text-black">
|
||||
⌘↩
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import LogOutIcon from "#/icons/log-out.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
|
||||
|
||||
interface AccountSettingsContextMenuProps {
|
||||
@ -58,6 +59,21 @@ export function AccountSettingsContextMenu({
|
||||
|
||||
<Divider />
|
||||
|
||||
<a
|
||||
href="https://docs.openhands.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<ContextMenuListItem
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2 p-2 hover:bg-[#5C5D62] rounded h-[30px]"
|
||||
>
|
||||
<DocumentIcon width={16} height={16} />
|
||||
<span className="text-white text-sm">{t(I18nKey.SIDEBAR$DOCS)}</span>
|
||||
</ContextMenuListItem>
|
||||
</a>
|
||||
|
||||
<ContextMenuListItem
|
||||
onClick={onLogout}
|
||||
className="flex items-center gap-2 p-2 hover:bg-[#5C5D62] rounded h-[30px]"
|
||||
|
||||
@ -13,6 +13,7 @@ import { useConversationStore } from "#/state/conversation-store";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useUnifiedWebSocketStatus } from "#/hooks/use-unified-websocket-status";
|
||||
import { useTaskPolling } from "#/hooks/query/use-task-polling";
|
||||
|
||||
export interface AgentStatusProps {
|
||||
className?: string;
|
||||
@ -35,6 +36,7 @@ export function AgentStatus({
|
||||
const { curStatusMessage } = useStatusStore();
|
||||
const webSocketStatus = useUnifiedWebSocketStatus();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { taskStatus } = useTaskPolling();
|
||||
|
||||
const statusCode = getStatusCode(
|
||||
curStatusMessage,
|
||||
@ -42,17 +44,24 @@ export function AgentStatus({
|
||||
conversation?.status || null,
|
||||
conversation?.runtime_status || null,
|
||||
curAgentState,
|
||||
taskStatus,
|
||||
);
|
||||
|
||||
const isTaskLoading =
|
||||
taskStatus && taskStatus !== "ERROR" && taskStatus !== "READY";
|
||||
|
||||
const shouldShownAgentLoading =
|
||||
isPausing ||
|
||||
curAgentState === AgentState.INIT ||
|
||||
curAgentState === AgentState.LOADING ||
|
||||
webSocketStatus === "CONNECTING";
|
||||
(webSocketStatus === "CONNECTING" && taskStatus !== "ERROR") ||
|
||||
isTaskLoading;
|
||||
|
||||
const shouldShownAgentError =
|
||||
curAgentState === AgentState.ERROR ||
|
||||
curAgentState === AgentState.RATE_LIMITED;
|
||||
curAgentState === AgentState.RATE_LIMITED ||
|
||||
webSocketStatus === "DISCONNECTED" ||
|
||||
taskStatus === "ERROR";
|
||||
|
||||
const shouldShownAgentStop = curAgentState === AgentState.RUNNING;
|
||||
|
||||
@ -61,7 +70,7 @@ export function AgentStatus({
|
||||
|
||||
// Update global state when agent loading condition changes
|
||||
useEffect(() => {
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
setShouldShownAgentLoading(!!shouldShownAgentLoading);
|
||||
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
@ -44,6 +44,7 @@ export function ConversationCard({
|
||||
contextMenuOpen = false,
|
||||
onContextMenuToggle,
|
||||
}: ConversationCardProps) {
|
||||
const posthog = usePostHog();
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
|
||||
const onTitleSave = (newTitle: string) => {
|
||||
|
||||
80
frontend/src/components/features/markdown/headings.tsx
Normal file
80
frontend/src/components/features/markdown/headings.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import { ExtraProps } from "react-markdown";
|
||||
|
||||
// Custom component to render <h1> in markdown
|
||||
export function h1({
|
||||
children,
|
||||
}: React.ClassAttributes<HTMLHeadingElement> &
|
||||
React.HTMLAttributes<HTMLHeadingElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<h1 className="text-[32px] text-white font-bold leading-8 mb-4 mt-6 first:mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom component to render <h2> in markdown
|
||||
export function h2({
|
||||
children,
|
||||
}: React.ClassAttributes<HTMLHeadingElement> &
|
||||
React.HTMLAttributes<HTMLHeadingElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<h2 className="text-xl font-semibold leading-6 -tracking-[0.02em] text-white mb-3 mt-5 first:mt-0">
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom component to render <h3> in markdown
|
||||
export function h3({
|
||||
children,
|
||||
}: React.ClassAttributes<HTMLHeadingElement> &
|
||||
React.HTMLAttributes<HTMLHeadingElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<h3 className="text-lg font-semibold text-white mb-2 mt-4 first:mt-0">
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom component to render <h4> in markdown
|
||||
export function h4({
|
||||
children,
|
||||
}: React.ClassAttributes<HTMLHeadingElement> &
|
||||
React.HTMLAttributes<HTMLHeadingElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<h4 className="text-base font-semibold text-white mb-2 mt-4 first:mt-0">
|
||||
{children}
|
||||
</h4>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom component to render <h5> in markdown
|
||||
export function h5({
|
||||
children,
|
||||
}: React.ClassAttributes<HTMLHeadingElement> &
|
||||
React.HTMLAttributes<HTMLHeadingElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<h5 className="text-sm font-semibold text-white mb-2 mt-3 first:mt-0">
|
||||
{children}
|
||||
</h5>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom component to render <h6> in markdown
|
||||
export function h6({
|
||||
children,
|
||||
}: React.ClassAttributes<HTMLHeadingElement> &
|
||||
React.HTMLAttributes<HTMLHeadingElement> &
|
||||
ExtraProps) {
|
||||
return (
|
||||
<h6 className="text-sm font-medium text-gray-300 mb-2 mt-3 first:mt-0">
|
||||
{children}
|
||||
</h6>
|
||||
);
|
||||
}
|
||||
@ -11,6 +11,7 @@ import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react";
|
||||
import { useAuthUrl } from "#/hooks/use-auth-url";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface AuthModalProps {
|
||||
githubAuthUrl: string | null;
|
||||
@ -26,6 +27,7 @@ export function AuthModal({
|
||||
providersConfigured,
|
||||
}: AuthModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackLoginButtonClick } = useTracking();
|
||||
|
||||
const gitlabAuthUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
@ -47,6 +49,7 @@ export function AuthModal({
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
trackLoginButtonClick({ provider: "github" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = githubAuthUrl;
|
||||
}
|
||||
@ -54,6 +57,7 @@ export function AuthModal({
|
||||
|
||||
const handleGitLabAuth = () => {
|
||||
if (gitlabAuthUrl) {
|
||||
trackLoginButtonClick({ provider: "gitlab" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = gitlabAuthUrl;
|
||||
}
|
||||
@ -61,6 +65,7 @@ export function AuthModal({
|
||||
|
||||
const handleBitbucketAuth = () => {
|
||||
if (bitbucketAuthUrl) {
|
||||
trackLoginButtonClick({ provider: "bitbucket" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = bitbucketAuthUrl;
|
||||
}
|
||||
@ -68,6 +73,7 @@ export function AuthModal({
|
||||
|
||||
const handleEnterpriseSsoAuth = () => {
|
||||
if (enterpriseSsoUrl) {
|
||||
trackLoginButtonClick({ provider: "enterprise_sso" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = enterpriseSsoUrl;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useLocation } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { organizeModelsAndProviders } from "#/utils/organize-models-and-providers";
|
||||
import { DangerModal } from "../confirmation-modals/danger-modal";
|
||||
@ -22,6 +22,7 @@ interface SettingsFormProps {
|
||||
}
|
||||
|
||||
export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
const posthog = usePostHog();
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
@ -49,6 +49,10 @@ const getExecuteBashObservationContent = (
|
||||
|
||||
let { output } = observation;
|
||||
|
||||
if (!output) {
|
||||
output = "";
|
||||
}
|
||||
|
||||
if (output.length > MAX_CONTENT_LENGTH) {
|
||||
output = `${output.slice(0, MAX_CONTENT_LENGTH)}...`;
|
||||
}
|
||||
@ -136,6 +140,7 @@ const getTaskTrackerObservationContent = (
|
||||
if (
|
||||
"content" in observation &&
|
||||
observation.content &&
|
||||
typeof observation.content === "string" &&
|
||||
observation.content.trim()
|
||||
) {
|
||||
content += `\n\n**Result:** ${observation.content.trim()}`;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import EventLogger from "#/utils/event-logger";
|
||||
import { handleAssistantMessage } from "#/services/actions";
|
||||
import { showChatError, trackError } from "#/utils/error-handler";
|
||||
@ -100,7 +101,10 @@ interface ErrorArgData {
|
||||
msg_id: string;
|
||||
}
|
||||
|
||||
export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) {
|
||||
export function updateStatusWhenErrorMessagePresent(
|
||||
data: ErrorArg | unknown,
|
||||
posthog?: ReturnType<typeof usePostHog>,
|
||||
) {
|
||||
const isObject = (val: unknown): val is object =>
|
||||
!!val && typeof val === "object";
|
||||
const isString = (val: unknown): val is string => typeof val === "string";
|
||||
@ -123,6 +127,7 @@ export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) {
|
||||
source: "websocket",
|
||||
metadata,
|
||||
msgId,
|
||||
posthog,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -131,6 +136,7 @@ export function WsClientProvider({
|
||||
conversationId,
|
||||
children,
|
||||
}: React.PropsWithChildren<WsClientProviderProps>) {
|
||||
const posthog = usePostHog();
|
||||
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
|
||||
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
const { addEvent, clearEvents } = useEventStore();
|
||||
@ -178,6 +184,7 @@ export function WsClientProvider({
|
||||
message: errorMessage,
|
||||
source: "chat",
|
||||
metadata: { msgId: event.id },
|
||||
posthog,
|
||||
});
|
||||
setErrorMessage(errorMessage);
|
||||
|
||||
@ -193,6 +200,7 @@ export function WsClientProvider({
|
||||
message: event.message,
|
||||
source: "chat",
|
||||
metadata: { msgId: event.id },
|
||||
posthog,
|
||||
});
|
||||
} else {
|
||||
removeErrorMessage();
|
||||
@ -260,14 +268,14 @@ export function WsClientProvider({
|
||||
sio.io.opts.query = sio.io.opts.query || {};
|
||||
sio.io.opts.query.latest_event_id = lastEventRef.current?.id;
|
||||
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
updateStatusWhenErrorMessagePresent(data, posthog);
|
||||
setErrorMessage(hasValidMessageProperty(data) ? data.message : "");
|
||||
}
|
||||
|
||||
function handleError(data: unknown) {
|
||||
// set status
|
||||
setWebSocketStatus("DISCONNECTED");
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
updateStatusWhenErrorMessagePresent(data, posthog);
|
||||
|
||||
setErrorMessage(
|
||||
hasValidMessageProperty(data)
|
||||
|
||||
@ -8,17 +8,18 @@
|
||||
import { HydratedRouter } from "react-router/dom";
|
||||
import React, { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import posthog from "posthog-js";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import "./i18n";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import OptionService from "./api/option-service/option-service.api";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
import { queryClient } from "./query-client-config";
|
||||
|
||||
function PosthogInit() {
|
||||
function PostHogWrapper({ children }: { children: React.ReactNode }) {
|
||||
const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
@ -27,20 +28,27 @@ function PosthogInit() {
|
||||
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
|
||||
} catch {
|
||||
displayErrorToast("Error fetching PostHog client key");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (posthogClientKey) {
|
||||
posthog.init(posthogClientKey, {
|
||||
if (isLoading || !posthogClientKey) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostHogProvider
|
||||
apiKey={posthogClientKey}
|
||||
options={{
|
||||
api_host: "https://us.i.posthog.com",
|
||||
person_profiles: "identified_only",
|
||||
});
|
||||
}
|
||||
}, [posthogClientKey]);
|
||||
|
||||
return null;
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PostHogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
async function prepareApp() {
|
||||
@ -62,10 +70,10 @@ prepareApp().then(() =>
|
||||
document,
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HydratedRouter />
|
||||
<PosthogInit />
|
||||
<PostHogWrapper>
|
||||
<HydratedRouter />
|
||||
</PostHogWrapper>
|
||||
</QueryClientProvider>
|
||||
<div id="modal-portal-exit" />
|
||||
</StrictMode>,
|
||||
);
|
||||
}),
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
import { Provider, ProviderToken } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
export const useAddGitProviders = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { trackGitProviderConnected } = useTracking();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
@ -11,7 +13,18 @@ export const useAddGitProviders = () => {
|
||||
}: {
|
||||
providers: Record<Provider, ProviderToken>;
|
||||
}) => SecretsService.addGitProvider(providers),
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (_, { providers }) => {
|
||||
// Track which providers were connected (filter out empty tokens)
|
||||
const connectedProviders = Object.entries(providers)
|
||||
.filter(([, value]) => value.token && value.token.trim() !== "")
|
||||
.map(([key]) => key);
|
||||
|
||||
if (connectedProviders.length > 0) {
|
||||
trackGitProviderConnected({
|
||||
providers: connectedProviders,
|
||||
});
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
},
|
||||
meta: {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
import { SuggestedTask } from "#/utils/types";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { CreateMicroagent, Conversation } from "#/api/open-hands.types";
|
||||
import { USE_V1_CONVERSATION_API } from "#/utils/feature-flags";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface CreateConversationVariables {
|
||||
query?: string;
|
||||
@ -31,6 +31,7 @@ interface CreateConversationResponse extends Partial<Conversation> {
|
||||
|
||||
export const useCreateConversation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { trackConversationCreated } = useTracking();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["create-conversation"],
|
||||
@ -86,12 +87,11 @@ export const useCreateConversation = () => {
|
||||
is_v1: false,
|
||||
};
|
||||
},
|
||||
onSuccess: async (_, { query, repository }) => {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
entry_point: "task_form",
|
||||
query_character_length: query?.length,
|
||||
has_repository: !!repository,
|
||||
onSuccess: async (_, { repository }) => {
|
||||
trackConversationCreated({
|
||||
hasRepository: !!repository,
|
||||
});
|
||||
|
||||
queryClient.removeQueries({
|
||||
queryKey: ["user", "conversations"],
|
||||
});
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import AuthService from "#/api/auth-service/auth-service.api";
|
||||
import { useConfig } from "../query/use-config";
|
||||
import { clearLoginData } from "#/utils/local-storage";
|
||||
|
||||
export const useLogout = () => {
|
||||
const posthog = usePostHog();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { PostSettings } from "#/types/settings";
|
||||
@ -41,6 +41,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
};
|
||||
|
||||
export const useSaveSettings = () => {
|
||||
const posthog = usePostHog();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: currentSettings } = useSettings();
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import ConversationService from "#/api/conversation-service/conversation-service
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
|
||||
export interface BatchFeedbackData {
|
||||
exists: boolean;
|
||||
@ -25,13 +26,20 @@ export const getFeedbackExistsQueryKey = (
|
||||
export const useBatchFeedback = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const queryClient = useQueryClient();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: getFeedbackQueryKey(conversationId),
|
||||
queryFn: () => ConversationService.getBatchFeedback(conversationId!),
|
||||
enabled: runtimeIsReady && !!conversationId && config?.APP_MODE === "saas",
|
||||
enabled:
|
||||
runtimeIsReady &&
|
||||
!!conversationId &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
!isV1Conversation,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { BatchFeedbackData, getFeedbackQueryKey } from "./use-batch-feedback";
|
||||
|
||||
export type FeedbackData = BatchFeedbackData;
|
||||
@ -9,6 +10,9 @@ export const useFeedbackExists = (eventId?: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
return useQuery<FeedbackData>({
|
||||
queryKey: [...getFeedbackQueryKey(conversationId), eventId],
|
||||
@ -22,7 +26,11 @@ export const useFeedbackExists = (eventId?: number) => {
|
||||
|
||||
return batchData?.[eventId.toString()] ?? { exists: false };
|
||||
},
|
||||
enabled: !!eventId && !!conversationId && config?.APP_MODE === "saas",
|
||||
enabled:
|
||||
!!eventId &&
|
||||
!!conversationId &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
!isV1Conversation,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useConfig } from "./use-config";
|
||||
import UserService from "#/api/user-service/user-service.api";
|
||||
import { useShouldShowUserFeatures } from "#/hooks/use-should-show-user-features";
|
||||
|
||||
export const useGitUser = () => {
|
||||
const posthog = usePostHog();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
// Use the shared hook to determine if we should fetch user data
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||
@ -61,12 +59,6 @@ export const useSettings = () => {
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.isFetched && query.data?.LLM_API_KEY_SET) {
|
||||
posthog.capture("user_activated");
|
||||
}
|
||||
}, [query.data?.LLM_API_KEY_SET, query.isFetched]);
|
||||
|
||||
// We want to return the defaults if the settings aren't found so the user can still see the
|
||||
// options to make their initial save. We don't set the defaults in `initialData` above because
|
||||
// that would prepopulate the data to the cache and mess with expectations. Read more:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
|
||||
import useMetricsStore from "#/stores/metrics-store";
|
||||
@ -29,6 +29,7 @@ export function useConversationNameContextMenu({
|
||||
showOptions = false,
|
||||
onContextMenuToggle,
|
||||
}: UseConversationNameContextMenuProps) {
|
||||
const posthog = usePostHog();
|
||||
const { t } = useTranslation();
|
||||
const { conversationId: currentConversationId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import React from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { useSaveSettings } from "./mutation/use-save-settings";
|
||||
|
||||
export const useMigrateUserConsent = () => {
|
||||
const posthog = usePostHog();
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
|
||||
/**
|
||||
@ -15,11 +17,11 @@ export const useMigrateUserConsent = () => {
|
||||
if (userAnalyticsConsent) {
|
||||
args?.handleAnalyticsWasPresentInLocalStorage();
|
||||
|
||||
await saveUserSettings(
|
||||
saveUserSettings(
|
||||
{ user_consents_to_analytics: userAnalyticsConsent === "true" },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(userAnalyticsConsent === "true");
|
||||
handleCaptureConsent(posthog, userAnalyticsConsent === "true");
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -27,7 +29,7 @@ export const useMigrateUserConsent = () => {
|
||||
localStorage.removeItem("analytics-consent");
|
||||
}
|
||||
},
|
||||
[],
|
||||
[posthog, saveUserSettings],
|
||||
);
|
||||
|
||||
return { migrateUserConsent };
|
||||
|
||||
@ -22,7 +22,7 @@ const renderCommand = (
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedContent = content.replaceAll("\n", "\r\n").trim();
|
||||
const trimmedContent = (content || "").replaceAll("\n", "\r\n").trim();
|
||||
// Only write if there's actual content to avoid empty newlines
|
||||
if (trimmedContent) {
|
||||
terminal.writeln(parseTerminalOutput(trimmedContent));
|
||||
|
||||
78
frontend/src/hooks/use-tracking.ts
Normal file
78
frontend/src/hooks/use-tracking.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useConfig } from "./query/use-config";
|
||||
import { useSettings } from "./query/use-settings";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
/**
|
||||
* Hook that provides tracking functions with automatic data collection
|
||||
* from available hooks (config, settings, etc.)
|
||||
*/
|
||||
export const useTracking = () => {
|
||||
const posthog = usePostHog();
|
||||
const { data: config } = useConfig();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
// Common properties included in all tracking events
|
||||
const commonProperties = {
|
||||
app_surface: config?.APP_MODE || "unknown",
|
||||
plan_tier: null,
|
||||
current_url: window.location.href,
|
||||
user_email: settings?.EMAIL || settings?.GIT_USER_EMAIL || null,
|
||||
};
|
||||
|
||||
const trackLoginButtonClick = ({ provider }: { provider: Provider }) => {
|
||||
posthog.capture("login_button_clicked", {
|
||||
provider,
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackConversationCreated = ({
|
||||
hasRepository,
|
||||
}: {
|
||||
hasRepository: boolean;
|
||||
}) => {
|
||||
posthog.capture("conversation_created", {
|
||||
has_repository: hasRepository,
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackPushButtonClick = () => {
|
||||
posthog.capture("push_button_clicked", {
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackPullButtonClick = () => {
|
||||
posthog.capture("pull_button_clicked", {
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackCreatePrButtonClick = () => {
|
||||
posthog.capture("create_pr_button_clicked", {
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackGitProviderConnected = ({
|
||||
providers,
|
||||
}: {
|
||||
providers: string[];
|
||||
}) => {
|
||||
posthog.capture("git_provider_connected", {
|
||||
providers,
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
trackLoginButtonClick,
|
||||
trackConversationCreated,
|
||||
trackPushButtonClick,
|
||||
trackPullButtonClick,
|
||||
trackCreatePrButtonClick,
|
||||
trackGitProviderConnected,
|
||||
};
|
||||
};
|
||||
@ -476,7 +476,10 @@ export enum I18nKey {
|
||||
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
|
||||
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
|
||||
STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION = "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION",
|
||||
STATUS$ERROR = "STATUS$ERROR",
|
||||
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
|
||||
STATUS$ERROR_MEMORY = "STATUS$ERROR_MEMORY",
|
||||
STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR = "STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR",
|
||||
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
|
||||
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
|
||||
AGENT_ERROR$ACTION_TIMEOUT = "AGENT_ERROR$ACTION_TIMEOUT",
|
||||
@ -934,4 +937,10 @@ export enum I18nKey {
|
||||
AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION = "AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION",
|
||||
COMMON$MORE_OPTIONS = "COMMON$MORE_OPTIONS",
|
||||
COMMON$CREATE_A_PLAN = "COMMON$CREATE_A_PLAN",
|
||||
COMMON$PLAN_MD = "COMMON$PLAN_MD",
|
||||
COMMON$READ_MORE = "COMMON$READ_MORE",
|
||||
COMMON$BUILD = "COMMON$BUILD",
|
||||
COMMON$ASK = "COMMON$ASK",
|
||||
COMMON$PLAN = "COMMON$PLAN",
|
||||
COMMON$LET_S_WORK_ON_A_PLAN = "COMMON$LET_S_WORK_ON_A_PLAN",
|
||||
}
|
||||
|
||||
@ -7615,6 +7615,22 @@
|
||||
"tr": "İçerik politikası ihlali. Çıktı, içerik filtreleme politikası tarafından engellendi.",
|
||||
"uk": "Порушення політики щодо вмісту. Вивід було заблоковано політикою фільтрації вмісту."
|
||||
},
|
||||
"STATUS$ERROR": {
|
||||
"en": "An error occurred. Please try again.",
|
||||
"zh-CN": "发生错误,请重试",
|
||||
"zh-TW": "發生錯誤,請重試",
|
||||
"ko-KR": "오류가 발생했습니다. 다시 시도해주세요.",
|
||||
"ja": "エラーが発生しました。もう一度お試しください。",
|
||||
"no": "Det oppstod en feil. Vennligst prøv igjen.",
|
||||
"ar": "حدث خطأ. يرجى المحاولة مرة أخرى.",
|
||||
"de": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"fr": "Une erreur s'est produite. Veuillez réessayer.",
|
||||
"it": "Si è verificato un errore. Per favore, riprova.",
|
||||
"pt": "Ocorreu um erro. Por favor, tente novamente.",
|
||||
"es": "Ocurrió un error. Por favor, inténtalo de nuevo.",
|
||||
"tr": "Bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
"uk": "Сталася помилка. Будь ласка, спробуйте ще раз."
|
||||
},
|
||||
"STATUS$ERROR_RUNTIME_DISCONNECTED": {
|
||||
"en": "There was an error while connecting to the runtime. Please refresh the page.",
|
||||
"zh-CN": "运行时已断开连接",
|
||||
@ -7631,6 +7647,38 @@
|
||||
"tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin.",
|
||||
"uk": "Під час підключення до середовища виконання сталася помилка. Оновіть сторінку."
|
||||
},
|
||||
"STATUS$ERROR_MEMORY": {
|
||||
"en": "Memory error occurred. Please try reducing the workload or restarting.",
|
||||
"zh-CN": "发生内存错误,请尝试减少工作负载或重新启动",
|
||||
"zh-TW": "發生記憶體錯誤,請嘗試減少工作負載或重新啟動",
|
||||
"ko-KR": "메모리 오류가 발생했습니다. 작업 부하를 줄이거나 다시 시작해주세요.",
|
||||
"ja": "メモリエラーが発生しました。作業負荷を減らすか、再起動してください。",
|
||||
"no": "Minnefeil oppstod. Vennligst prøv å redusere arbeidsmengden eller start på nytt.",
|
||||
"ar": "حدث خطأ في الذاكرة. يرجى محاولة تقليل عبء العمل أو إعادة التشغيل.",
|
||||
"de": "Speicherfehler aufgetreten. Bitte versuchen Sie, die Arbeitslast zu reduzieren oder neu zu starten.",
|
||||
"fr": "Erreur de mémoire. Veuillez essayer de réduire la charge de travail ou de redémarrer.",
|
||||
"it": "Si è verificato un errore di memoria. Prova a ridurre il carico di lavoro o a riavviare.",
|
||||
"pt": "Ocorreu um erro de memória. Tente reduzir a carga de trabalho ou reiniciar.",
|
||||
"es": "Ocurrió un error de memoria. Intenta reducir la carga de trabajo o reiniciar.",
|
||||
"tr": "Bellek hatası oluştu. Lütfen iş yükünü azaltmayı veya yeniden başlatmayı deneyin.",
|
||||
"uk": "Сталася помилка пам'яті. Спробуйте зменшити навантаження або перезапустити."
|
||||
},
|
||||
"STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR": {
|
||||
"en": "Error authenticating with the Git provider. Please check your credentials.",
|
||||
"zh-CN": "Git提供商认证错误,请检查您的凭据",
|
||||
"zh-TW": "Git提供商認證錯誤,請檢查您的憑據",
|
||||
"ko-KR": "Git 공급자 인증 오류. 자격 증명을 확인해주세요.",
|
||||
"ja": "Git プロバイダーの認証エラー。認証情報を確認してください。",
|
||||
"no": "Feil ved autentisering med Git-leverandøren. Vennligst sjekk dine legitimasjoner.",
|
||||
"ar": "خطأ في المصادقة مع مزود Git. يرجى التحقق من بيانات الاعتماد الخاصة بك.",
|
||||
"de": "Fehler bei der Authentifizierung mit dem Git-Anbieter. Bitte überprüfen Sie Ihre Anmeldedaten.",
|
||||
"fr": "Erreur d'authentification auprès du fournisseur Git. Veuillez vérifier vos informations d'identification.",
|
||||
"it": "Errore di autenticazione con il provider Git. Controlla le tue credenziali.",
|
||||
"pt": "Erro ao autenticar com o provedor Git. Por favor, verifique suas credenciais.",
|
||||
"es": "Error al autenticar con el proveedor Git. Por favor, verifica tus credenciales.",
|
||||
"tr": "Git sağlayıcısı ile kimlik doğrulama hatası. Lütfen kimlik bilgilerinizi kontrol edin.",
|
||||
"uk": "Помилка автентифікації у постачальника Git. Перевірте свої облікові дані."
|
||||
},
|
||||
"STATUS$LLM_RETRY": {
|
||||
"en": "Retrying LLM request",
|
||||
"es": "Reintentando solicitud LLM",
|
||||
@ -14942,5 +14990,101 @@
|
||||
"tr": "Bir plan oluştur",
|
||||
"de": "Einen Plan erstellen",
|
||||
"uk": "Створити план"
|
||||
},
|
||||
"COMMON$PLAN_MD": {
|
||||
"en": "Plan.md",
|
||||
"ja": "Plan.md",
|
||||
"zh-CN": "Plan.md",
|
||||
"zh-TW": "Plan.md",
|
||||
"ko-KR": "Plan.md",
|
||||
"no": "Plan.md",
|
||||
"it": "Plan.md",
|
||||
"pt": "Plan.md",
|
||||
"es": "Plan.md",
|
||||
"ar": "Plan.md",
|
||||
"fr": "Plan.md",
|
||||
"tr": "Plan.md",
|
||||
"de": "Plan.md",
|
||||
"uk": "Plan.md"
|
||||
},
|
||||
"COMMON$READ_MORE": {
|
||||
"en": "Read more",
|
||||
"ja": "続きを読む",
|
||||
"zh-CN": "阅读更多",
|
||||
"zh-TW": "閱讀更多",
|
||||
"ko-KR": "더 읽기",
|
||||
"no": "Les mer",
|
||||
"it": "Leggi di più",
|
||||
"pt": "Leia mais",
|
||||
"es": "Leer más",
|
||||
"ar": "اقرأ المزيد",
|
||||
"fr": "En savoir plus",
|
||||
"tr": "Devamını oku",
|
||||
"de": "Mehr lesen",
|
||||
"uk": "Читати далі"
|
||||
},
|
||||
"COMMON$BUILD": {
|
||||
"en": "Build",
|
||||
"ja": "ビルド",
|
||||
"zh-CN": "构建",
|
||||
"zh-TW": "建構",
|
||||
"ko-KR": "빌드",
|
||||
"no": "Bygg",
|
||||
"it": "Compila",
|
||||
"pt": "Construir",
|
||||
"es": "Compilar",
|
||||
"ar": "بناء",
|
||||
"fr": "Construire",
|
||||
"tr": "Derle",
|
||||
"de": "Erstellen",
|
||||
"uk": "Зібрати"
|
||||
},
|
||||
"COMMON$ASK": {
|
||||
"en": "Ask",
|
||||
"ja": "質問する",
|
||||
"zh-CN": "提问",
|
||||
"zh-TW": "詢問",
|
||||
"ko-KR": "질문",
|
||||
"no": "Spør",
|
||||
"it": "Chiedi",
|
||||
"pt": "Perguntar",
|
||||
"es": "Preguntar",
|
||||
"ar": "اسأل",
|
||||
"fr": "Demander",
|
||||
"tr": "Sor",
|
||||
"de": "Fragen",
|
||||
"uk": "Запитати"
|
||||
},
|
||||
"COMMON$PLAN": {
|
||||
"en": "Plan",
|
||||
"ja": "計画",
|
||||
"zh-CN": "计划",
|
||||
"zh-TW": "計劃",
|
||||
"ko-KR": "계획",
|
||||
"no": "Plan",
|
||||
"it": "Piano",
|
||||
"pt": "Plano",
|
||||
"es": "Plan",
|
||||
"ar": "خطة",
|
||||
"fr": "Planifier",
|
||||
"tr": "Plan",
|
||||
"de": "Plan",
|
||||
"uk": "План"
|
||||
},
|
||||
"COMMON$LET_S_WORK_ON_A_PLAN": {
|
||||
"en": "Let’s work on a plan",
|
||||
"ja": "プランに取り組みましょう",
|
||||
"zh-CN": "让我们制定一个计划吧",
|
||||
"zh-TW": "讓我們來制定計劃吧",
|
||||
"ko-KR": "계획을 세워봅시다",
|
||||
"no": "La oss lage en plan",
|
||||
"it": "Lavoriamo su un piano",
|
||||
"pt": "Vamos trabalhar em um plano",
|
||||
"es": "Trabajemos en un plan",
|
||||
"ar": "لنضع خطة معًا",
|
||||
"fr": "Travaillons sur un plan",
|
||||
"tr": "Bir plan üzerinde çalışalım",
|
||||
"de": "Lassen Sie uns an einem Plan arbeiten",
|
||||
"uk": "Давайте розробимо план"
|
||||
}
|
||||
}
|
||||
|
||||
3
frontend/src/icons/code-tag.svg
Normal file
3
frontend/src/icons/code-tag.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M7.062 8.367L3.0915 12.336L7.062 16.305L6 17.367L1.5 12.867V11.805L6 7.305L7.062 8.367ZM17.562 7.305L16.5 8.367L20.4705 12.336L16.5 16.305L17.562 17.367L22.062 12.867V11.805L17.562 7.305ZM7.362 19.5L8.703 20.172L16.203 5.172L14.862 4.5L7.362 19.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@ -25,6 +25,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<Toaster />
|
||||
<div id="modal-portal-exit" />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -2,6 +2,7 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react";
|
||||
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
|
||||
@ -11,6 +12,7 @@ import { openHands } from "#/api/open-hands-axios";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
|
||||
export default function AcceptTOS() {
|
||||
const posthog = usePostHog();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
@ -23,7 +25,7 @@ export default function AcceptTOS() {
|
||||
const { mutate: acceptTOS, isPending: isSubmitting } = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Set consent for analytics
|
||||
handleCaptureConsent(true);
|
||||
handleCaptureConsent(posthog, true);
|
||||
|
||||
// Call the API to record TOS acceptance in the database
|
||||
return openHands.post("/api/accept_tos", {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
@ -20,6 +21,7 @@ import { useConfig } from "#/hooks/query/use-config";
|
||||
import { parseMaxBudgetPerTask } from "#/utils/settings-utils";
|
||||
|
||||
function AppSettingsScreen() {
|
||||
const posthog = usePostHog();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { mutate: saveSettings, isPending } = useSaveSettings();
|
||||
@ -93,7 +95,7 @@ function AppSettingsScreen() {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(enableAnalytics);
|
||||
handleCaptureConsent(posthog, enableAnalytics);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
},
|
||||
onError: (error) => {
|
||||
@ -125,7 +127,8 @@ function AppSettingsScreen() {
|
||||
};
|
||||
|
||||
const checkIfAnalyticsSwitchHasChanged = (checked: boolean) => {
|
||||
const currentAnalytics = !!settings?.USER_CONSENTS_TO_ANALYTICS;
|
||||
// Treat null as true since analytics is opt-in by default
|
||||
const currentAnalytics = settings?.USER_CONSENTS_TO_ANALYTICS ?? true;
|
||||
setAnalyticsSwitchHasChanged(checked !== currentAnalytics);
|
||||
};
|
||||
|
||||
@ -197,7 +200,7 @@ function AppSettingsScreen() {
|
||||
<SettingsSwitch
|
||||
testId="enable-analytics-switch"
|
||||
name="enable-analytics-switch"
|
||||
defaultIsToggled={!!settings.USER_CONSENTS_TO_ANALYTICS}
|
||||
defaultIsToggled={settings.USER_CONSENTS_TO_ANALYTICS ?? true}
|
||||
onToggle={checkIfAnalyticsSwitchHasChanged}
|
||||
>
|
||||
{t(I18nKey.ANALYTICS$SEND_ANONYMOUS_DATA)}
|
||||
|
||||
@ -1,13 +1,52 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { code } from "#/components/features/markdown/code";
|
||||
import { ul, ol } from "#/components/features/markdown/list";
|
||||
import { paragraph } from "#/components/features/markdown/paragraph";
|
||||
import { anchor } from "#/components/features/markdown/anchor";
|
||||
import {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
} from "#/components/features/markdown/headings";
|
||||
|
||||
function PlannerTab() {
|
||||
const { t } = useTranslation();
|
||||
const setConversationMode = useConversationStore(
|
||||
(state) => state.setConversationMode,
|
||||
);
|
||||
|
||||
const { planContent, setConversationMode } = useConversationStore();
|
||||
|
||||
if (planContent) {
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full p-4 overflow-auto">
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
ul,
|
||||
ol,
|
||||
a: anchor,
|
||||
p: paragraph,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
}}
|
||||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||||
>
|
||||
{planContent}
|
||||
</Markdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full p-10">
|
||||
|
||||
@ -72,6 +72,7 @@ export function handleStatusMessage(message: StatusMessage) {
|
||||
message: message.message,
|
||||
source: "chat",
|
||||
metadata: { msgId: message.id },
|
||||
posthog: undefined, // Service file - can't use hooks
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ interface ConversationState {
|
||||
submittedMessage: string | null;
|
||||
shouldHideSuggestions: boolean; // New state to hide suggestions when input expands
|
||||
hasRightPanelToggled: boolean;
|
||||
planContent: string | null;
|
||||
conversationMode: ConversationMode;
|
||||
}
|
||||
|
||||
@ -78,6 +79,91 @@ export const useConversationStore = create<ConversationStore>()(
|
||||
submittedMessage: null,
|
||||
shouldHideSuggestions: false,
|
||||
hasRightPanelToggled: true,
|
||||
planContent: `
|
||||
# Improve Developer Onboarding and Examples
|
||||
|
||||
## Overview
|
||||
|
||||
Based on the analysis of Browser-Use's current documentation and examples, this plan addresses gaps in developer onboarding by creating a progressive learning path, troubleshooting resources, and practical examples that address real-world scenarios (like the LM Studio/local LLM integration issues encountered).
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
**Strengths:**
|
||||
|
||||
- Good quickstart documentation in \`docs/quickstart.mdx\`
|
||||
- Extensive examples across multiple categories (60+ example files)
|
||||
- Well-structured docs with multiple LLM provider examples
|
||||
- Active community support via Discord
|
||||
|
||||
**Gaps Identified:**
|
||||
|
||||
- No progressive tutorial series that builds complexity gradually
|
||||
- Limited troubleshooting documentation for common issues
|
||||
- Sparse comments in example files explaining what's happening
|
||||
- Local LLM setup (Ollama/LM Studio) not prominently featured
|
||||
- No "first 10 minutes" success path
|
||||
- Missing visual/conceptual architecture guides for beginners
|
||||
- Error messages don't always point to solutions
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Create Interactive Tutorial Series (\`examples/tutorials/\`)
|
||||
|
||||
**New folder structure:**
|
||||
|
||||
\`\`\`
|
||||
examples/tutorials/
|
||||
├── README.md # Tutorial overview and prerequisites
|
||||
├── 00_hello_world.py # Absolute minimal example
|
||||
├── 01_your_first_search.py # Basic search with detailed comments
|
||||
├── 02_understanding_actions.py # How actions work
|
||||
├── 03_data_extraction_basics.py # Extract data step-by-step
|
||||
├── 04_error_handling.py # Common errors and solutions
|
||||
├── 05_custom_tools_intro.py # First custom tool
|
||||
├── 06_local_llm_setup.py # Ollama/LM Studio complete guide
|
||||
└── 07_debugging_tips.py # Debugging strategies
|
||||
\`\`\`
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Each file 50–80 lines max
|
||||
- Extensive inline comments explaining every concept
|
||||
- Clear learning objectives at the top of each file
|
||||
- "What you'll learn" and "Prerequisites" sections
|
||||
- Common pitfalls highlighted
|
||||
- Expected output shown in comments
|
||||
|
||||
### 2. Troubleshooting Guide (\`docs/troubleshooting.mdx\`)
|
||||
|
||||
**Sections:**
|
||||
|
||||
- Installation issues (Chromium, dependencies, virtual environments)
|
||||
- LLM provider connection errors (API keys, timeouts, rate limits)
|
||||
- Local LLM setup (Ollama vs LM Studio, model compatibility)
|
||||
- Browser automation issues (element not found, timeout errors)
|
||||
- Common error messages with solutions
|
||||
- Performance optimization tips
|
||||
- When to ask for help (Discord/GitHub)
|
||||
|
||||
**Format:**
|
||||
|
||||
**Error: "LLM call timed out after 60 seconds"**
|
||||
|
||||
**What it means:**
|
||||
The model took too long to respond
|
||||
|
||||
**Common causes:**
|
||||
|
||||
1. Model is too slow for the task
|
||||
2. LM Studio/Ollama not responding properly
|
||||
3. Complex page overwhelming the model
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- Use flash_mode for faster execution
|
||||
- Try a faster model (Gemini Flash, GPT-4 Turbo Mini)
|
||||
- Simplify the task
|
||||
- Check model server logs`,
|
||||
conversationMode: "code",
|
||||
|
||||
// Actions
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import posthog from "posthog-js";
|
||||
import type { PostHog } from "posthog-js";
|
||||
import { handleStatusMessage } from "#/services/actions";
|
||||
import { displayErrorToast } from "./custom-toast-handlers";
|
||||
|
||||
@ -7,9 +7,17 @@ interface ErrorDetails {
|
||||
source?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
msgId?: string;
|
||||
posthog?: PostHog;
|
||||
}
|
||||
|
||||
export function trackError({ message, source, metadata = {} }: ErrorDetails) {
|
||||
export function trackError({
|
||||
message,
|
||||
source,
|
||||
metadata = {},
|
||||
posthog,
|
||||
}: ErrorDetails) {
|
||||
if (!posthog) return;
|
||||
|
||||
const error = new Error(message);
|
||||
posthog.captureException(error, {
|
||||
error_source: source || "unknown",
|
||||
@ -21,8 +29,9 @@ export function showErrorToast({
|
||||
message,
|
||||
source,
|
||||
metadata = {},
|
||||
posthog,
|
||||
}: ErrorDetails) {
|
||||
trackError({ message, source, metadata });
|
||||
trackError({ message, source, metadata, posthog });
|
||||
displayErrorToast(message);
|
||||
}
|
||||
|
||||
@ -31,8 +40,9 @@ export function showChatError({
|
||||
source,
|
||||
metadata = {},
|
||||
msgId,
|
||||
posthog,
|
||||
}: ErrorDetails) {
|
||||
trackError({ message, source, metadata });
|
||||
trackError({ message, source, metadata, posthog });
|
||||
handleStatusMessage({
|
||||
type: "error",
|
||||
message,
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import posthog from "posthog-js";
|
||||
import type { PostHog } from "posthog-js";
|
||||
|
||||
/**
|
||||
* Handle user consent for tracking
|
||||
* @param posthog PostHog instance (from usePostHog hook)
|
||||
* @param consent Whether the user consents to tracking
|
||||
*/
|
||||
export const handleCaptureConsent = (consent: boolean) => {
|
||||
export const handleCaptureConsent = (
|
||||
posthog: PostHog | undefined,
|
||||
consent: boolean,
|
||||
) => {
|
||||
if (!posthog) return;
|
||||
|
||||
if (consent && !posthog.has_opted_in_capturing()) {
|
||||
posthog.opt_in_capturing();
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { AgentState } from "#/types/agent-state";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { StatusMessage } from "#/types/message";
|
||||
import { RuntimeStatus } from "#/types/runtime-status";
|
||||
import { V1AppConversationStartTaskStatus } from "#/api/conversation-service/v1-conversation-service.types";
|
||||
|
||||
export enum IndicatorColor {
|
||||
BLUE = "bg-blue-500",
|
||||
@ -103,8 +104,15 @@ export function getStatusCode(
|
||||
conversationStatus: ConversationStatus | null,
|
||||
runtimeStatus: RuntimeStatus | null,
|
||||
agentState: AgentState | null,
|
||||
taskStatus?: V1AppConversationStartTaskStatus | null,
|
||||
) {
|
||||
// Handle conversation and runtime stopped states
|
||||
// PRIORITY 1: Handle task error state (when start-tasks API returns ERROR)
|
||||
// This must come first to prevent "Connecting..." from showing when task has errored
|
||||
if (taskStatus === "ERROR") {
|
||||
return I18nKey.AGENT_STATUS$ERROR_OCCURRED;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Handle conversation and runtime stopped states
|
||||
if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") {
|
||||
return I18nKey.CHAT_INTERFACE$STOPPED;
|
||||
}
|
||||
@ -134,7 +142,8 @@ export function getStatusCode(
|
||||
return runtimeStatus;
|
||||
}
|
||||
|
||||
// Handle WebSocket connection states
|
||||
// PRIORITY 3: Handle WebSocket connection states
|
||||
// Note: WebSocket may be stuck in CONNECTING when task errors, so we check taskStatus first
|
||||
if (webSocketStatus === "DISCONNECTED") {
|
||||
return I18nKey.CHAT_INTERFACE$DISCONNECTED;
|
||||
}
|
||||
|
||||
4
openhands-cli/.gitignore
vendored
4
openhands-cli/.gitignore
vendored
@ -50,3 +50,7 @@ coverage.xml
|
||||
*.manifest
|
||||
# Note: We keep our custom spec file in version control
|
||||
# *.spec
|
||||
|
||||
# Generated artifacts
|
||||
build
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
|
||||
from openhands_cli.utils import get_default_cli_agent, get_llm_metadata
|
||||
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
||||
|
||||
from openhands.sdk import LLM
|
||||
@ -269,7 +269,7 @@ def main() -> int:
|
||||
llm=LLM(
|
||||
model='dummy-model',
|
||||
api_key='dummy-key',
|
||||
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
||||
litellm_extra_body={"metadata": get_llm_metadata(model_name='dummy-model', llm_type='openhands')},
|
||||
)
|
||||
)
|
||||
if not test_executable(dummy_agent):
|
||||
|
||||
@ -53,7 +53,7 @@ a = Analysis(
|
||||
'mcp.client',
|
||||
'mcp.server',
|
||||
'mcp.shared',
|
||||
'openhands.tools.execute_bash',
|
||||
'openhands.tools.terminal',
|
||||
'openhands.tools.str_replace_editor',
|
||||
'openhands.tools.task_tracker',
|
||||
],
|
||||
|
||||
@ -12,7 +12,7 @@ from openhands.sdk import (
|
||||
Message,
|
||||
TextContent,
|
||||
)
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
|
||||
@ -184,9 +184,9 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
||||
|
||||
conversation = runner.conversation
|
||||
if not (
|
||||
conversation.state.agent_status == AgentExecutionStatus.PAUSED
|
||||
or conversation.state.agent_status
|
||||
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
conversation.state.execution_status == ConversationExecutionStatus.PAUSED
|
||||
or conversation.state.execution_status
|
||||
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
):
|
||||
print_formatted_text(
|
||||
HTML('<red>No paused conversation to resume...</red>')
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import BaseConversation, Message
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus, ConversationState
|
||||
from openhands.sdk.conversation.state import (
|
||||
ConversationExecutionStatus,
|
||||
ConversationState,
|
||||
)
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
AlwaysConfirm,
|
||||
ConfirmationPolicyBase,
|
||||
@ -51,7 +54,10 @@ class ConversationRunner:
|
||||
|
||||
def _print_run_status(self) -> None:
|
||||
print_formatted_text('')
|
||||
if self.conversation.state.agent_status == AgentExecutionStatus.PAUSED:
|
||||
if (
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.PAUSED
|
||||
):
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'<yellow>Resuming paused conversation...</yellow><grey> (Press Ctrl-P to pause)</grey>'
|
||||
@ -91,8 +97,8 @@ class ConversationRunner:
|
||||
def _run_with_confirmation(self) -> None:
|
||||
# If agent was paused, resume with confirmation request
|
||||
if (
|
||||
self.conversation.state.agent_status
|
||||
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
):
|
||||
user_confirmation = self._handle_confirmation_request()
|
||||
if user_confirmation == UserConfirmation.DEFER:
|
||||
@ -106,12 +112,15 @@ class ConversationRunner:
|
||||
break
|
||||
|
||||
# In confirmation mode, agent either finishes or waits for user confirmation
|
||||
if self.conversation.state.agent_status == AgentExecutionStatus.FINISHED:
|
||||
if (
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.FINISHED
|
||||
):
|
||||
break
|
||||
|
||||
elif (
|
||||
self.conversation.state.agent_status
|
||||
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
):
|
||||
user_confirmation = self._handle_confirmation_request()
|
||||
if user_confirmation == UserConfirmation.DEFER:
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import uuid
|
||||
|
||||
from openhands.sdk.conversation import visualizer
|
||||
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace, register_tool
|
||||
from openhands.tools.execute_bash import BashTool
|
||||
from openhands.tools.file_editor import FileEditorTool
|
||||
from openhands.tools.task_tracker import TaskTrackerTool
|
||||
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace
|
||||
from openhands_cli.locations import CONVERSATIONS_DIR, WORK_DIR
|
||||
from openhands_cli.tui.settings.store import AgentStore
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
AlwaysConfirm,
|
||||
)
|
||||
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
||||
from openhands_cli.tui.visualizer import CLIVisualizer
|
||||
|
||||
|
||||
register_tool('BashTool', BashTool)
|
||||
register_tool('FileEditorTool', FileEditorTool)
|
||||
register_tool('TaskTrackerTool', TaskTrackerTool)
|
||||
# register tools
|
||||
from openhands.tools.terminal import TerminalTool
|
||||
from openhands.tools.file_editor import FileEditorTool
|
||||
from openhands.tools.task_tracker import TaskTrackerTool
|
||||
|
||||
|
||||
class MissingAgentSpec(Exception):
|
||||
@ -75,11 +75,7 @@ def setup_conversation(
|
||||
|
||||
agent = load_agent_specs(str(conversation_id))
|
||||
|
||||
if not include_security_analyzer:
|
||||
# Remove security analyzer from agent spec
|
||||
agent = agent.model_copy(
|
||||
update={"security_analyzer": None}
|
||||
)
|
||||
|
||||
|
||||
# Create conversation - agent context is now set in AgentStore.load()
|
||||
conversation: BaseConversation = Conversation(
|
||||
@ -88,9 +84,14 @@ def setup_conversation(
|
||||
# Conversation will add /<conversation_id> to this path
|
||||
persistence_dir=CONVERSATIONS_DIR,
|
||||
conversation_id=conversation_id,
|
||||
visualizer=CLIVisualizer
|
||||
)
|
||||
|
||||
if include_security_analyzer:
|
||||
# Security analyzer is set though conversation API now
|
||||
if not include_security_analyzer:
|
||||
conversation.set_security_analyzer(None)
|
||||
else:
|
||||
conversation.set_security_analyzer(LLMSecurityAnalyzer())
|
||||
conversation.set_confirmation_policy(AlwaysConfirm())
|
||||
|
||||
print_formatted_text(
|
||||
|
||||
@ -5,7 +5,7 @@ from prompt_toolkit import HTML, print_formatted_text
|
||||
from prompt_toolkit.shortcuts import print_container
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
|
||||
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
|
||||
from openhands_cli.utils import get_default_cli_agent, get_llm_metadata
|
||||
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
||||
from openhands_cli.pt_style import COLOR_GREY
|
||||
from openhands_cli.tui.settings.store import AgentStore
|
||||
@ -180,7 +180,7 @@ class SettingsScreen:
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
usage_id='agent',
|
||||
metadata=get_llm_metadata(model_name=model, llm_type='agent'),
|
||||
litellm_extra_body={"metadata": get_llm_metadata(model_name=model, llm_type='agent')},
|
||||
)
|
||||
|
||||
agent = self.agent_store.load()
|
||||
|
||||
@ -5,13 +5,13 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastmcp.mcp_config import MCPConfig
|
||||
from openhands_cli.utils import get_llm_metadata
|
||||
from openhands_cli.locations import (
|
||||
AGENT_SETTINGS_PATH,
|
||||
MCP_CONFIG_FILE,
|
||||
PERSISTENCE_DIR,
|
||||
WORK_DIR,
|
||||
)
|
||||
from openhands_cli.utils import get_llm_metadata
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import Agent, AgentContext, LocalFileStore
|
||||
@ -38,6 +38,16 @@ class AgentStore:
|
||||
str_spec = self.file_store.read(AGENT_SETTINGS_PATH)
|
||||
agent = Agent.model_validate_json(str_spec)
|
||||
|
||||
|
||||
# Temporary to remove security analyzer from agent specs
|
||||
# Security analyzer is set via conversation API now
|
||||
# Doing this so that deprecation warning is thrown only the first time running CLI
|
||||
if agent.security_analyzer:
|
||||
agent = agent.model_copy(
|
||||
update={"security_analyzer": None}
|
||||
)
|
||||
self.save(agent)
|
||||
|
||||
# Update tools with most recent working directory
|
||||
updated_tools = get_default_tools(enable_browser=False)
|
||||
|
||||
@ -51,20 +61,23 @@ class AgentStore:
|
||||
agent_llm_metadata = get_llm_metadata(
|
||||
model_name=agent.llm.model, llm_type='agent', session_id=session_id
|
||||
)
|
||||
updated_llm = agent.llm.model_copy(update={'metadata': agent_llm_metadata})
|
||||
updated_llm = agent.llm.model_copy(update={'litellm_extra_body': {'metadata': agent_llm_metadata}})
|
||||
|
||||
condenser_updates = {}
|
||||
if agent.condenser and isinstance(agent.condenser, LLMSummarizingCondenser):
|
||||
condenser_updates['llm'] = agent.condenser.llm.model_copy(
|
||||
update={
|
||||
'metadata': get_llm_metadata(
|
||||
model_name=agent.condenser.llm.model,
|
||||
llm_type='condenser',
|
||||
session_id=session_id,
|
||||
)
|
||||
'litellm_extra_body': {
|
||||
'metadata': get_llm_metadata(
|
||||
model_name=agent.condenser.llm.model,
|
||||
llm_type='condenser',
|
||||
session_id=session_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Update tools and context
|
||||
agent = agent.model_copy(
|
||||
update={
|
||||
'llm': updated_llm,
|
||||
|
||||
312
openhands-cli/openhands_cli/tui/visualizer.py
Normal file
312
openhands-cli/openhands_cli/tui/visualizer.py
Normal file
@ -0,0 +1,312 @@
|
||||
import re
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from openhands.sdk.conversation.visualizer.base import (
|
||||
ConversationVisualizerBase,
|
||||
)
|
||||
from openhands.sdk.event import (
|
||||
ActionEvent,
|
||||
AgentErrorEvent,
|
||||
MessageEvent,
|
||||
ObservationEvent,
|
||||
PauseEvent,
|
||||
SystemPromptEvent,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.sdk.event.base import Event
|
||||
from openhands.sdk.event.condenser import Condensation
|
||||
|
||||
|
||||
# These are external inputs
|
||||
_OBSERVATION_COLOR = "yellow"
|
||||
_MESSAGE_USER_COLOR = "gold3"
|
||||
_PAUSE_COLOR = "bright_yellow"
|
||||
# These are internal system stuff
|
||||
_SYSTEM_COLOR = "magenta"
|
||||
_THOUGHT_COLOR = "bright_black"
|
||||
_ERROR_COLOR = "red"
|
||||
# These are agent actions
|
||||
_ACTION_COLOR = "blue"
|
||||
_MESSAGE_ASSISTANT_COLOR = _ACTION_COLOR
|
||||
|
||||
DEFAULT_HIGHLIGHT_REGEX = {
|
||||
r"^Reasoning:": f"bold {_THOUGHT_COLOR}",
|
||||
r"^Thought:": f"bold {_THOUGHT_COLOR}",
|
||||
r"^Action:": f"bold {_ACTION_COLOR}",
|
||||
r"^Arguments:": f"bold {_ACTION_COLOR}",
|
||||
r"^Tool:": f"bold {_OBSERVATION_COLOR}",
|
||||
r"^Result:": f"bold {_OBSERVATION_COLOR}",
|
||||
r"^Rejection Reason:": f"bold {_ERROR_COLOR}",
|
||||
# Markdown-style
|
||||
r"\*\*(.*?)\*\*": "bold",
|
||||
r"\*(.*?)\*": "italic",
|
||||
}
|
||||
|
||||
_PANEL_PADDING = (1, 1)
|
||||
|
||||
|
||||
class CLIVisualizer(ConversationVisualizerBase):
|
||||
"""Handles visualization of conversation events with Rich formatting.
|
||||
|
||||
Provides Rich-formatted output with panels and complete content display.
|
||||
"""
|
||||
|
||||
_console: Console
|
||||
_skip_user_messages: bool
|
||||
_highlight_patterns: dict[str, str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = None,
|
||||
highlight_regex: dict[str, str] | None = DEFAULT_HIGHLIGHT_REGEX,
|
||||
skip_user_messages: bool = False,
|
||||
):
|
||||
"""Initialize the visualizer.
|
||||
|
||||
Args:
|
||||
name: Optional name to prefix in panel titles to identify
|
||||
which agent/conversation is speaking.
|
||||
highlight_regex: Dictionary mapping regex patterns to Rich color styles
|
||||
for highlighting keywords in the visualizer.
|
||||
For example: {"Reasoning:": "bold blue",
|
||||
"Thought:": "bold green"}
|
||||
skip_user_messages: If True, skip displaying user messages. Useful for
|
||||
scenarios where user input is not relevant to show.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
)
|
||||
self._console = Console()
|
||||
self._skip_user_messages = skip_user_messages
|
||||
self._highlight_patterns = highlight_regex or {}
|
||||
|
||||
def on_event(self, event: Event) -> None:
|
||||
"""Main event handler that displays events with Rich formatting."""
|
||||
panel = self._create_event_panel(event)
|
||||
if panel:
|
||||
self._console.print(panel)
|
||||
self._console.print() # Add spacing between events
|
||||
|
||||
def _apply_highlighting(self, text: Text) -> Text:
|
||||
"""Apply regex-based highlighting to text content.
|
||||
|
||||
Args:
|
||||
text: The Rich Text object to highlight
|
||||
|
||||
Returns:
|
||||
A new Text object with highlighting applied
|
||||
"""
|
||||
if not self._highlight_patterns:
|
||||
return text
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
highlighted = text.copy()
|
||||
|
||||
# Apply each pattern using Rich's built-in highlight_regex method
|
||||
for pattern, style in self._highlight_patterns.items():
|
||||
pattern_compiled = re.compile(pattern, re.MULTILINE)
|
||||
highlighted.highlight_regex(pattern_compiled, style)
|
||||
|
||||
return highlighted
|
||||
|
||||
def _create_event_panel(self, event: Event) -> Panel | None:
|
||||
"""Create a Rich Panel for the event with appropriate styling."""
|
||||
# Use the event's visualize property for content
|
||||
content = event.visualize
|
||||
|
||||
if not content.plain.strip():
|
||||
return None
|
||||
|
||||
# Apply highlighting if configured
|
||||
if self._highlight_patterns:
|
||||
content = self._apply_highlighting(content)
|
||||
|
||||
# Don't emit system prompt in CLI
|
||||
if isinstance(event, SystemPromptEvent):
|
||||
title = f"[bold {_SYSTEM_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"System Prompt[/bold {_SYSTEM_COLOR}]"
|
||||
return None
|
||||
elif isinstance(event, ActionEvent):
|
||||
# Check if action is None (non-executable)
|
||||
title = f"[bold {_ACTION_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
if event.action is None:
|
||||
title += f"Agent Action (Not Executed)[/bold {_ACTION_COLOR}]"
|
||||
else:
|
||||
title += f"Agent Action[/bold {_ACTION_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=_ACTION_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, ObservationEvent):
|
||||
title = f"[bold {_OBSERVATION_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"Observation[/bold {_OBSERVATION_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=_OBSERVATION_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, UserRejectObservation):
|
||||
title = f"[bold {_ERROR_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"User Rejected Action[/bold {_ERROR_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=_ERROR_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, MessageEvent):
|
||||
if (
|
||||
self._skip_user_messages
|
||||
and event.llm_message
|
||||
and event.llm_message.role == "user"
|
||||
):
|
||||
return
|
||||
assert event.llm_message is not None
|
||||
# Role-based styling
|
||||
role_colors = {
|
||||
"user": _MESSAGE_USER_COLOR,
|
||||
"assistant": _MESSAGE_ASSISTANT_COLOR,
|
||||
}
|
||||
role_color = role_colors.get(event.llm_message.role, "white")
|
||||
|
||||
# "User Message To [Name] Agent" for user
|
||||
# "Message from [Name] Agent" for agent
|
||||
agent_name = f"{self._name} " if self._name else ""
|
||||
|
||||
if event.llm_message.role == "user":
|
||||
title_text = (
|
||||
f"[bold {role_color}]User Message to "
|
||||
f"{agent_name}Agent[/bold {role_color}]"
|
||||
)
|
||||
else:
|
||||
title_text = (
|
||||
f"[bold {role_color}]Message from "
|
||||
f"{agent_name}Agent[/bold {role_color}]"
|
||||
)
|
||||
return Panel(
|
||||
content,
|
||||
title=title_text,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=role_color,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, AgentErrorEvent):
|
||||
title = f"[bold {_ERROR_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"Agent Error[/bold {_ERROR_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=_ERROR_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, PauseEvent):
|
||||
title = f"[bold {_PAUSE_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"User Paused[/bold {_PAUSE_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style=_PAUSE_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
elif isinstance(event, Condensation):
|
||||
title = f"[bold {_SYSTEM_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"Condensation[/bold {_SYSTEM_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=self._format_metrics_subtitle(),
|
||||
border_style=_SYSTEM_COLOR,
|
||||
expand=True,
|
||||
)
|
||||
else:
|
||||
# Fallback panel for unknown event types
|
||||
title = f"[bold {_ERROR_COLOR}]"
|
||||
if self._name:
|
||||
title += f"{self._name} "
|
||||
title += f"UNKNOWN Event: {event.__class__.__name__}[/bold {_ERROR_COLOR}]"
|
||||
return Panel(
|
||||
content,
|
||||
title=title,
|
||||
subtitle=f"({event.source})",
|
||||
border_style=_ERROR_COLOR,
|
||||
padding=_PANEL_PADDING,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
def _format_metrics_subtitle(self) -> str | None:
|
||||
"""Format LLM metrics as a visually appealing subtitle string with icons,
|
||||
colors, and k/m abbreviations using conversation stats."""
|
||||
stats = self.conversation_stats
|
||||
if not stats:
|
||||
return None
|
||||
|
||||
combined_metrics = stats.get_combined_metrics()
|
||||
if not combined_metrics or not combined_metrics.accumulated_token_usage:
|
||||
return None
|
||||
|
||||
usage = combined_metrics.accumulated_token_usage
|
||||
cost = combined_metrics.accumulated_cost or 0.0
|
||||
|
||||
# helper: 1234 -> "1.2K", 1200000 -> "1.2M"
|
||||
def abbr(n: int | float) -> str:
|
||||
n = int(n or 0)
|
||||
if n >= 1_000_000_000:
|
||||
val, suffix = n / 1_000_000_000, "B"
|
||||
elif n >= 1_000_000:
|
||||
val, suffix = n / 1_000_000, "M"
|
||||
elif n >= 1_000:
|
||||
val, suffix = n / 1_000, "K"
|
||||
else:
|
||||
return str(n)
|
||||
return f"{val:.2f}".rstrip("0").rstrip(".") + suffix
|
||||
|
||||
input_tokens = abbr(usage.prompt_tokens or 0)
|
||||
output_tokens = abbr(usage.completion_tokens or 0)
|
||||
|
||||
# Cache hit rate (prompt + cache)
|
||||
prompt = usage.prompt_tokens or 0
|
||||
cache_read = usage.cache_read_tokens or 0
|
||||
cache_rate = f"{(cache_read / prompt * 100):.2f}%" if prompt > 0 else "N/A"
|
||||
reasoning_tokens = usage.reasoning_tokens or 0
|
||||
|
||||
# Cost
|
||||
cost_str = f"{cost:.4f}" if cost > 0 else "0.00"
|
||||
|
||||
# Build with fixed color scheme
|
||||
parts: list[str] = []
|
||||
parts.append(f"[cyan]↑ input {input_tokens}[/cyan]")
|
||||
parts.append(f"[magenta]cache hit {cache_rate}[/magenta]")
|
||||
if reasoning_tokens > 0:
|
||||
parts.append(f"[yellow] reasoning {abbr(reasoning_tokens)}[/yellow]")
|
||||
parts.append(f"[blue]↓ output {output_tokens}[/blue]")
|
||||
parts.append(f"[green]$ {cost_str}[/green]")
|
||||
|
||||
return "Tokens: " + " • ".join(parts)
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
||||
from openhands.tools.preset import get_default_agent
|
||||
from openhands.sdk import LLM
|
||||
|
||||
@ -67,10 +66,4 @@ def get_default_cli_agent(
|
||||
cli_mode=True
|
||||
)
|
||||
|
||||
agent = agent.model_copy(
|
||||
update={
|
||||
'security_analyzer': LLMSecurityAnalyzer()
|
||||
}
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
|
||||
|
||||
[project]
|
||||
name = "openhands"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
@ -18,8 +18,8 @@ classifiers = [
|
||||
# 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==1.0.0a5",
|
||||
"openhands-tools==1.0.0a5",
|
||||
"openhands-sdk==1.1",
|
||||
"openhands-tools==1.1",
|
||||
"prompt-toolkit>=3",
|
||||
"typer>=0.17.4",
|
||||
]
|
||||
@ -102,5 +102,5 @@ ignore_missing_imports = true
|
||||
# UNCOMMENT TO USE EXACT COMMIT FROM AGENT-SDK
|
||||
|
||||
# [tool.uv.sources]
|
||||
# openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-sdk", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
|
||||
# openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-tools", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
|
||||
# openhands-sdk = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-sdk", rev = "7b695dc519084e75c482b34473e714845d6cef92" }
|
||||
# openhands-tools = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-tools", rev = "7b695dc519084e75c482b34473e714845d6cef92" }
|
||||
|
||||
@ -6,7 +6,7 @@ import pytest
|
||||
from prompt_toolkit.input.defaults import create_pipe_input
|
||||
from prompt_toolkit.output.defaults import DummyOutput
|
||||
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
from openhands_cli.user_actions import UserConfirmation
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ def run_resume_command_test(commands, agent_status=None, expect_runner_created=T
|
||||
conv = MagicMock()
|
||||
conv.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
if agent_status:
|
||||
conv.state.agent_status = agent_status
|
||||
conv.state.execution_status = agent_status
|
||||
mock_setup_conversation.return_value = conv
|
||||
|
||||
# Mock runner
|
||||
@ -93,7 +93,7 @@ def run_resume_command_test(commands, agent_status=None, expect_runner_created=T
|
||||
def test_resume_command_warnings(commands, expected_warning, expect_runner_created):
|
||||
"""Test /resume command shows appropriate warnings."""
|
||||
# Set agent status to FINISHED for the "conversation exists but not paused" test
|
||||
agent_status = AgentExecutionStatus.FINISHED if expect_runner_created else None
|
||||
agent_status = ConversationExecutionStatus.FINISHED if expect_runner_created else None
|
||||
|
||||
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
||||
commands, agent_status=agent_status, expect_runner_created=expect_runner_created
|
||||
@ -117,8 +117,8 @@ def test_resume_command_warnings(commands, expected_warning, expect_runner_creat
|
||||
@pytest.mark.parametrize(
|
||||
"agent_status",
|
||||
[
|
||||
AgentExecutionStatus.PAUSED,
|
||||
AgentExecutionStatus.WAITING_FOR_CONFIRMATION,
|
||||
ConversationExecutionStatus.PAUSED,
|
||||
ConversationExecutionStatus.WAITING_FOR_CONFIRMATION,
|
||||
],
|
||||
)
|
||||
def test_resume_command_successful_resume(agent_status):
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
"""Test that first-time settings screen usage creates a default agent with security analyzer."""
|
||||
"""Test that first-time settings screen usage creates a default agent and conversation with security analyzer."""
|
||||
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
||||
from openhands_cli.user_actions.settings_action import SettingsType
|
||||
from openhands.sdk import LLM
|
||||
from openhands.sdk import LLM, Conversation, Workspace
|
||||
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
||||
from pydantic import SecretStr
|
||||
|
||||
|
||||
def test_first_time_settings_creates_default_agent_with_security_analyzer():
|
||||
"""Test that using the settings screen for the first time creates a default agent with a non-None security analyzer."""
|
||||
def test_first_time_settings_creates_default_agent_and_conversation_with_security_analyzer():
|
||||
"""Test that using the settings screen for the first time creates a default agent and conversation with security analyzer."""
|
||||
|
||||
# Create a settings screen instance (no conversation initially)
|
||||
screen = SettingsScreen(conversation=None)
|
||||
@ -50,17 +51,20 @@ def test_first_time_settings_creates_default_agent_with_security_analyzer():
|
||||
assert saved_agent.llm.model == 'openai/gpt-4o-mini', f"Expected model 'openai/gpt-4o-mini', got '{saved_agent.llm.model}'"
|
||||
assert saved_agent.llm.api_key.get_secret_value() == 'sk-test-key-123', "API key should match the provided value"
|
||||
|
||||
# Verify that the agent has a security analyzer and it's not None
|
||||
assert hasattr(saved_agent, 'security_analyzer'), "Agent should have a security_analyzer attribute"
|
||||
assert saved_agent.security_analyzer is not None, "Security analyzer should not be None"
|
||||
# Test that a conversation can be created with the agent and security analyzer can be set
|
||||
conversation = Conversation(agent=saved_agent, workspace=Workspace(working_dir='/tmp'))
|
||||
|
||||
# Verify the security analyzer has the expected type/kind
|
||||
assert hasattr(saved_agent.security_analyzer, 'kind'), "Security analyzer should have a 'kind' attribute"
|
||||
assert saved_agent.security_analyzer.kind == 'LLMSecurityAnalyzer', f"Expected security analyzer kind 'LLMSecurityAnalyzer', got '{saved_agent.security_analyzer.kind}'"
|
||||
# Set security analyzer using the new API
|
||||
security_analyzer = LLMSecurityAnalyzer()
|
||||
conversation.set_security_analyzer(security_analyzer)
|
||||
|
||||
# Verify that the security analyzer was set correctly
|
||||
assert conversation.state.security_analyzer is not None, "Conversation should have a security analyzer"
|
||||
assert conversation.state.security_analyzer.kind == 'LLMSecurityAnalyzer', f"Expected security analyzer kind 'LLMSecurityAnalyzer', got '{conversation.state.security_analyzer.kind}'"
|
||||
|
||||
|
||||
def test_first_time_settings_with_advanced_configuration():
|
||||
"""Test that advanced settings also create a default agent with security analyzer."""
|
||||
"""Test that advanced settings also create a default agent and conversation with security analyzer."""
|
||||
|
||||
screen = SettingsScreen(conversation=None)
|
||||
|
||||
@ -94,11 +98,20 @@ def test_first_time_settings_with_advanced_configuration():
|
||||
|
||||
saved_agent = screen.agent_store.load()
|
||||
|
||||
# Verify agent creation and security analyzer
|
||||
# Verify agent creation
|
||||
assert saved_agent is not None, "Agent should be created with advanced settings"
|
||||
assert saved_agent.security_analyzer is not None, "Security analyzer should not be None in advanced settings"
|
||||
assert saved_agent.security_analyzer.kind == 'LLMSecurityAnalyzer', "Security analyzer should be LLMSecurityAnalyzer"
|
||||
|
||||
# Verify advanced settings were applied
|
||||
assert saved_agent.llm.model == 'anthropic/claude-3-5-sonnet', "Custom model should be set"
|
||||
assert saved_agent.llm.base_url == 'https://api.anthropic.com', "Base URL should be set"
|
||||
assert saved_agent.llm.base_url == 'https://api.anthropic.com', "Base URL should be set"
|
||||
|
||||
# Test that a conversation can be created with the agent and security analyzer can be set
|
||||
conversation = Conversation(agent=saved_agent, workspace=Workspace(working_dir='/tmp'))
|
||||
|
||||
# Set security analyzer using the new API
|
||||
security_analyzer = LLMSecurityAnalyzer()
|
||||
conversation.set_security_analyzer(security_analyzer)
|
||||
|
||||
# Verify that the security analyzer was set correctly
|
||||
assert conversation.state.security_analyzer is not None, "Conversation should have a security analyzer"
|
||||
assert conversation.state.security_analyzer.kind == 'LLMSecurityAnalyzer', "Security analyzer should be LLMSecurityAnalyzer"
|
||||
@ -45,6 +45,7 @@ class TestConfirmationMode:
|
||||
patch('openhands_cli.setup.print_formatted_text') as mock_print,
|
||||
patch('openhands_cli.setup.HTML'),
|
||||
patch('openhands_cli.setup.uuid') as mock_uuid,
|
||||
patch('openhands_cli.setup.CLIVisualizer') as mock_visualizer,
|
||||
):
|
||||
# Mock dependencies
|
||||
mock_conversation_id = MagicMock()
|
||||
@ -72,6 +73,7 @@ class TestConfirmationMode:
|
||||
workspace=ANY,
|
||||
persistence_dir=ANY,
|
||||
conversation_id=mock_conversation_id,
|
||||
visualizer=mock_visualizer
|
||||
)
|
||||
|
||||
def test_setup_conversation_raises_missing_agent_spec(self) -> None:
|
||||
|
||||
@ -9,7 +9,7 @@ from pydantic import ConfigDict, SecretStr, model_validator
|
||||
from openhands.sdk import Conversation, ConversationCallbackType, LocalConversation
|
||||
from openhands.sdk.agent.base import AgentBase
|
||||
from openhands.sdk.conversation import ConversationState
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
from openhands.sdk.llm import LLM
|
||||
from openhands.sdk.security.confirmation_policy import AlwaysConfirm, NeverConfirm
|
||||
from unittest.mock import MagicMock
|
||||
@ -45,7 +45,7 @@ class FakeAgent(AgentBase):
|
||||
) -> None:
|
||||
self.step_count += 1
|
||||
if self.step_count == self.finish_on_step:
|
||||
conversation.state.agent_status = AgentExecutionStatus.FINISHED
|
||||
conversation.state.execution_status = ConversationExecutionStatus.FINISHED
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@ -56,10 +56,10 @@ def agent() -> FakeAgent:
|
||||
|
||||
class TestConversationRunner:
|
||||
@pytest.mark.parametrize(
|
||||
'agent_status', [AgentExecutionStatus.RUNNING, AgentExecutionStatus.PAUSED]
|
||||
'agent_status', [ConversationExecutionStatus.RUNNING, ConversationExecutionStatus.PAUSED]
|
||||
)
|
||||
def test_non_confirmation_mode_runs_once(
|
||||
self, agent: FakeAgent, agent_status: AgentExecutionStatus
|
||||
self, agent: FakeAgent, agent_status: ConversationExecutionStatus
|
||||
) -> None:
|
||||
"""
|
||||
1. Confirmation mode is not on
|
||||
@ -68,28 +68,38 @@ class TestConversationRunner:
|
||||
|
||||
convo = Conversation(agent)
|
||||
convo.max_iteration_per_run = 1
|
||||
convo.state.agent_status = agent_status
|
||||
convo.state.execution_status = agent_status
|
||||
cr = ConversationRunner(convo)
|
||||
cr.set_confirmation_policy(NeverConfirm())
|
||||
cr.process_message(message=None)
|
||||
|
||||
assert agent.step_count == 1
|
||||
assert convo.state.agent_status != AgentExecutionStatus.PAUSED
|
||||
assert (
|
||||
convo.state.execution_status != ConversationExecutionStatus.PAUSED
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'confirmation, final_status, expected_run_calls',
|
||||
[
|
||||
# Case 1: Agent waiting for confirmation; user DEFERS -> early return, no run()
|
||||
(UserConfirmation.DEFER, AgentExecutionStatus.WAITING_FOR_CONFIRMATION, 0),
|
||||
(
|
||||
UserConfirmation.DEFER,
|
||||
ConversationExecutionStatus.WAITING_FOR_CONFIRMATION,
|
||||
0,
|
||||
),
|
||||
# Case 2: Agent waiting for confirmation; user ACCEPTS -> run() once, break (finished=True)
|
||||
(UserConfirmation.ACCEPT, AgentExecutionStatus.FINISHED, 1),
|
||||
(
|
||||
UserConfirmation.ACCEPT,
|
||||
ConversationExecutionStatus.FINISHED,
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_confirmation_mode_waiting_and_user_decision_controls_run(
|
||||
self,
|
||||
agent: FakeAgent,
|
||||
confirmation: UserConfirmation,
|
||||
final_status: AgentExecutionStatus,
|
||||
final_status: ConversationExecutionStatus,
|
||||
expected_run_calls: int,
|
||||
) -> None:
|
||||
"""
|
||||
@ -98,16 +108,18 @@ class TestConversationRunner:
|
||||
3. If not paused, we should still ask for confirmation on actions
|
||||
4. If deferred no run call to agent should be made
|
||||
5. If accepted, run call to agent should be made
|
||||
|
||||
"""
|
||||
if final_status == AgentExecutionStatus.FINISHED:
|
||||
if final_status == ConversationExecutionStatus.FINISHED:
|
||||
agent.finish_on_step = 1
|
||||
|
||||
# Add a mock security analyzer to enable confirmation mode
|
||||
agent.security_analyzer = MagicMock()
|
||||
|
||||
convo = Conversation(agent)
|
||||
convo.state.agent_status = AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
|
||||
# Set security analyzer using the new API to enable confirmation mode
|
||||
convo.set_security_analyzer(MagicMock())
|
||||
|
||||
convo.state.execution_status = (
|
||||
ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
)
|
||||
cr = ConversationRunner(convo)
|
||||
cr.set_confirmation_policy(AlwaysConfirm())
|
||||
|
||||
@ -115,9 +127,10 @@ class TestConversationRunner:
|
||||
cr, '_handle_confirmation_request', return_value=confirmation
|
||||
) as mock_confirmation_request:
|
||||
cr.process_message(message=None)
|
||||
|
||||
mock_confirmation_request.assert_called_once()
|
||||
assert agent.step_count == expected_run_calls
|
||||
assert convo.state.agent_status == final_status
|
||||
assert convo.state.execution_status == final_status
|
||||
|
||||
def test_confirmation_mode_not_waiting__runs_once_when_finished_true(
|
||||
self, agent: FakeAgent
|
||||
@ -129,7 +142,7 @@ class TestConversationRunner:
|
||||
"""
|
||||
agent.finish_on_step = 1
|
||||
convo = Conversation(agent)
|
||||
convo.state.agent_status = AgentExecutionStatus.PAUSED
|
||||
convo.state.execution_status = ConversationExecutionStatus.PAUSED
|
||||
|
||||
cr = ConversationRunner(convo)
|
||||
cr.set_confirmation_policy(AlwaysConfirm())
|
||||
|
||||
@ -65,6 +65,6 @@ class TestToolFix:
|
||||
) # BashTool, FileEditorTool, TaskTrackerTool
|
||||
|
||||
tool_names = [tool.name for tool in loaded_agent.tools]
|
||||
assert 'BashTool' in tool_names
|
||||
assert 'FileEditorTool' in tool_names
|
||||
assert 'TaskTrackerTool' in tool_names
|
||||
assert 'terminal' in tool_names
|
||||
assert 'file_editor' in tool_names
|
||||
assert 'task_tracker' in tool_names
|
||||
|
||||
238
openhands-cli/tests/visualizer/test_visualizer.py
Normal file
238
openhands-cli/tests/visualizer/test_visualizer.py
Normal file
@ -0,0 +1,238 @@
|
||||
"""Tests for the conversation visualizer and event visualization."""
|
||||
|
||||
import json
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from openhands_cli.tui.visualizer import (
|
||||
CLIVisualizer,
|
||||
)
|
||||
from openhands.sdk.event import (
|
||||
ActionEvent,
|
||||
SystemPromptEvent,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.sdk.llm import (
|
||||
MessageToolCall,
|
||||
TextContent,
|
||||
)
|
||||
from openhands.sdk.tool import Action
|
||||
|
||||
|
||||
class VisualizerMockAction(Action):
|
||||
"""Mock action for testing."""
|
||||
|
||||
command: str = "test command"
|
||||
working_dir: str = "/tmp"
|
||||
|
||||
|
||||
class VisualizerCustomAction(Action):
|
||||
"""Custom action with overridden visualize method."""
|
||||
|
||||
task_list: list[dict] = []
|
||||
|
||||
@property
|
||||
def visualize(self) -> Text:
|
||||
"""Custom visualization for task tracker."""
|
||||
content = Text()
|
||||
content.append("Task Tracker Action\n", style="bold")
|
||||
content.append(f"Tasks: {len(self.task_list)}")
|
||||
for i, task in enumerate(self.task_list):
|
||||
content.append(f"\n {i + 1}. {task.get('title', 'Untitled')}")
|
||||
return content
|
||||
|
||||
|
||||
def create_tool_call(
|
||||
call_id: str, function_name: str, arguments: dict
|
||||
) -> MessageToolCall:
|
||||
"""Helper to create a MessageToolCall."""
|
||||
return MessageToolCall(
|
||||
id=call_id,
|
||||
name=function_name,
|
||||
arguments=json.dumps(arguments),
|
||||
origin="completion",
|
||||
)
|
||||
|
||||
|
||||
def test_conversation_visualizer_initialization():
|
||||
"""Test DefaultConversationVisualizer can be initialized."""
|
||||
visualizer = CLIVisualizer()
|
||||
assert visualizer is not None
|
||||
assert hasattr(visualizer, "on_event")
|
||||
assert hasattr(visualizer, "_create_event_panel")
|
||||
|
||||
|
||||
def test_visualizer_event_panel_creation():
|
||||
"""Test that visualizer creates panels for different event types."""
|
||||
conv_viz = CLIVisualizer()
|
||||
|
||||
# Test with a simple action event
|
||||
action = VisualizerMockAction(command="test")
|
||||
tool_call = create_tool_call("call_1", "test", {})
|
||||
action_event = ActionEvent(
|
||||
thought=[TextContent(text="Testing")],
|
||||
action=action,
|
||||
tool_name="test",
|
||||
tool_call_id="call_1",
|
||||
tool_call=tool_call,
|
||||
llm_response_id="response_1",
|
||||
)
|
||||
panel = conv_viz._create_event_panel(action_event)
|
||||
assert panel is not None
|
||||
assert hasattr(panel, "renderable")
|
||||
|
||||
|
||||
def test_visualizer_action_event_with_none_action_panel():
|
||||
"""ActionEvent with action=None should render as 'Agent Action (Not Executed)'."""
|
||||
visualizer = CLIVisualizer()
|
||||
tc = create_tool_call("call_ne_1", "missing_fn", {})
|
||||
action_event = ActionEvent(
|
||||
thought=[TextContent(text="...")],
|
||||
tool_call=tc,
|
||||
tool_name=tc.name,
|
||||
tool_call_id=tc.id,
|
||||
llm_response_id="resp_viz_1",
|
||||
action=None,
|
||||
)
|
||||
panel = visualizer._create_event_panel(action_event)
|
||||
assert panel is not None
|
||||
# Ensure it doesn't fall back to UNKNOWN
|
||||
assert "UNKNOWN Event" not in str(panel.title)
|
||||
# And uses the 'Agent Action (Not Executed)' title
|
||||
assert "Agent Action (Not Executed)" in str(panel.title)
|
||||
|
||||
|
||||
def test_visualizer_user_reject_observation_panel():
|
||||
"""UserRejectObservation should render a dedicated panel."""
|
||||
visualizer = CLIVisualizer()
|
||||
event = UserRejectObservation(
|
||||
tool_name="demo_tool",
|
||||
tool_call_id="fc_call_1",
|
||||
action_id="action_1",
|
||||
rejection_reason="User rejected the proposed action.",
|
||||
)
|
||||
|
||||
panel = visualizer._create_event_panel(event)
|
||||
assert panel is not None
|
||||
title = str(panel.title)
|
||||
assert "UNKNOWN Event" not in title
|
||||
assert "User Rejected Action" in title
|
||||
# ensure the reason is part of the renderable text
|
||||
renderable = panel.renderable
|
||||
assert isinstance(renderable, Text)
|
||||
assert "User rejected the proposed action." in renderable.plain
|
||||
|
||||
|
||||
def test_metrics_formatting():
|
||||
"""Test metrics subtitle formatting."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openhands.sdk.conversation.conversation_stats import ConversationStats
|
||||
from openhands.sdk.llm.utils.metrics import Metrics
|
||||
|
||||
# Create conversation stats with metrics
|
||||
conversation_stats = ConversationStats()
|
||||
|
||||
# Create metrics and add to conversation stats
|
||||
metrics = Metrics(model_name="test-model")
|
||||
metrics.add_cost(0.0234)
|
||||
metrics.add_token_usage(
|
||||
prompt_tokens=1500,
|
||||
completion_tokens=500,
|
||||
cache_read_tokens=300,
|
||||
cache_write_tokens=0,
|
||||
reasoning_tokens=200,
|
||||
context_window=8000,
|
||||
response_id="test_response",
|
||||
)
|
||||
|
||||
# Add metrics to conversation stats
|
||||
conversation_stats.usage_to_metrics["test_usage"] = metrics
|
||||
|
||||
# Create visualizer and initialize with mock state
|
||||
visualizer = CLIVisualizer()
|
||||
mock_state = MagicMock()
|
||||
mock_state.stats = conversation_stats
|
||||
visualizer.initialize(mock_state)
|
||||
|
||||
# Test the metrics subtitle formatting
|
||||
subtitle = visualizer._format_metrics_subtitle()
|
||||
assert subtitle is not None
|
||||
assert "1.5K" in subtitle # Input tokens abbreviated (trailing zeros removed)
|
||||
assert "500" in subtitle # Output tokens
|
||||
assert "20.00%" in subtitle # Cache hit rate
|
||||
assert "200" in subtitle # Reasoning tokens
|
||||
assert "0.0234" in subtitle # Cost
|
||||
|
||||
|
||||
def test_metrics_abbreviation_formatting():
|
||||
"""Test number abbreviation with various edge cases."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openhands.sdk.conversation.conversation_stats import ConversationStats
|
||||
from openhands.sdk.llm.utils.metrics import Metrics
|
||||
|
||||
test_cases = [
|
||||
# (input_tokens, expected_abbr)
|
||||
(999, "999"), # Below threshold
|
||||
(1000, "1K"), # Exact K boundary, trailing zeros removed
|
||||
(1500, "1.5K"), # K with one decimal, trailing zero removed
|
||||
(89080, "89.08K"), # K with two decimals (regression test for bug)
|
||||
(89000, "89K"), # K with trailing zeros removed
|
||||
(1000000, "1M"), # Exact M boundary
|
||||
(1234567, "1.23M"), # M with decimals
|
||||
(1000000000, "1B"), # Exact B boundary
|
||||
]
|
||||
|
||||
for tokens, expected in test_cases:
|
||||
stats = ConversationStats()
|
||||
metrics = Metrics(model_name="test-model")
|
||||
metrics.add_token_usage(
|
||||
prompt_tokens=tokens,
|
||||
completion_tokens=100,
|
||||
cache_read_tokens=0,
|
||||
cache_write_tokens=0,
|
||||
reasoning_tokens=0,
|
||||
context_window=8000,
|
||||
response_id="test",
|
||||
)
|
||||
stats.usage_to_metrics["test"] = metrics
|
||||
|
||||
visualizer = CLIVisualizer()
|
||||
mock_state = MagicMock()
|
||||
mock_state.stats = stats
|
||||
visualizer.initialize(mock_state)
|
||||
subtitle = visualizer._format_metrics_subtitle()
|
||||
|
||||
assert subtitle is not None, f"Failed for {tokens}"
|
||||
assert expected in subtitle, (
|
||||
f"Expected '{expected}' in subtitle for {tokens}, got: {subtitle}"
|
||||
)
|
||||
|
||||
|
||||
def test_event_base_fallback_visualize():
|
||||
"""Test that Event provides fallback visualization."""
|
||||
from openhands.sdk.event.base import Event
|
||||
from openhands.sdk.event.types import SourceType
|
||||
|
||||
class UnknownEvent(Event):
|
||||
source: SourceType = "agent"
|
||||
|
||||
event = UnknownEvent()
|
||||
|
||||
conv_viz = CLIVisualizer()
|
||||
panel = conv_viz._create_event_panel(event)
|
||||
|
||||
assert "UNKNOWN Event" in str(panel.title)
|
||||
|
||||
|
||||
def test_visualizer_does_not_render_system_prompt():
|
||||
"""Test that Event provides fallback visualization."""
|
||||
system_prompt_event = SystemPromptEvent(
|
||||
source="agent",
|
||||
system_prompt=TextContent(text="dummy"),
|
||||
tools=[]
|
||||
)
|
||||
conv_viz = CLIVisualizer()
|
||||
panel = conv_viz._create_event_panel(system_prompt_event)
|
||||
assert panel is None
|
||||
333
openhands-cli/uv.lock
generated
333
openhands-cli/uv.lock
generated
@ -1055,6 +1055,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
@ -1064,6 +1066,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
@ -1071,6 +1075,8 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||
]
|
||||
|
||||
@ -1091,6 +1097,47 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/08/24d62fccb01c4e86c59ba79073af7e5c8ab643846823c2fa3e957bde4b58/groq-0.32.0-py3-none-any.whl", hash = "sha256:0ed0be290042f8826f851f3a1defaac4f979dcfce86ec4a0681a23af00ec800b", size = 135387, upload-time = "2025-09-27T23:01:33.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.76.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
@ -1426,6 +1473,33 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/f6/6aeedf8c6e75bfca08b9c73385186016446e8286803b381fcb9cac9c1594/litellm-1.78.5-py3-none-any.whl", hash = "sha256:aa716e9f2dfec406f1fb33831f3e49bc8bc6df73aa736aae21790516b7bb7832", size = 9827414, upload-time = "2025-10-18T22:24:35.398Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lmnr"
|
||||
version = "0.7.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
||||
{ name = "opentelemetry-instrumentation" },
|
||||
{ name = "opentelemetry-instrumentation-threading" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "opentelemetry-semantic-conventions-ai" },
|
||||
{ name = "orjson" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/c0/996403cc2f6967881a42af4b27ff8931956d57ab3ed2d8bf11e5b37aed40/lmnr-0.7.20.tar.gz", hash = "sha256:1f484cd618db2d71af65f90a0b8b36d20d80dc91a5138b811575c8677bf7c4fd", size = 194075, upload-time = "2025-11-04T16:53:34.49Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/df/4665a3931b2fbc5f5b66e4906ffab106f3f65ab7e78732ecdaf3ba4a3076/lmnr-0.7.20-py3-none-any.whl", hash = "sha256:5f9fa7444e6f96c25e097f66484ff29e632bdd1de0e9346948bf5595f4a8af38", size = 247465, upload-time = "2025-11-04T16:53:32.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
@ -1828,7 +1902,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openhands"
|
||||
version = "1.0.5"
|
||||
version = "1.0.7"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "openhands-sdk" },
|
||||
@ -1855,8 +1929,8 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openhands-sdk", specifier = "==1.0.0a5" },
|
||||
{ name = "openhands-tools", specifier = "==1.0.0a5" },
|
||||
{ name = "openhands-sdk", specifier = "==1.1" },
|
||||
{ name = "openhands-tools", specifier = "==1.1" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3" },
|
||||
{ name = "typer", specifier = ">=0.17.4" },
|
||||
]
|
||||
@ -1879,26 +1953,27 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "openhands-sdk"
|
||||
version = "1.0.0a5"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
{ name = "httpx" },
|
||||
{ name = "litellm" },
|
||||
{ name = "lmnr" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-frontmatter" },
|
||||
{ name = "python-json-logger" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/90/d40f6716641a95a61d2042f00855e0eadc0b2558167078324576cc5a3c22/openhands_sdk-1.0.0a5.tar.gz", hash = "sha256:8888d6892d58cf9b11a71fa80086156c0b6c9a0b50df6839c0a9cafffba2338c", size = 152810, upload-time = "2025-10-29T16:19:52.086Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/b2/97d9deb743b266683f3e70cebaa1d34ee247c019f7d6e42c2f5de529cb47/openhands_sdk-1.1.0.tar.gz", hash = "sha256:855e0d8f3657205e4119e50520c17e65b3358b1a923f7a051a82512a54bf426c", size = 166636, upload-time = "2025-11-11T19:07:04.249Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/6b/d3aa28019163f22f4b589ad818b83e3bea23d0a50b0c51ecc070ffdec139/openhands_sdk-1.0.0a5-py3-none-any.whl", hash = "sha256:db20272b04cf03627f9f7d1e87992078ac4ce15d188955a2962aa9e754d0af03", size = 204063, upload-time = "2025-10-29T16:19:50.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/9f/a97a10447f3be53df4639e43748c4178853e958df07ba74890f4968829d6/openhands_sdk-1.1.0-py3-none-any.whl", hash = "sha256:4a984ce1687a48cf99a67fdf3d37b116f8b2840743d4807810b5024af6a1d57e", size = 221594, upload-time = "2025-11-11T19:07:02.847Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openhands-tools"
|
||||
version = "1.0.0a5"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bashlex" },
|
||||
@ -1910,9 +1985,200 @@ dependencies = [
|
||||
{ name = "openhands-sdk" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/8d/d62bc5e6c986676363692743688f10b6a922fd24dd525e5c6e87bd6fc08e/openhands_tools-1.0.0a5.tar.gz", hash = "sha256:6c67454e612596e95c5151267659ddd3b633a5d4a1b70b348f7f913c62146562", size = 63012, upload-time = "2025-10-29T16:19:53.783Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/89/e2c5fc2d9e8dc6840ef2891ff6f76b9769b50a4c508fd3a626c1ab476fb1/openhands_tools-1.1.0.tar.gz", hash = "sha256:c2fadaa4f4e16e9a3df5781ea847565dcae7171584f09ef7c0e1d97c8dfc83f6", size = 62818, upload-time = "2025-11-11T19:07:06.527Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/9d/4da48258f0af73d017b61ed3f12786fae4caccc7e7cd97d77ef2bb25f00c/openhands_tools-1.0.0a5-py3-none-any.whl", hash = "sha256:74c27e23e6adc9a0bad00e32448bd4872019ce0786474e8de2fbf2d7c0887e8e", size = 84724, upload-time = "2025-10-29T16:19:52.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a3/e58d75b7bd8d5dfbe063fcfaaadbdfd24fd511d633a528cefd29f0e01056/openhands_tools-1.1.0-py3-none-any.whl", hash = "sha256:767d6746f05edade49263aa24450a037485a3dc23379f56917ef19aad22033f9", size = 85062, upload-time = "2025-11-11T19:07:05.315Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-grpc"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "grpcio" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common" },
|
||||
{ name = "opentelemetry-proto" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common" },
|
||||
{ name = "opentelemetry-proto" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation"
|
||||
version = "0.59b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-threading"
|
||||
version = "0.59b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-instrumentation" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/7a/84e97d8992808197006e607ae410c2219bdbbc23d1289ba0c244d3220741/opentelemetry_instrumentation_threading-0.59b0.tar.gz", hash = "sha256:ce5658730b697dcbc0e0d6d13643a69fd8aeb1b32fa8db3bade8ce114c7975f3", size = 8770, upload-time = "2025-10-16T08:40:03.587Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/50/32d29076aaa1c91983cdd3ca8c6bb4d344830cd7d87a7c0fdc2d98c58509/opentelemetry_instrumentation_threading-0.59b0-py3-none-any.whl", hash = "sha256:76da2fc01fe1dccebff6581080cff9e42ac7b27cc61eb563f3c4435c727e8eca", size = 9313, upload-time = "2025-10-16T08:39:15.876Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.59b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions-ai"
|
||||
version = "0.4.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368, upload-time = "2025-08-22T10:14:17.387Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080, upload-time = "2025-08-22T10:14:16.477Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.11.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571, upload-time = "2025-10-24T15:49:10.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891, upload-time = "2025-10-24T15:49:11.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137, upload-time = "2025-10-24T15:49:12.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152, upload-time = "2025-10-24T15:49:13.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834, upload-time = "2025-10-24T15:49:15.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519, upload-time = "2025-10-24T15:49:16.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749, upload-time = "2025-10-24T15:49:17.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325, upload-time = "2025-10-24T15:49:19.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204, upload-time = "2025-10-24T15:49:20.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242, upload-time = "2025-10-24T15:49:21.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013, upload-time = "2025-10-24T15:49:23.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951, upload-time = "2025-10-24T15:49:24.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049, upload-time = "2025-10-24T15:49:25.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461, upload-time = "2025-10-24T15:49:27.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167, upload-time = "2025-10-24T15:49:28.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501, upload-time = "2025-10-24T15:49:54.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862, upload-time = "2025-10-24T15:49:55.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047, upload-time = "2025-10-24T15:49:57.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073, upload-time = "2025-10-24T15:49:58.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597, upload-time = "2025-10-24T15:50:00.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515, upload-time = "2025-10-24T15:50:01.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703, upload-time = "2025-10-24T15:50:02.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311, upload-time = "2025-10-24T15:50:04.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127, upload-time = "2025-10-24T15:50:07.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201, upload-time = "2025-10-24T15:50:08.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872, upload-time = "2025-10-24T15:50:10.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931, upload-time = "2025-10-24T15:50:11.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065, upload-time = "2025-10-24T15:50:13.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310, upload-time = "2025-10-24T15:50:14.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151, upload-time = "2025-10-24T15:50:15.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5919,6 +6185,55 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.17.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.22.0"
|
||||
|
||||
@ -286,7 +286,7 @@ def response_to_actions(
|
||||
f'Unexpected task format in task_list: {type(task)} - {task}'
|
||||
)
|
||||
raise FunctionCallValidationError(
|
||||
f'Unexpected task format in task_list: {type(task)}. Each task shoud be a dictionary.'
|
||||
f'Unexpected task format in task_list: {type(task)}. Each task should be a dictionary.'
|
||||
)
|
||||
normalized_task_list.append(normalized_task)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user