Compare commits

..

27 Commits

Author SHA1 Message Date
Engel Nyst f9abb05b59 Delete example.py 2025-03-19 23:12:24 +01:00
OpenHands Bot c1e08dc548 🤖 Auto-fix Python linting issues 2025-03-19 22:11:08 +00:00
Engel Nyst 4c62e76e5e Delete tests/runtime/test_delegation.py.bak 2025-03-19 23:10:04 +01:00
openhands 34f65cb7c5 Fix pr #7364: Fix issue #7227: Integration test for delegation 2025-03-19 21:45:34 +00:00
openhands 9e42e4bff1 Fix issue #7227: Integration test for delegation 2025-03-19 21:30:03 +00:00
Xingyao Wang e4ccd4057d misc: tweak frontend prompt to prevent agent push to a different branch & update app prompt (#7357) 2025-03-20 05:09:51 +08:00
chuckbutkus c3d60b31d1 All-1465 Move user conversations (#7340) 2025-03-19 16:03:09 -04:00
mamoodi 35b70ca915 Release 0.29.1 (#7350) 2025-03-19 16:01:16 -04:00
Ivan Dagelic a8d65c11e0 fix: daytona runtime action execution handling (#7100)
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
2025-03-19 15:27:41 -04:00
Xingyao Wang a4746a53d8 Update prompt for runtime additional info (#7349) 2025-03-19 16:35:20 +00:00
Zaid Sheikh 13bb474623 feat(Session): add sandbox base, runtime container image to session settings (#7329) 2025-03-19 16:08:43 +00:00
blacksmith-sh[bot] 09aa62f1c3 blacksmith.sh: Migrate workflows to Blacksmith (#7148)
Co-authored-by: blacksmith-sh[bot] <157653362+blacksmith-sh[bot]@users.noreply.github.com>
2025-03-19 15:10:17 +00:00
Robert Brennan cbc26a5e40 Pass litellm error types to user and update error message (#7344)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-19 14:44:30 +00:00
Graham Neubig 6824d14ed8 Update config.template.toml to match current codebase (#7314)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-19 15:37:49 +01:00
Engel Nyst d9e40f721c (chore) Fix linting issues across the codebase (#7336)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-19 04:42:26 +00:00
Jason Burt 8a73184801 Docs : adding in github fine grained tokens documentation and settings link to documentation … (#7192)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-18 22:04:52 -04:00
Rohit Malhotra 06e8c4dad4 [Debug]: Add logs to runtime to assess root cause of expired github token (#7331) 2025-03-18 22:40:00 +00:00
Rohit Malhotra e2521743b6 [Bug]: Refresh runtime gh token when agent calls gh apis (#7330) 2025-03-18 21:24:57 +00:00
Xingyao Wang f2a54f4e23 Implement asynchronous browser initialization (#7328)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-19 03:34:57 +08:00
Graham Neubig a594595fea docs: fix broken links in LLM documentation (#7322)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-18 18:45:12 +01:00
Joseph Turian 0620679d11 fix: Correct JavaScript syntax in GitHub Actions workflow (#7194) 2025-03-18 13:42:37 -04:00
Nick 78708efbf1 feat(microagents): Add security microagent (#7323) 2025-03-18 17:13:06 +00:00
Jason Burt cf06f20a0e docs: Add development overview and documentation resources (#7220)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-18 17:09:37 +00:00
dependabot[bot] c68fba01a8 chore(deps): bump the version-all group with 4 updates (#7325)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-18 16:55:43 +00:00
mamoodi f2c7f8a6da Release 0.29 (#7236) 2025-03-18 11:52:31 -04:00
Engel Nyst 259140ffc9 Add tests for NullObservation with cause > 0 and clarify event flow (#7315)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-18 15:21:09 +00:00
Rohit Malhotra 3150af1ad7 [Fix]: Make provider tokens immutable (#7317) 2025-03-18 10:50:13 -04:00
57 changed files with 1575 additions and 463 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ on:
jobs:
del_runs:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
actions: write
contents: read
+4 -4
View File
@@ -24,18 +24,18 @@ jobs:
build:
if: github.repository == 'All-Hands-AI/OpenHands'
name: Build Docusaurus
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- uses: useblacksmith/setup-node@v5
with:
node-version: 18
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install dependencies
@@ -52,7 +52,7 @@ jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.repository == 'All-Hands-AI/OpenHands'
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
# This job only runs on "main" so only run one of these jobs at a time
# otherwise it will fail if one is already running
concurrency:
+3 -3
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
@@ -25,13 +25,13 @@ jobs:
- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux
- name: Setup Node.js
uses: actions/setup-node@v4
uses: useblacksmith/setup-node@v5
with:
node-version: '22.x'
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
cache: 'poetry'
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
# Run frontend unit tests
fe-test:
name: FE Unit Tests
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
node-version: [20, 22]
@@ -30,7 +30,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
uses: useblacksmith/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
+16 -16
View File
@@ -32,7 +32,7 @@ jobs:
# Builds the OpenHands Docker images
ghcr_build_app:
name: Build App Image
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: read
packages: write
@@ -80,7 +80,7 @@ jobs:
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Image
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: read
packages: write
@@ -108,11 +108,11 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v3
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Cache Poetry dependencies
uses: actions/cache@v4
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
@@ -150,7 +150,7 @@ jobs:
verify_hash_equivalence_in_runtime_and_app:
name: Verify Hash Equivalence in Runtime and Docker images
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [ghcr_build_runtime, ghcr_build_app]
strategy:
fail-fast: false
@@ -161,7 +161,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Cache Poetry dependencies
uses: actions/cache@v4
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
@@ -170,7 +170,7 @@ jobs:
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
@@ -204,7 +204,7 @@ jobs:
test_runtime_root:
name: RT Unit Tests (Root)
needs: [ghcr_build_runtime]
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:
@@ -226,7 +226,7 @@ jobs:
run: |
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
- name: Cache Poetry dependencies
uses: actions/cache@v4
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
@@ -235,7 +235,7 @@ jobs:
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
@@ -269,7 +269,7 @@ jobs:
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
name: RT Unit Tests (openhands)
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [ghcr_build_runtime]
strategy:
matrix:
@@ -291,7 +291,7 @@ jobs:
run: |
docker load --input /tmp/runtime-${{ matrix.base_image }}.tar
- name: Cache Poetry dependencies
uses: actions/cache@v4
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
@@ -300,7 +300,7 @@ jobs:
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
@@ -338,7 +338,7 @@ jobs:
runtime_tests_check_success:
name: All Runtime Tests Passed
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: All tests passed
@@ -347,7 +347,7 @@ jobs:
runtime_tests_check_fail:
name: All Runtime Tests Passed
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: Some tests failed
@@ -358,7 +358,7 @@ jobs:
name: Update PR Description
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
needs: [ghcr_build_runtime]
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
+3 -3
View File
@@ -18,7 +18,7 @@ env:
jobs:
run-integration-tests:
if: github.event.label.name == 'integration-test' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: "read"
id-token: "write"
@@ -35,13 +35,13 @@ jobs:
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Node.js
uses: actions/setup-node@v4
uses: useblacksmith/setup-node@v5
with:
node-version: '22.x'
+4 -4
View File
@@ -9,7 +9,7 @@ jobs:
lint-fix-frontend:
if: github.event.label.name == 'lint-fix'
name: Fix frontend linting issues
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: write
pull-requests: write
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Node.js 20
uses: actions/setup-node@v4
uses: useblacksmith/setup-node@v5
with:
node-version: 20
- name: Install frontend dependencies
@@ -52,7 +52,7 @@ jobs:
lint-fix-python:
if: github.event.label.name == 'lint-fix'
name: Fix Python linting issues
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: write
pull-requests: write
@@ -65,7 +65,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: 'pip'
+6 -6
View File
@@ -19,11 +19,11 @@ jobs:
# Run lint on the frontend code
lint-frontend:
name: Lint frontend
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Install Node.js 20
uses: actions/setup-node@v4
uses: useblacksmith/setup-node@v5
with:
node-version: 20
- name: Install dependencies
@@ -39,13 +39,13 @@ jobs:
# Run lint on the python code
lint-python:
name: Lint python
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: 'pip'
@@ -57,11 +57,11 @@ jobs:
# Check version consistency across documentation
check-version-consistency:
name: Check version consistency
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Set up python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
- name: Run version consistency check
+12 -7
View File
@@ -74,13 +74,13 @@ jobs:
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
)
)
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: "3.12"
@@ -106,7 +106,7 @@ jobs:
contains(github.event.review.body, '@openhands-agent-exp')
)
)
uses: actions/cache@v4
uses: useblacksmith/cache@v5
with:
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
@@ -295,11 +295,12 @@ jobs:
if: always()
env:
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const fs = require('fs');
const issueNumber = ${{ env.ISSUE_NUMBER }};
const issueNumber = process.env.ISSUE_NUMBER;
let logContent = '';
try {
@@ -330,13 +331,15 @@ jobs:
if: always() # Comment on issue even if the previous steps fail
env:
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
RESOLUTION_SUCCESS: ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const fs = require('fs');
const path = require('path');
const issueNumber = ${{ env.ISSUE_NUMBER }};
const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }};
const issueNumber = process.env.ISSUE_NUMBER;
const success = process.env.RESOLUTION_SUCCESS === 'true';
let prNumber = '';
let branchName = '';
@@ -401,10 +404,12 @@ jobs:
- name: Fallback Error Comment
uses: actions/github-script@v7
if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps
env:
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
script: |
const issueNumber = ${{ env.ISSUE_NUMBER }};
const issueNumber = process.env.ISSUE_NUMBER;
github.rest.issues.createComment({
issue_number: issueNumber,
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
# Run python unit tests on Linux
test-on-linux:
name: Python Unit Tests on Linux
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
env:
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
strategy:
@@ -33,13 +33,13 @@ jobs:
- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux
- name: Setup Node.js
uses: actions/setup-node@v4
uses: useblacksmith/setup-node@v5
with:
node-version: '22.x'
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
+2 -2
View File
@@ -12,10 +12,10 @@ on:
jobs:
release:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
- name: Install Poetry
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
trigger-job:
name: Trigger remote eval job
if: ${{ github.event.label.name == 'run-eval-xs' || github.event.label.name == 'run-eval-s' || github.event.label.name == 'run-eval-m' }}
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout PR branch
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
stale:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/stale@v9
with:
+34
View File
@@ -0,0 +1,34 @@
---
name: security
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- security
- vulnerability
- authentication
- authorization
- permissions
---
This document provides guidance on security best practices
You should always be considering security implications when developing.
You should always complete the task requested. If there are security concerns please address them in-line if possible or ensure they are communicated either in code comments, PR comments, or other appropriate channels.
## Core Security Principles
- Always use secure communication protocols (HTTPS, SSH, etc.)
- Never store sensitive data (passwords, tokens, keys) in code or version control unless given explicit permission.
- Apply the principle of least privilege
- Validate and sanitize all user inputs
## Common Security Checks
- Ensure proper authentication and authorization mechanisms
- Verify secure session management
- Confirm secure storage of sensitive data
- Validate secure configuration of services and APIs
## Error Handling
- Never expose sensitive information in error messages
- Log security events appropriately
- Implement proper exception handling
- Use secure error reporting mechanisms
+16
View File
@@ -126,3 +126,19 @@ cd ./containers/dev
```
You do need [Docker](https://docs.docker.com/engine/install/) installed on your host though.
## Key Documentation Resources
Here's a guide to the important documentation files in the repository:
- `/README.md`: Main project overview, features, and basic setup instructions
- `/Development.md` (this file): Comprehensive guide for developers working on OpenHands
- `/CONTRIBUTING.md`: Guidelines for contributing to the project, including code style and PR process
- `/docs/DOC_STYLE_GUIDE.md`: Standards for writing and maintaining project documentation
- `/openhands/README.md`: Details about the backend Python implementation
- `/frontend/README.md`: Frontend React application setup and development guide
- `/containers/README.md`: Information about Docker containers and deployment
- `/tests/unit/README.md`: Guide to writing and running unit tests
- `/evaluation/README.md`: Documentation for the evaluation framework and benchmarks
- `/microagents/README.md`: Information about the microagents architecture and implementation
- `/openhands/server/README.md`: Server implementation details and API documentation
- `/openhands/runtime/README.md`: Documentation for the runtime environment and execution model
+42 -16
View File
@@ -38,9 +38,6 @@ workspace_base = "./workspace"
# Disable color in terminal output
#disable_color = false
# Enable saving and restoring the session when run from CLI
#enable_cli_session = false
# Path to store trajectories, can be a folder or a file
# If it's a folder, the session id will be used as the file name
#save_trajectory_path="./trajectories"
@@ -56,9 +53,6 @@ workspace_base = "./workspace"
# File store type
#file_store = "memory"
# List of allowed file extensions for uploads
#file_uploads_allowed_extensions = [".*"]
# Maximum file size for uploads, in megabytes
#file_uploads_max_file_size_mb = 0
@@ -100,6 +94,12 @@ workspace_base = "./workspace"
# When false, a NoOpCondenserConfig (no summarization) will be used
#enable_default_condenser = true
# Maximum number of concurrent conversations per user
#max_concurrent_conversations = 3
# Maximum age of conversations in seconds before they are automatically closed
#conversation_max_age_seconds = 864000 # 10 days
#################################### LLM #####################################
# Configuration for LLM models (group name starts with 'llm')
# use 'llm' for the default LLM config
@@ -196,6 +196,8 @@ model = "gpt-4o"
# https://github.com/All-Hands-AI/OpenHands/pull/4711
#native_tool_calling = None
[llm.gpt4o-mini]
api_key = ""
model = "gpt-4o"
@@ -209,21 +211,15 @@ model = "gpt-4o"
##############################################################################
[agent]
# whether the browsing tool is enabled
# Whether the browsing tool is enabled
codeact_enable_browsing = true
# whether the LLM draft editor is enabled
# Whether the LLM draft editor is enabled
codeact_enable_llm_editor = false
# whether the IPython tool is enabled
# Whether the IPython tool is enabled
codeact_enable_jupyter = true
# Memory enabled
#memory_enabled = false
# Memory maximum threads
#memory_max_threads = 3
# LLM config group to use
#llm_config = 'your-llm-config-group'
@@ -258,7 +254,7 @@ llm_config = 'gpt3'
# Use host network
#use_host_network = false
# runtime extra build args
# Runtime extra build args
#runtime_extra_build_args = ["--network=host", "--add-host=host.docker.internal:host-gateway"]
# Enable auto linting after editing
@@ -276,6 +272,33 @@ llm_config = 'gpt3'
# BrowserGym environment to use for evaluation
#browsergym_eval_env = ""
# Platform to use for building the runtime image (e.g., "linux/amd64")
#platform = ""
# Force rebuild of runtime image even if it exists
#force_rebuild_runtime = false
# Runtime container image to use (if not provided, will be built from base_container_image)
#runtime_container_image = ""
# Keep runtime alive after session ends
#keep_runtime_alive = false
# Pause closed runtimes instead of stopping them
#pause_closed_runtimes = false
# Delay in seconds before closing idle runtimes
#close_delay = 300
# Remove all containers when stopping the runtime
#rm_all_containers = false
# Enable GPU support in the runtime
#enable_gpu = false
# Additional Docker runtime kwargs
#docker_runtime_kwargs = {}
#################################### Security ###################################
# Configuration for security features
##############################################################################
@@ -287,6 +310,9 @@ llm_config = 'gpt3'
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
#security_analyzer = ""
# Whether to enable security analyzer
#enable_security_analyzer = false
#################################### Condenser #################################
# Condensers control how conversation history is managed and compressed when
# the context grows too large. Each agent uses one condenser configuration.
@@ -11,41 +11,39 @@ la priorité.
# Table des matières
1. [Configuration de base](#configuration-de-base)
- [Clés API](#clés-api)
- [Espace de travail](#espace-de-travail)
- [Débogage et journalisation](#débogage-et-journalisation)
- [Gestion des sessions](#gestion-des-sessions)
- [Trajectoires](#trajectoires)
- [Stockage de fichiers](#stockage-de-fichiers)
- [Gestion des tâches](#gestion-des-tâches)
- [Configuration du bac à sable](#configuration-du-bac-à-sable)
- [Divers](#divers)
2. [Configuration LLM](#configuration-llm)
- [Informations d'identification AWS](#informations-didentification-aws)
- [Configuration de l'API](#configuration-de-lapi)
- [Fournisseur LLM personnalisé](#fournisseur-llm-personnalisé)
1. [Configuration de base](#core-configuration)
- [Clés API](#api-keys)
- [Espace de travail](#workspace)
- [Débogage et journalisation](#debugging-and-logging)
- [Trajectoires](#trajectories)
- [Stockage de fichiers](#file-store)
- [Gestion des tâches](#task-management)
- [Configuration du bac à sable](#sandbox-configuration)
- [Divers](#miscellaneous)
2. [Configuration LLM](#llm-configuration)
- [Informations d'identification AWS](#aws-credentials)
- [Configuration de l'API](#api-configuration)
- [Fournisseur LLM personnalisé](#custom-llm-provider)
- [Embeddings](#embeddings)
- [Gestion des messages](#gestion-des-messages)
- [Sélection du modèle](#sélection-du-modèle)
- [Nouvelles tentatives](#nouvelles-tentatives)
- [Options avancées](#options-avancées)
3. [Configuration de l'agent](#configuration-de-lagent)
- [Configuration du micro-agent](#configuration-du-micro-agent)
- [Configuration de la mémoire](#configuration-de-la-mémoire)
- [Configuration LLM](#configuration-llm-2)
- [Configuration de l'espace d'action](#configuration-de-lespace-daction)
- [Utilisation du micro-agent](#utilisation-du-micro-agent)
4. [Configuration du bac à sable](#configuration-du-bac-à-sable-2)
- [Exécution](#exécution)
- [Image de conteneur](#image-de-conteneur)
- [Mise en réseau](#mise-en-réseau)
- [Linting et plugins](#linting-et-plugins)
- [Dépendances et environnement](#dépendances-et-environnement)
- [Évaluation](#évaluation)
5. [Configuration de sécurité](#configuration-de-sécurité)
- [Mode de confirmation](#mode-de-confirmation)
- [Analyseur de sécurité](#analyseur-de-sécurité)
- [Gestion des messages](#message-handling)
- [Sélection du modèle](#model-selection)
- [Nouvelles tentatives](#retrying)
- [Options avancées](#advanced-options)
3. [Configuration de l'agent](#agent-configuration)
- [Configuration de la mémoire](#memory-configuration)
- [Configuration LLM](#llm-configuration-1)
- [Configuration de l'espace d'action](#actionspace-configuration)
- [Utilisation du micro-agent](#microagent-usage)
4. [Configuration du bac à sable](#sandbox-configuration-1)
- [Exécution](#execution)
- [Image de conteneur](#container-image)
- [Mise en réseau](#networking)
- [Linting et plugins](#linting-and-plugins)
- [Dépendances et environnement](#dependencies-and-environment)
- [Évaluation](#evaluation)
5. [Configuration de sécurité](#security-configuration)
- [Mode de confirmation](#confirmation-mode)
- [Analyseur de sécurité](#security-analyzer)
---
@@ -10,41 +10,39 @@
# 目录
1. [核心配置](#核心配置)
1. [核心配置](#core-configuration)
- [API Keys](#api-keys)
- [工作区](#工作区)
- [调试和日志记录](#调试和日志记录)
- [会话管理](#会话管理)
- [轨迹](#轨迹)
- [文件存储](#文件存储)
- [任务管理](#任务管理)
- [沙箱配置](#沙箱配置)
- [其他](#其他)
2. [LLM 配置](#llm-配置)
- [AWS 凭证](#aws-凭证)
- [API 配置](#api-配置)
- [自定义 LLM Provider](#自定义-llm-provider)
- [工作区](#workspace)
- [调试和日志记录](#debugging-and-logging)
- [轨迹](#trajectories)
- [文件存储](#file-store)
- [任务管理](#task-management)
- [沙箱配置](#sandbox-configuration)
- [其他](#miscellaneous)
2. [LLM 配置](#llm-configuration)
- [AWS 凭证](#aws-credentials)
- [API 配置](#api-configuration)
- [自定义 LLM Provider](#custom-llm-provider)
- [Embeddings](#embeddings)
- [消息处理](#消息处理)
- [模型选择](#模型选择)
- [重试](#重试)
- [高级选项](#高级选项)
3. [Agent 配置](#agent-配置)
- [Microagent 配置](#microagent-配置)
- [内存配置](#内存配置)
- [LLM 配置](#llm-配置-2)
- [ActionSpace 配置](#actionspace-配置)
- [Microagent 使用](#microagent-使用)
4. [沙箱配置](#沙箱配置-2)
- [执行](#执行)
- [容器镜像](#容器镜像)
- [网络](#网络)
- [Linting 和插件](#linting-和插件)
- [依赖和环境](#依赖和环境)
- [评估](#评估)
5. [安全配置](#安全配置)
- [确认模式](#确认模式)
- [安全分析器](#安全分析器)
- [消息处理](#message-handling)
- [模型选择](#model-selection)
- [重试](#retrying)
- [高级选项](#advanced-options)
3. [Agent 配置](#agent-configuration)
- [内存配置](#memory-configuration)
- [LLM 配置](#llm-configuration-1)
- [ActionSpace 配置](#actionspace-configuration)
- [Microagent 使用](#microagent-usage)
4. [沙箱配置](#sandbox-configuration-1)
- [执行](#execution)
- [容器镜像](#container-image)
- [网络](#networking)
- [Linting 和插件](#linting-and-plugins)
- [依赖和环境](#dependencies-and-environment)
- [评估](#evaluation)
5. [安全配置](#security-configuration)
- [确认模式](#confirmation-mode)
- [安全分析器](#security-analyzer)
---
@@ -0,0 +1,74 @@
---
sidebar_position: 9
---
# Development Overview
This guide provides an overview of the key documentation resources available in the OpenHands repository. Whether you're looking to contribute, understand the architecture, or work on specific components, these resources will help you navigate the codebase effectively.
## Core Documentation
### Project Fundamentals
- **Main Project Overview** (`/README.md`)
The primary entry point for understanding OpenHands, including features and basic setup instructions.
- **Development Guide** (`/Development.md`)
Comprehensive guide for developers working on OpenHands, including setup, requirements, and development workflows.
- **Contributing Guidelines** (`/CONTRIBUTING.md`)
Essential information for contributors, covering code style, PR process, and contribution workflows.
### Component Documentation
#### Frontend
- **Frontend Application** (`/frontend/README.md`)
Complete guide for setting up and developing the React-based frontend application.
#### Backend
- **Backend Implementation** (`/openhands/README.md`)
Detailed documentation of the Python backend implementation and architecture.
- **Server Documentation** (`/openhands/server/README.md`)
Server implementation details, API documentation, and service architecture.
- **Runtime Environment** (`/openhands/runtime/README.md`)
Documentation covering the runtime environment, execution model, and runtime configurations.
#### Infrastructure
- **Container Documentation** (`/containers/README.md`)
Comprehensive information about Docker containers, deployment strategies, and container management.
### Testing and Evaluation
- **Unit Testing Guide** (`/tests/unit/README.md`)
Instructions for writing, running, and maintaining unit tests.
- **Evaluation Framework** (`/evaluation/README.md`)
Documentation for the evaluation framework, benchmarks, and performance testing.
### Advanced Features
- **Microagents Architecture** (`/microagents/README.md`)
Detailed information about the microagents architecture, implementation, and usage.
### Documentation Standards
- **Documentation Style Guide** (`/docs/DOC_STYLE_GUIDE.md`)
Standards and guidelines for writing and maintaining project documentation.
## Getting Started with Development
If you're new to developing with OpenHands, we recommend following this sequence:
1. Start with the main `README.md` to understand the project's purpose and features
2. Review the `CONTRIBUTING.md` guidelines if you plan to contribute
3. Follow the setup instructions in `Development.md`
4. Dive into specific component documentation based on your area of interest:
- Frontend developers should focus on `/frontend/README.md`
- Backend developers should start with `/openhands/README.md`
- Infrastructure work should begin with `/containers/README.md`
## Documentation Updates
When making changes to the codebase, please ensure that:
1. Relevant documentation is updated to reflect your changes
2. New features are documented in the appropriate README files
3. Any API changes are reflected in the server documentation
4. Documentation follows the style guide in `/docs/DOC_STYLE_GUIDE.md`
+9 -5
View File
@@ -4,7 +4,7 @@ OpenHands provides a Graphical User Interface (GUI) mode for interacting with th
## Installation and Setup
1. Follow the instructions in the [Installation](../installation) guide to install OpenHands.
1. Follow the installation instructions to install OpenHands.
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
## Interacting with the GUI
@@ -21,14 +21,18 @@ OpenHands provides a Graphical User Interface (GUI) mode for interacting with th
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
- **Local Installation**: The user directly inputs their GitHub token.
**Local Installation**: The user directly inputs their GitHub token.
<details>
<summary>Setting Up a GitHub Token</summary>
1. **Generate a Personal Access Token (PAT)**:
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
- Click `Generate new token (classic)`.
- Required scopes:
- **New token (classic)**
- Required scopes:
- `repo` (Full control of private repositories)
- **Fine-Grained Tokens**
- All Repositories (You can select specific repositories, but this will impact what returns in repo search)
- Minimal Permissions ( Select **Meta Data = Read-only** read for search, **Pull Requests = Read and Write**, **Content = Read and Write** for branch creation)
2. **Enter Token in OpenHands**:
- Click the Settings button (gear icon).
- Navigate to the `GitHub Settings` section.
@@ -74,7 +78,7 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it
- Check the browser console for any error messages.
</details>
- **OpenHands Cloud**: The token is obtained through GitHub OAuth authentication.
**OpenHands Cloud**: The token is obtained through GitHub OAuth authentication.
<details>
<summary>OAuth Authentication</summary>
+2 -2
View File
@@ -5,7 +5,7 @@ OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their
## Azure OpenAI Configuration
When running OpenHands, you'll need to set the following environment variable using `-e` in the
[docker run command](/modules/usage/installation#start-the-app):
[docker run command](../installation#running-openhands):
```
LLM_API_VERSION="<api-version>" # e.g. "2023-05-15"
@@ -34,7 +34,7 @@ You will need your ChatGPT deployment name which can be found on the deployments
### Azure OpenAI Configuration
When running OpenHands, set the following environment variable using `-e` in the
[docker run command](/modules/usage/installation#start-the-app):
[docker run command](../installation#running-openhands):
```
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
+1 -1
View File
@@ -16,7 +16,7 @@ If the model is not in the list, toggle `Advanced` options, and enter it in `Cus
## VertexAI - Google Cloud Platform Configs
To use Vertex AI through Google Cloud Platform when running OpenHands, you'll need to set the following environment
variables using `-e` in the [docker run command](/modules/usage/installation#start-the-app):
variables using `-e` in the [docker run command](../installation#running-openhands):
```
GOOGLE_APPLICATION_CREDENTIALS="<json-dump-of-gcp-service-account-json>"
+1 -1
View File
@@ -41,7 +41,7 @@ The following can be set in the OpenHands UI through the Settings:
- `Base URL` (through `Advanced` settings)
There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these
can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app)
can be set through environment variables passed to the docker run command when starting the app
using `-e`:
- `LLM_API_VERSION`
+5
View File
@@ -170,6 +170,11 @@ const sidebars: SidebarsConfig = {
type: 'category',
label: 'For OpenHands Developers',
items: [
{
type: 'doc',
label: 'Development Overview',
id: 'usage/how-to/development-overview',
},
{
type: 'category',
label: 'Architecture',
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.29.0",
"version": "0.29.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.29.0",
"version": "0.29.1",
"dependencies": {
"@heroui/react": "2.7.4",
"@monaco-editor/react": "^4.7.0-rc.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.29.0",
"version": "0.29.1",
"private": true,
"type": "module",
"engines": {
@@ -40,7 +40,7 @@ export function ActionSuggestions({
suggestion={{
label: "Push to Branch",
value:
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request.",
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.",
}}
onClick={(value) => {
posthog.capture("push_to_branch_button_clicked");
@@ -51,7 +51,7 @@ export function ActionSuggestions({
suggestion={{
label: "Push & Create PR",
value:
"Please push the changes to GitHub and open a pull request.",
"Please push the changes to GitHub and open a pull request. Please use the exact SAME branch name as the one you are currently on.",
}}
onClick={(value) => {
posthog.capture("create_pr_button_clicked");
+27 -7
View File
@@ -409,13 +409,33 @@ function AccountSettings() {
}
placeholder={isGitHubTokenSet ? "**********" : ""}
/>
<HelpLink
testId="github-token-help-anchor"
text="Get your token"
linkText="here"
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
/>
<p data-testId="github-token-help-anchor" className="text-xs">
{" "}
Generate a token on{" "}
<b>
{" "}
<a
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
className="underline underline-offset-2"
rel="noopener noreferrer"
>
GitHub
</a>{" "}
</b>
or see the{" "}
<b>
<a
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
target="_blank"
className="underline underline-offset-2"
rel="noopener noreferrer"
>
documentation
</a>
</b>
.
</p>
</>
)}
@@ -20,6 +20,7 @@ from openhands.agenthub.codeact_agent.tools import (
create_cmd_run_tool,
create_str_replace_editor_tool,
)
from openhands.agenthub.codeact_agent.tools.delegate import DelegateTool
from openhands.core.exceptions import (
FunctionCallNotExistsError,
FunctionCallValidationError,
@@ -99,10 +100,18 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
f'Missing required argument "code" in tool call {tool_call.function.name}'
)
action = IPythonRunCellAction(code=arguments['code'])
elif tool_call.function.name == 'delegate_to_browsing_agent':
elif tool_call.function.name == DelegateTool['function']['name']:
if 'agent' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "agent" in tool call {tool_call.function.name}'
)
if 'inputs' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "inputs" in tool call {tool_call.function.name}'
)
action = AgentDelegateAction(
agent='BrowsingAgent',
inputs=arguments,
agent=arguments['agent'],
inputs=arguments['inputs'],
)
# ================================================
@@ -238,6 +247,7 @@ def get_tools(
create_cmd_run_tool(use_simplified_description=use_simplified_tool_desc),
ThinkTool,
FinishTool,
DelegateTool,
]
if codeact_enable_browsing:
tools.append(WebReadTool)
@@ -19,6 +19,7 @@ each of which has a corresponding port:
When starting a web server, use the corresponding ports. You should also
set any options to allow iframes and CORS requests, and allow the server to
be accessed from any host (e.g. 0.0.0.0).
For example, if you are using vite.config.js, you should set server.host to 0.0.0.0, server.port to the port assigned to you, and allowedHosts to the host assigned to you.
{% endif %}
{% if runtime_info.additional_agent_instructions %}
{{ runtime_info.additional_agent_instructions }}
@@ -0,0 +1,23 @@
from litellm import ChatCompletionToolParam
DelegateTool = ChatCompletionToolParam(
type='function',
function={
'name': 'delegate',
'description': 'Delegate a task to another agent.',
'parameters': {
'type': 'object',
'properties': {
'agent': {
'type': 'string',
'description': 'The name of the agent to delegate to.',
},
'inputs': {
'type': 'object',
'description': 'The inputs to pass to the agent.',
},
},
'required': ['agent', 'inputs'],
},
},
)
+39 -25
View File
@@ -4,12 +4,19 @@ import os
import traceback
from typing import Callable, ClassVar, Type
import litellm
from litellm.exceptions import (
import litellm # noqa
from litellm.exceptions import ( # noqa
APIConnectionError,
APIError,
AuthenticationError,
BadRequestError,
ContextWindowExceededError,
InternalServerError,
NotFoundError,
OpenAIError,
RateLimitError,
ServiceUnavailableError,
Timeout,
)
from openhands.controller.agent import Agent
@@ -223,20 +230,20 @@ class AgentController:
await self.set_agent_state_to(AgentState.ERROR)
if self.status_callback is not None:
err_id = ''
if isinstance(e, litellm.AuthenticationError):
if isinstance(e, AuthenticationError):
err_id = 'STATUS$ERROR_LLM_AUTHENTICATION'
elif isinstance(
e,
(
litellm.ServiceUnavailableError,
litellm.APIConnectionError,
litellm.APIError,
ServiceUnavailableError,
APIConnectionError,
APIError,
),
):
err_id = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE'
elif isinstance(e, litellm.InternalServerError):
elif isinstance(e, InternalServerError):
err_id = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR'
elif isinstance(e, litellm.BadRequestError) and 'ExceededBudget' in str(e):
elif isinstance(e, BadRequestError) and 'ExceededBudget' in str(e):
err_id = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
elif isinstance(e, RateLimitError):
await self.set_agent_state_to(AgentState.RATE_LIMITED)
@@ -256,24 +263,30 @@ class AgentController:
f'Traceback: {traceback.format_exc()}',
)
reported = RuntimeError(
'There was an unexpected error while running the agent. Please '
'report this error to the developers by opening an issue at '
'https://github.com/All-Hands-AI/OpenHands. Your session ID is '
f' {self.id}. Error type: {e.__class__.__name__}'
f'There was an unexpected error while running the agent: {e.__class__.__name__}. You can refresh the page or ask the agent to try again.'
)
if (
isinstance(e, litellm.AuthenticationError)
or isinstance(e, litellm.BadRequestError)
isinstance(e, Timeout)
or isinstance(e, APIError)
or isinstance(e, BadRequestError)
or isinstance(e, NotFoundError)
or isinstance(e, InternalServerError)
or isinstance(e, AuthenticationError)
or isinstance(e, RateLimitError)
or isinstance(e, LLMContextWindowExceedError)
):
reported = e
else:
self.log(
'warning',
f'Unknown exception type while running the agent: {type(e).__name__}.',
)
await self._react_to_exception(reported)
def should_step(self, event: Event) -> bool:
"""
Whether the agent should take a step based on an event. In general,
the agent should take a step if it receives a message from the user,
"""Whether the agent should take a step based on an event.
In general, the agent should take a step if it receives a message from the user,
or observes something in the environment (after acting).
"""
# it might be the delegate's day in the sun
@@ -296,7 +309,8 @@ class AgentController:
if (
isinstance(event, NullObservation)
and event.cause is not None
and event.cause > 0
and event.cause
> 0 # NullObservation has cause > 0 (RecallAction), not 0 (user message)
):
return True
if isinstance(event, AgentStateChangedObservation) or isinstance(
@@ -312,7 +326,6 @@ class AgentController:
Args:
event (Event): The incoming event to process.
"""
# If we have a delegate that is not finished or errored, forward events to it
if self.delegate is not None:
delegate_state = self.delegate.get_agent_state()
@@ -469,7 +482,7 @@ class AgentController:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
def _reset(self) -> None:
"""Resets the agent controller"""
"""Resets the agent controller."""
# Runnable actions need an Observation
# make sure there is an Observation with the tool call metadata to be recognized by the agent
# otherwise the pending action is found in history, but it's incomplete without an obs with tool result
@@ -621,7 +634,8 @@ class AgentController:
)
def end_delegate(self) -> None:
"""Ends the currently active delegate (e.g., if it is finished or errored)
"""Ends the currently active delegate (e.g., if it is finished or errored).
so that this controller can resume normal operation.
"""
if self.delegate is None:
@@ -1029,8 +1043,9 @@ class AgentController:
)
def _apply_conversation_window(self, events: list[Event]) -> list[Event]:
"""Cuts history roughly in half when context window is exceeded, preserving action-observation pairs
and ensuring the first user message is always included.
"""Cuts history roughly in half when context window is exceeded.
It preserves action-observation pairs and ensures that the first user message is always included.
The algorithm:
1. Cut history in half
@@ -1183,8 +1198,7 @@ class AgentController:
return False
def _first_user_message(self) -> MessageAction | None:
"""
Get the first user message for this agent.
"""Get the first user message for this agent.
For regular agents, this is the first user message from the beginning (start_id=0).
For delegate agents, this is the first user message after the delegate's start_id.
+24 -4
View File
@@ -102,22 +102,42 @@ class State:
extra_data: dict[str, Any] = field(default_factory=dict)
last_error: str = ''
def save_to_session(self, sid: str, file_store: FileStore):
def save_to_session(self, sid: str, file_store: FileStore, user_id: str | None):
pickled = pickle.dumps(self)
logger.debug(f'Saving state to session {sid}:{self.agent_state}')
encoded = base64.b64encode(pickled).decode('utf-8')
try:
file_store.write(get_conversation_agent_state_filename(sid), encoded)
file_store.write(
get_conversation_agent_state_filename(sid, user_id), encoded
)
# see if state is in old directory. If yes, delete it.
filename = get_conversation_agent_state_filename(sid)
try:
file_store.delete(filename)
except Exception:
pass
except Exception as e:
logger.error(f'Failed to save state to session: {e}')
raise e
@staticmethod
def restore_from_session(sid: str, file_store: FileStore) -> 'State':
def restore_from_session(
sid: str, file_store: FileStore, user_id: str | None = None
) -> 'State':
try:
encoded = file_store.read(get_conversation_agent_state_filename(sid))
encoded = file_store.read(
get_conversation_agent_state_filename(sid, user_id)
)
pickled = base64.b64decode(encoded)
state = pickle.loads(pickled)
except FileNotFoundError:
if user_id:
# see if state is in old directory. If yes, load it.
filename = get_conversation_agent_state_filename(sid)
encoded = file_store.read(filename)
pickled = base64.b64decode(encoded)
state = pickle.loads(pickled)
except Exception as e:
logger.debug(f'Could not restore state from session: {e}')
raise e
+1 -1
View File
@@ -79,7 +79,7 @@ class AppConfig(BaseModel):
runloop_api_key: SecretStr | None = Field(default=None)
daytona_api_key: SecretStr | None = Field(default=None)
daytona_api_url: str = Field(default='https://app.daytona.io/api')
daytona_target: str = Field(default='us')
daytona_target: str = Field(default='eu')
cli_multiline_input: bool = Field(default=False)
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
enable_default_condenser: bool = Field(default=True)
+3 -1
View File
@@ -194,7 +194,9 @@ async def run_controller(
if config.file_store is not None and config.file_store != 'memory':
end_state = controller.get_state()
# NOTE: the saved state does not include delegates events
end_state.save_to_session(event_stream.sid, event_stream.file_store)
end_state.save_to_session(
event_stream.sid, event_stream.file_store, event_stream.user_id
)
await controller.close(set_stop_state=False)
+42 -12
View File
@@ -32,9 +32,11 @@ class EventStreamSubscriber(str, Enum):
TEST = 'test'
async def session_exists(sid: str, file_store: FileStore) -> bool:
async def session_exists(
sid: str, file_store: FileStore, user_id: str | None = None
) -> bool:
try:
await call_sync_from_async(file_store.list, get_conversation_dir(sid))
await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
return True
except FileNotFoundError:
return False
@@ -57,6 +59,7 @@ class AsyncEventStreamWrapper:
class EventStream:
sid: str
user_id: str | None
file_store: FileStore
secrets: dict[str, str]
# For each subscriber ID, there is a map of callback functions - useful
@@ -70,9 +73,10 @@ class EventStream:
_thread_pools: dict[str, dict[str, ThreadPoolExecutor]]
_thread_loops: dict[str, dict[str, asyncio.AbstractEventLoop]]
def __init__(self, sid: str, file_store: FileStore):
def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
self.sid = sid
self.file_store = file_store
self.user_id = user_id
self._stop_flag = threading.Event()
self._queue: queue.Queue[Event] = queue.Queue()
self._thread_pools = {}
@@ -90,10 +94,24 @@ class EventStream:
self.__post_init__()
def __post_init__(self) -> None:
events = []
try:
events = self.file_store.list(get_conversation_events_dir(self.sid))
events_dir = get_conversation_events_dir(self.sid, self.user_id)
events += self.file_store.list(events_dir)
except FileNotFoundError:
logger.debug(f'No events found for session {self.sid}')
logger.debug(f'No events found for session {self.sid} at {events_dir}')
if self.user_id:
# During transition to new location, try old location if user_id is set
# TODO: remove this code after 5/1/2025
try:
events_dir = get_conversation_events_dir(self.sid)
events += self.file_store.list(events_dir)
except FileNotFoundError:
logger.debug(f'No events found for session {self.sid} at {events_dir}')
if not events:
self._cur_id = 0
return
@@ -156,8 +174,8 @@ class EventStream:
del self._subscribers[subscriber_id][callback_id]
def _get_filename_for_id(self, id: int) -> str:
return get_conversation_event_filename(self.sid, id)
def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
return get_conversation_event_filename(self.sid, id, user_id)
@staticmethod
def _get_id_from_filename(filename: str) -> int:
@@ -223,10 +241,20 @@ class EventStream:
event_id += 1
def get_event(self, id: int) -> Event:
filename = self._get_filename_for_id(id)
content = self.file_store.read(filename)
data = json.loads(content)
return event_from_dict(data)
filename = self._get_filename_for_id(id, self.user_id)
try:
content = self.file_store.read(filename)
data = json.loads(content)
return event_from_dict(data)
except FileNotFoundError:
logger.debug(f'File {filename} not found')
# TODO remove this block after 5/1/2025
if self.user_id:
filename = self._get_filename_for_id(id, None)
content = self.file_store.read(filename)
data = json.loads(content)
return event_from_dict(data)
raise
def get_latest_event(self) -> Event:
return self.get_event(self._cur_id - 1)
@@ -277,7 +305,9 @@ class EventStream:
data = self._replace_secrets(data)
event = event_from_dict(data)
if event.id is not None:
self.file_store.write(self._get_filename_for_id(event.id), json.dumps(data))
self.file_store.write(
self._get_filename_for_id(event.id, self.user_id), json.dumps(data)
)
self._queue.put(event)
def set_secrets(self, secrets: dict[str, str]):
+83 -34
View File
@@ -1,6 +1,16 @@
from enum import Enum
from __future__ import annotations
from pydantic import BaseModel, SecretStr, SerializationInfo, field_serializer
from enum import Enum
from types import MappingProxyType
from pydantic import (
BaseModel,
Field,
SecretStr,
SerializationInfo,
field_serializer,
model_validator,
)
from pydantic.json import pydantic_encoder
from openhands.integrations.github.github_service import GithubServiceImpl
@@ -19,43 +29,42 @@ class ProviderType(Enum):
class ProviderToken(BaseModel):
token: SecretStr | None
user_id: str | None
token: SecretStr | None = Field(default=None)
user_id: str | None = Field(default=None)
model_config = {
'frozen': True, # Makes the entire model immutable
'validate_assignment': True,
}
@classmethod
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
"""Factory method to create a ProviderToken from various input types"""
if isinstance(token_value, ProviderToken):
return token_value
elif isinstance(token_value, dict):
token_str = token_value.get('token')
user_id = token_value.get('user_id')
return cls(token=SecretStr(token_str), user_id=user_id)
else:
raise ValueError('Unsupport Provider token type')
PROVIDER_TOKEN_TYPE = dict[ProviderType, ProviderToken]
CUSTOM_SECRETS_TYPE = dict[str, SecretStr]
PROVIDER_TOKEN_TYPE = MappingProxyType[ProviderType, ProviderToken]
CUSTOM_SECRETS_TYPE = MappingProxyType[str, SecretStr]
class SecretStore(BaseModel):
provider_tokens: PROVIDER_TOKEN_TYPE = {}
provider_tokens: PROVIDER_TOKEN_TYPE = Field(
default_factory=lambda: MappingProxyType({})
)
@classmethod
def _convert_token(
cls, token_value: str | ProviderToken | SecretStr
) -> ProviderToken:
if isinstance(token_value, ProviderToken):
return token_value
elif isinstance(token_value, str):
return ProviderToken(token=SecretStr(token_value), user_id=None)
elif isinstance(token_value, SecretStr):
return ProviderToken(token=token_value, user_id=None)
else:
raise ValueError(f'Invalid token type: {type(token_value)}')
def model_post_init(self, __context) -> None:
# Convert any string tokens to ProviderToken objects
converted_tokens = {}
for token_type, token_value in self.provider_tokens.items():
if token_value: # Only convert non-empty tokens
try:
if isinstance(token_type, str):
token_type = ProviderType(token_type)
converted_tokens[token_type] = self._convert_token(token_value)
except ValueError:
# Skip invalid provider types or tokens
continue
self.provider_tokens = converted_tokens
model_config = {
'frozen': True,
'validate_assignment': True,
'arbitrary_types_allowed': True,
}
@field_serializer('provider_tokens')
def provider_tokens_serializer(
@@ -82,6 +91,40 @@ class SecretStore(BaseModel):
return tokens
@model_validator(mode='before')
@classmethod
def convert_dict_to_mappingproxy(
cls, data: dict[str, dict[str, dict[str, str]]] | PROVIDER_TOKEN_TYPE
) -> dict[str, MappingProxyType]:
"""Custom deserializer to convert dictionary into MappingProxyType"""
if not isinstance(data, dict):
raise ValueError('SecretStore must be initialized with a dictionary')
new_data = {}
if 'provider_tokens' in data:
tokens = data['provider_tokens']
if isinstance(
tokens, dict
): # Ensure conversion happens only for dict inputs
converted_tokens = {}
for key, value in tokens.items():
try:
provider_type = (
ProviderType(key) if isinstance(key, str) else key
)
converted_tokens[provider_type] = ProviderToken.from_value(
value
)
except ValueError:
# Skip invalid provider types or tokens
continue
# Convert to MappingProxyType
new_data['provider_tokens'] = MappingProxyType(converted_tokens)
return new_data
class ProviderHandler:
def __init__(
@@ -94,8 +137,14 @@ class ProviderHandler:
ProviderType.GITLAB: GitLabServiceImpl,
}
self.provider_tokens = provider_tokens
# Create immutable copy through SecretStore
self.external_auth_token = external_auth_token
self._provider_tokens = provider_tokens
@property
def provider_tokens(self) -> PROVIDER_TOKEN_TYPE:
"""Read-only access to provider tokens."""
return self._provider_tokens
def _get_service(self, provider: ProviderType) -> GitService:
"""Helper method to instantiate a service for a given provider"""
+44 -2
View File
@@ -31,6 +31,7 @@ from starlette.background import BackgroundTask
from starlette.exceptions import HTTPException as StarletteHTTPException
from uvicorn import run
from openhands.core.exceptions import BrowserUnavailableException
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
Action,
@@ -159,7 +160,9 @@ class ActionExecutor:
self.lock = asyncio.Lock()
self.plugins: dict[str, Plugin] = {}
self.file_editor = OHEditor(workspace_root=self._initial_cwd)
self.browser = BrowserEnv(browsergym_eval_env)
self.browser: BrowserEnv | None = None
self.browser_init_task: asyncio.Task | None = None
self.browsergym_eval_env = browsergym_eval_env
self.start_time = time.time()
self.last_execution_time = self.start_time
self._initialized = False
@@ -183,6 +186,38 @@ class ActionExecutor:
def initial_cwd(self):
return self._initial_cwd
async def _init_browser_async(self):
"""Initialize the browser asynchronously."""
logger.debug('Initializing browser asynchronously')
try:
self.browser = BrowserEnv(self.browsergym_eval_env)
logger.debug('Browser initialized asynchronously')
except Exception as e:
logger.error(f'Failed to initialize browser: {e}')
self.browser = None
async def _ensure_browser_ready(self):
"""Ensure the browser is ready for use."""
if self.browser is None:
if self.browser_init_task is None:
# Start browser initialization if it hasn't been started
self.browser_init_task = asyncio.create_task(self._init_browser_async())
elif self.browser_init_task.done():
# If the task is done but browser is still None, restart initialization
self.browser_init_task = asyncio.create_task(self._init_browser_async())
# Wait for browser to be initialized
if self.browser_init_task:
logger.debug('Waiting for browser to be ready...')
await self.browser_init_task
# Check if browser was successfully initialized
if self.browser is None:
raise BrowserUnavailableException('Browser initialization failed')
# If we get here, the browser is ready
logger.debug('Browser is ready')
async def ainit(self):
# bash needs to be initialized first
logger.debug('Initializing bash session')
@@ -197,6 +232,10 @@ class ActionExecutor:
self.bash_session.initialize()
logger.debug('Bash session initialized')
# Start browser initialization in the background
self.browser_init_task = asyncio.create_task(self._init_browser_async())
logger.debug('Browser initialization started in background')
await wait_all(
(self._init_plugin(plugin) for plugin in self.plugins_to_load),
timeout=30,
@@ -459,16 +498,19 @@ class ActionExecutor:
)
async def browse(self, action: BrowseURLAction) -> Observation:
await self._ensure_browser_ready()
return await browse(action, self.browser)
async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
await self._ensure_browser_ready()
return await browse(action, self.browser)
def close(self):
self.memory_monitor.stop_monitoring()
if self.bash_session is not None:
self.bash_session.close()
self.browser.close()
if self.browser is not None:
self.browser.close()
if __name__ == '__main__':
+35 -7
View File
@@ -132,6 +132,9 @@ class Runtime(FileEditRuntimeMixin):
self.user_id = user_id
# TODO: remove once done debugging expired github token
self.prev_token: SecretStr | None = None
def setup_initial_env(self) -> None:
if self.attach_to_existing:
return
@@ -220,24 +223,49 @@ class Runtime(FileEditRuntimeMixin):
assert event.timeout is not None
try:
if isinstance(event, CmdRunAction):
if self.user_id and '$GITHUB_TOKEN' in event.command:
if self.user_id and 'GITHUB_TOKEN' in event.command:
gh_client = GithubServiceImpl(
external_auth_id=self.user_id, external_token_manager=True
)
logger.info(f'Fetching latest github token for runtime: {self.sid}')
token = await gh_client.get_latest_token()
if token:
export_cmd = CmdRunAction(
f"export GITHUB_TOKEN='{token.get_secret_value()}'"
if not token:
logger.error(
f'Failed to refresh github token for runtime: {self.sid}'
)
if token:
raw_token = token.get_secret_value()
if not self.prev_token:
logger.info(
f'Setting github token in runtime: {self.sid}\nToken value: {raw_token[0:5]}; length: {len(raw_token)}'
)
elif self.prev_token.get_secret_value() != raw_token:
logger.info(
f'Setting new github token in runtime {self.sid}\nToken value: {raw_token[0:5]}; length: {len(raw_token)}'
)
self.prev_token = token
env_vars = {
'GITHUB_TOKEN': raw_token,
}
try:
self.add_env_vars(env_vars)
except Exception as e:
logger.warning(
f'Failed export latest github token to runtime: {self.sid}, {e}'
)
self.event_stream.update_secrets(
{
'github_token': token.get_secret_value(),
'github_token': raw_token,
}
)
await call_sync_from_async(self.run, export_cmd)
observation: Observation = await call_sync_from_async(
self.run_action, event
)
+1 -1
View File
@@ -67,7 +67,7 @@ docker run -it --rm --pull=always \
docker.all-hands.dev/all-hands-ai/openhands:${OPENHANDS_VERSION}
```
> **Tip:** If you don't want your sandboxes to default to the US region, you can set the `DAYTONA_TARGET` environment variable to `eu`
> **Tip:** If you don't want your sandboxes to default to the EU region, you can set the `DAYTONA_TARGET` environment variable to `us`
### Running OpenHands Locally Without Docker
@@ -1,6 +1,7 @@
import json
from typing import Callable
import requests
import tenacity
from daytona_sdk import (
CreateWorkspaceParams,
@@ -17,6 +18,7 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
)
from openhands.runtime.plugins.requirement import PluginRequirement
from openhands.runtime.utils.command import get_action_execution_server_startup_command
from openhands.runtime.utils.request import RequestHTTPError
from openhands.utils.async_utils import call_sync_from_async
from openhands.utils.tenacity_stop import stop_if_should_exit
@@ -111,18 +113,6 @@ class DaytonaRuntime(ActionExecutionClient):
workspace = self.daytona.create(workspace_params)
return workspace
def _get_workspace_status(self) -> str:
assert self.workspace is not None, 'Workspace is not initialized'
assert (
self.workspace.instance.info is not None
), 'Workspace info is not available'
assert (
self.workspace.instance.info.provider_metadata is not None
), 'Provider metadata is not available'
provider_metadata = json.loads(self.workspace.instance.info.provider_metadata)
return provider_metadata.get('status', 'unknown')
def _construct_api_url(self, port: int) -> str:
assert self.workspace is not None, 'Workspace is not initialized'
assert (
@@ -143,10 +133,6 @@ class DaytonaRuntime(ActionExecutionClient):
def _start_action_execution_server(self) -> None:
assert self.workspace is not None, 'Workspace is not initialized'
self.workspace.process.exec(
f'mkdir -p {self.config.workspace_mount_path_in_sandbox}'
)
start_command: list[str] = get_action_execution_server_startup_command(
server_port=self._sandbox_port,
plugins=self.plugins,
@@ -154,7 +140,10 @@ class DaytonaRuntime(ActionExecutionClient):
override_user_id=1000,
override_username='openhands',
)
start_command_str: str = ' '.join(start_command)
start_command_str: str = (
f'mkdir -p {self.config.workspace_mount_path_in_sandbox} && cd /openhands/code && '
+ ' '.join(start_command)
)
self.log(
'debug',
@@ -163,10 +152,6 @@ class DaytonaRuntime(ActionExecutionClient):
exec_session_id = 'action-execution-server'
self.workspace.process.create_session(exec_session_id)
self.workspace.process.execute_session_command(
exec_session_id,
SessionExecuteRequest(command='cd /openhands/code', var_async=True),
)
exec_command = self.workspace.process.execute_session_command(
exec_session_id,
@@ -185,24 +170,33 @@ class DaytonaRuntime(ActionExecutionClient):
async def connect(self):
self.send_status_message('STATUS$STARTING_RUNTIME')
should_start_action_execution_server = False
if self.attach_to_existing:
self.workspace = await call_sync_from_async(self._get_workspace)
else:
should_start_action_execution_server = True
if self.workspace is None:
self.send_status_message('STATUS$PREPARING_CONTAINER')
self.workspace = await call_sync_from_async(self._create_workspace)
self.log('info', f'Created new workspace with id: {self.workspace_id}')
if self._get_workspace_status() == 'stopped':
self.api_url = self._construct_api_url(self._sandbox_port)
state = self.workspace.instance.state
if state == 'stopping':
self.log('info', 'Waiting for Daytona workspace to stop...')
await call_sync_from_async(self.workspace.wait_for_workspace_stop)
state = 'stopped'
if state == 'stopped':
self.log('info', 'Starting Daytona workspace...')
await call_sync_from_async(self.workspace.start)
should_start_action_execution_server = True
self.api_url = await call_sync_from_async(
self._construct_api_url, self._sandbox_port
)
if not self.attach_to_existing:
if should_start_action_execution_server:
await call_sync_from_async(self._start_action_execution_server)
self.log(
'info',
@@ -213,7 +207,7 @@ class DaytonaRuntime(ActionExecutionClient):
self.send_status_message('STATUS$WAITING_FOR_CLIENT')
await call_sync_from_async(self._wait_until_alive)
if not self.attach_to_existing:
if should_start_action_execution_server:
await call_sync_from_async(self.setup_initial_env)
self.log(
@@ -221,10 +215,25 @@ class DaytonaRuntime(ActionExecutionClient):
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}',
)
if not self.attach_to_existing:
if should_start_action_execution_server:
self.send_status_message(' ')
self._runtime_initialized = True
@tenacity.retry(
retry=tenacity.retry_if_exception(
lambda e: (
isinstance(e, requests.HTTPError) or isinstance(e, RequestHTTPError)
)
and hasattr(e, 'response')
and e.response.status_code == 502
),
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
wait=tenacity.wait_fixed(1),
reraise=True,
)
def _send_action_server_request(self, method, url, **kwargs):
return super()._send_action_server_request(method, url, **kwargs)
def close(self):
super().close()
@@ -37,7 +37,9 @@ class ConversationManager(ABC):
"""Clean up the conversation manager."""
@abstractmethod
async def attach_to_conversation(self, sid: str) -> Conversation | None:
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
) -> Conversation | None:
"""Attach to an existing conversation or create a new one."""
@abstractmethod
@@ -63,9 +63,11 @@ class StandaloneConversationManager(ConversationManager):
self._cleanup_task.cancel()
self._cleanup_task = None
async def attach_to_conversation(self, sid: str) -> Conversation | None:
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
) -> Conversation | None:
start_time = time.time()
if not await session_exists(sid, self.file_store):
if not await session_exists(sid, self.file_store, user_id=user_id):
return None
async with self._conversations_lock:
@@ -88,7 +90,9 @@ class StandaloneConversationManager(ConversationManager):
return conversation
# Create new conversation if none exists
c = Conversation(sid, file_store=self.file_store, config=self.config)
c = Conversation(
sid, file_store=self.file_store, config=self.config, user_id=user_id
)
try:
await c.connect()
except AgentRuntimeUnavailableError as e:
@@ -119,7 +123,7 @@ class StandaloneConversationManager(ConversationManager):
)
await self.sio.enter_room(connection_id, ROOM_KEY.format(sid=sid))
self._local_connection_id_to_session_id[connection_id] = sid
event_stream = await self._get_event_stream(sid)
event_stream = await self._get_event_stream(sid, user_id)
if not event_stream:
return await self.maybe_start_agent_loop(
sid, settings, user_id, github_user_id=github_user_id
@@ -299,7 +303,7 @@ class StandaloneConversationManager(ConversationManager):
except ValueError:
pass # Already subscribed - take no action
event_stream = await self._get_event_stream(sid)
event_stream = await self._get_event_stream(sid, user_id)
if not event_stream:
logger.error(
f'No event stream after starting agent loop: {sid}',
@@ -308,7 +312,9 @@ class StandaloneConversationManager(ConversationManager):
raise RuntimeError(f'no_event_stream:{sid}')
return event_stream
async def _get_event_stream(self, sid: str) -> EventStream | None:
async def _get_event_stream(
self, sid: str, user_id: str | None
) -> EventStream | None:
logger.info(f'_get_event_stream:{sid}', extra={'session_id': sid})
session = self._local_agent_loops_by_sid.get(sid)
if session:
+3 -1
View File
@@ -148,7 +148,9 @@ class AttachConversationMiddleware(SessionMiddlewareInterface):
Attach the user's session based on the provided authentication token.
"""
request.state.conversation = (
await shared.conversation_manager.attach_to_conversation(request.state.sid)
await shared.conversation_manager.attach_to_conversation(
request.state.sid, get_user_id(request)
)
)
if not request.state.conversation:
return JSONResponse(
+15 -9
View File
@@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.integrations.provider import ProviderToken, ProviderType, SecretStore
from openhands.integrations.utils import validate_provider_token
from openhands.server.auth import get_provider_tokens, get_user_id
from openhands.server.settings import GETSettingsModel, POSTSettingsModel, Settings
@@ -26,12 +26,11 @@ async def load_settings(request: Request) -> GETSettingsModel | JSONResponse:
github_token_is_set = bool(user_id) or bool(get_provider_tokens(request))
settings_with_token_data = GETSettingsModel(
**settings.model_dump(),
**settings.model_dump(exclude='secrets_store'),
github_token_is_set=github_token_is_set,
)
settings_with_token_data.llm_api_key = settings.llm_api_key
del settings_with_token_data.secrets_store
settings_with_token_data.llm_api_key = settings.llm_api_key
return settings_with_token_data
except Exception as e:
logger.warning(f'Invalid token: {e}')
@@ -90,9 +89,10 @@ async def store_settings(
existing_settings.user_consents_to_analytics
)
# Handle token updates immutably
if settings.unset_github_token:
settings.secrets_store.provider_tokens = {}
settings.provider_tokens = {}
settings = settings.model_copy(update={'secrets_store': SecretStore()})
else: # Only merge if not unsetting tokens
if settings.provider_tokens:
if existing_settings.secrets_store:
@@ -156,16 +156,22 @@ def convert_to_settings(settings_with_token_data: POSTSettingsModel) -> Settings
# Convert the `llm_api_key` to a `SecretStr` instance
filtered_settings_data['llm_api_key'] = settings_with_token_data.llm_api_key
# Create a new Settings instance without provider tokens
# Create a new Settings instance with empty SecretStore
settings = Settings(**filtered_settings_data)
# Update provider tokens if any are provided
# Create new provider tokens immutably
if settings_with_token_data.provider_tokens:
tokens = {}
for token_type, token_value in settings_with_token_data.provider_tokens.items():
if token_value:
provider = ProviderType(token_type)
settings.secrets_store.provider_tokens[provider] = ProviderToken(
tokens[provider] = ProviderToken(
token=SecretStr(token_value), user_id=None
)
# Create new SecretStore with tokens
settings = settings.model_copy(
update={'secrets_store': SecretStore(provider_tokens=tokens)}
)
return settings
+6 -3
View File
@@ -37,6 +37,7 @@ class AgentSession:
"""
sid: str
user_id: str | None
event_stream: EventStream
file_store: FileStore
controller: AgentController | None = None
@@ -63,7 +64,7 @@ class AgentSession:
"""
self.sid = sid
self.event_stream = EventStream(sid, file_store)
self.event_stream = EventStream(sid, file_store, user_id)
self.file_store = file_store
self._status_callback = status_callback
self.user_id = user_id
@@ -186,7 +187,7 @@ class AgentSession:
self.event_stream.close()
if self.controller is not None:
end_state = self.controller.get_state()
end_state.save_to_session(self.sid, self.file_store)
end_state.save_to_session(self.sid, self.file_store, self.user_id)
await self.controller.close()
if self.runtime is not None:
self.runtime.close()
@@ -371,7 +372,9 @@ class AgentSession:
# Use a heuristic to figure out if we should have a state:
# if we have events in the stream.
try:
restored_state = State.restore_from_session(self.sid, self.file_store)
restored_state = State.restore_from_session(
self.sid, self.file_store, self.user_id
)
self.logger.debug(f'Restored state from session, sid: {self.sid}')
except Exception as e:
if self.event_stream.get_latest_event_id() > 0:
+4 -5
View File
@@ -14,17 +14,16 @@ class Conversation:
file_store: FileStore
event_stream: EventStream
runtime: Runtime
user_id: str | None
def __init__(
self,
sid: str,
file_store: FileStore,
config: AppConfig,
self, sid: str, file_store: FileStore, config: AppConfig, user_id: str | None
):
self.sid = sid
self.config = config
self.file_store = file_store
self.event_stream = EventStream(sid, file_store)
self.user_id = user_id
self.event_stream = EventStream(sid, file_store, user_id)
if config.security.security_analyzer:
self.security_analyzer = options.SecurityAnalyzers.get(
config.security.security_analyzer, SecurityAnalyzer
+10
View File
@@ -99,6 +99,16 @@ class Session:
self.config.security.security_analyzer = (
settings.security_analyzer or self.config.security.security_analyzer
)
self.config.sandbox.base_container_image = (
settings.sandbox_base_container_image
or self.config.sandbox.base_container_image
)
self.config.sandbox.runtime_container_image = (
settings.sandbox_runtime_container_image
if settings.sandbox_base_container_image
or settings.sandbox_runtime_container_image
else self.config.sandbox.runtime_container_image
)
max_iterations = settings.max_iterations or self.config.max_iterations
# This is a shallow copy of the default LLM config, so changes here will
+10 -34
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from pydantic import (
BaseModel,
Field,
SecretStr,
SerializationInfo,
field_serializer,
@@ -11,7 +12,7 @@ from pydantic.json import pydantic_encoder
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.utils import load_app_config
from openhands.integrations.provider import ProviderToken, ProviderType, SecretStore
from openhands.integrations.provider import SecretStore
class Settings(BaseModel):
@@ -28,10 +29,16 @@ class Settings(BaseModel):
llm_api_key: SecretStr | None = None
llm_base_url: str | None = None
remote_runtime_resource_factor: int | None = None
secrets_store: SecretStore = SecretStore()
secrets_store: SecretStore = Field(default_factory=SecretStore, frozen=True)
enable_default_condenser: bool = False
enable_sound_notifications: bool = False
user_consents_to_analytics: bool | None = None
sandbox_base_container_image: str | None = None
sandbox_runtime_container_image: str | None = None
model_config = {
'validate_assignment': True,
}
@field_serializer('llm_api_key')
def llm_api_key_serializer(self, llm_api_key: SecretStr, info: SerializationInfo):
@@ -45,23 +52,6 @@ class Settings(BaseModel):
return pydantic_encoder(llm_api_key) if llm_api_key else None
@staticmethod
def _convert_token_value(
token_type: ProviderType, token_value: str | dict
) -> ProviderToken | None:
"""Convert a token value to a ProviderToken object."""
if isinstance(token_value, dict):
token_str = token_value.get('token')
if not token_str:
return None
return ProviderToken(
token=SecretStr(token_str),
user_id=token_value.get('user_id'),
)
if isinstance(token_value, str) and token_value:
return ProviderToken(token=SecretStr(token_value), user_id=None)
return None
@model_validator(mode='before')
@classmethod
def convert_provider_tokens(cls, data: dict | object) -> dict | object:
@@ -77,21 +67,7 @@ class Settings(BaseModel):
if not isinstance(tokens, dict):
return data
converted_tokens = {}
for token_type_str, token_value in tokens.items():
if not token_value:
continue
try:
token_type = ProviderType(token_type_str)
except ValueError:
continue
provider_token = cls._convert_token_value(token_type, token_value)
if provider_token:
converted_tokens[token_type] = provider_token
data['secrets_store'] = SecretStore(provider_tokens=converted_tokens)
data['secrets_store'] = SecretStore(provider_tokens=tokens)
return data
@field_serializer('secrets_store')
+17 -12
View File
@@ -1,25 +1,30 @@
CONVERSATION_BASE_DIR = 'sessions'
def get_conversation_dir(sid: str) -> str:
return f'{CONVERSATION_BASE_DIR}/{sid}/'
def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
if user_id:
return f'users/{user_id}/conversations/{sid}/'
else:
return f'{CONVERSATION_BASE_DIR}/{sid}/'
def get_conversation_events_dir(sid: str) -> str:
return f'{get_conversation_dir(sid)}events/'
def get_conversation_events_dir(sid: str, user_id: str | None = None) -> str:
return f'{get_conversation_dir(sid, user_id)}events/'
def get_conversation_event_filename(sid: str, id: int) -> str:
return f'{get_conversation_events_dir(sid)}{id}.json'
def get_conversation_event_filename(
sid: str, id: int, user_id: str | None = None
) -> str:
return f'{get_conversation_events_dir(sid, user_id)}{id}.json'
def get_conversation_metadata_filename(sid: str) -> str:
return f'{get_conversation_dir(sid)}metadata.json'
def get_conversation_metadata_filename(sid: str, user_id: str | None = None) -> str:
return f'{get_conversation_dir(sid, user_id)}metadata.json'
def get_conversation_init_data_filename(sid: str) -> str:
return f'{get_conversation_dir(sid)}init.json'
def get_conversation_init_data_filename(sid: str, user_id: str | None = None) -> str:
return f'{get_conversation_dir(sid, user_id)}init.json'
def get_conversation_agent_state_filename(sid: str) -> str:
return f'{get_conversation_dir(sid)}agent_state.pkl'
def get_conversation_agent_state_filename(sid: str, user_id: str | None = None) -> str:
return f'{get_conversation_dir(sid, user_id)}agent_state.pkl'
Generated
+114 -107
View File
@@ -496,18 +496,18 @@ files = [
[[package]]
name = "boto3"
version = "1.37.13"
version = "1.37.14"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "boto3-1.37.13-py3-none-any.whl", hash = "sha256:90fa5a91d7d7456219f0b7c4a93b38335dc5cf4613d885da4d4c1d099e04c6b7"},
{file = "boto3-1.37.13.tar.gz", hash = "sha256:295648f887464ab74c5c301a44982df76f9ba39ebfc16be5b8f071ad1a81fe95"},
{file = "boto3-1.37.14-py3-none-any.whl", hash = "sha256:56b4d1e084dbca43d5fdd070f633a84de61a6ce592655b4d239d263d1a0097fc"},
{file = "boto3-1.37.14.tar.gz", hash = "sha256:cf2e5e6d56efd5850db8ce3d9094132e4759cf2d4b5fd8200d69456bf61a20f3"},
]
[package.dependencies]
botocore = ">=1.37.13,<1.38.0"
botocore = ">=1.37.14,<1.38.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.11.0,<0.12.0"
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.37.13"
version = "1.37.14"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "botocore-1.37.13-py3-none-any.whl", hash = "sha256:aa417bac0f4d79533080e6e17c0509e149353aec83cfe7879597a7942f7f08d0"},
{file = "botocore-1.37.13.tar.gz", hash = "sha256:60dfb831c54eb466db9b91891a6c8a0c223626caa049969d5d42858ad1e7f8c7"},
{file = "botocore-1.37.14-py3-none-any.whl", hash = "sha256:709a1796f436f8e378e52170e58501c1f3b5f2d1308238cf1d6a3bdba2e32851"},
{file = "botocore-1.37.14.tar.gz", hash = "sha256:b0adce3f0fb42b914eb05079f50cf368cb9cf9745fdd206bd91fe6ac67b29aca"},
]
[package.dependencies]
@@ -1319,18 +1319,19 @@ urllib3 = ">=1.25.3,<3.0.0"
[[package]]
name = "daytona-sdk"
version = "0.10.2"
version = "0.10.4"
description = "Python SDK for Daytona"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "daytona_sdk-0.10.2-py3-none-any.whl", hash = "sha256:d2dcd314b452273977bd13e3df1656ae9948d32b54412fbacde91eb61a8b1565"},
{file = "daytona_sdk-0.10.2.tar.gz", hash = "sha256:f792b31f177f4494433a4506020c5e282c85a1a045287d219f6ec79ecd4ddfa9"},
{file = "daytona_sdk-0.10.4-py3-none-any.whl", hash = "sha256:c61930f04771924961f02765978fc78c002dfa6ae5506779a740856fa0e6cc81"},
{file = "daytona_sdk-0.10.4.tar.gz", hash = "sha256:072f4cfc6f50e2bc0caae4eb41d22103e920d75ac13bf4b666dd58aa6478ee9f"},
]
[package.dependencies]
daytona_api_client = ">=0.14.0,<1.0.0"
Deprecated = ">=1.2.18,<2.0.0"
environs = ">=9.5.0,<10.0.0"
marshmallow = ">=3.19.0,<4.0.0"
pydantic = ">=2.4.2,<3.0.0"
@@ -3756,100 +3757,106 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "levenshtein"
version = "0.26.1"
version = "0.27.1"
description = "Python extension for computing string edit distances and similarities."
optional = false
python-versions = ">=3.9"
groups = ["testgeneval"]
files = [
{file = "levenshtein-0.26.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8dc4a4aecad538d944a1264c12769c99e3c0bf8e741fc5e454cc954913befb2e"},
{file = "levenshtein-0.26.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec108f368c12b25787c8b1a4537a1452bc53861c3ee4abc810cc74098278edcd"},
{file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69229d651c97ed5b55b7ce92481ed00635cdbb80fbfb282a22636e6945dc52d5"},
{file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79dcd157046d62482a7719b08ba9e3ce9ed3fc5b015af8ea989c734c702aedd4"},
{file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f53f9173ae21b650b4ed8aef1d0ad0c37821f367c221a982f4d2922b3044e0d"},
{file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3956f3c5c229257dbeabe0b6aacd2c083ebcc1e335842a6ff2217fe6cc03b6b"},
{file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1e83af732726987d2c4cd736f415dae8b966ba17b7a2239c8b7ffe70bfb5543"},
{file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4f052c55046c2a9c9b5f742f39e02fa6e8db8039048b8c1c9e9fdd27c8a240a1"},
{file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9895b3a98f6709e293615fde0dcd1bb0982364278fa2072361a1a31b3e388b7a"},
{file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a3777de1d8bfca054465229beed23994f926311ce666f5a392c8859bb2722f16"},
{file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:81c57e1135c38c5e6e3675b5e2077d8a8d3be32bf0a46c57276c092b1dffc697"},
{file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:91d5e7d984891df3eff7ea9fec8cf06fdfacc03cd074fd1a410435706f73b079"},
{file = "levenshtein-0.26.1-cp310-cp310-win32.whl", hash = "sha256:f48abff54054b4142ad03b323e80aa89b1d15cabc48ff49eb7a6ff7621829a56"},
{file = "levenshtein-0.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:79dd6ad799784ea7b23edd56e3bf94b3ca866c4c6dee845658ee75bb4aefdabf"},
{file = "levenshtein-0.26.1-cp310-cp310-win_arm64.whl", hash = "sha256:3351ddb105ef010cc2ce474894c5d213c83dddb7abb96400beaa4926b0b745bd"},
{file = "levenshtein-0.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44c51f5d33b3cfb9db518b36f1288437a509edd82da94c4400f6a681758e0cb6"},
{file = "levenshtein-0.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56b93203e725f9df660e2afe3d26ba07d71871b6d6e05b8b767e688e23dfb076"},
{file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:270d36c5da04a0d89990660aea8542227cbd8f5bc34e9fdfadd34916ff904520"},
{file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:480674c05077eeb0b0f748546d4fcbb386d7c737f9fff0010400da3e8b552942"},
{file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13946e37323728695ba7a22f3345c2e907d23f4600bc700bf9b4352fb0c72a48"},
{file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceb673f572d1d0dc9b1cd75792bb8bad2ae8eb78a7c6721e23a3867d318cb6f2"},
{file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42d6fa242e3b310ce6bfd5af0c83e65ef10b608b885b3bb69863c01fb2fcff98"},
{file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8b68295808893a81e0a1dbc2274c30dd90880f14d23078e8eb4325ee615fc68"},
{file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b01061d377d1944eb67bc40bef5d4d2f762c6ab01598efd9297ce5d0047eb1b5"},
{file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9d12c8390f156745e533d01b30773b9753e41d8bbf8bf9dac4b97628cdf16314"},
{file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:48825c9f967f922061329d1481b70e9fee937fc68322d6979bc623f69f75bc91"},
{file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8ec137170b95736842f99c0e7a9fd8f5641d0c1b63b08ce027198545d983e2b"},
{file = "levenshtein-0.26.1-cp311-cp311-win32.whl", hash = "sha256:798f2b525a2e90562f1ba9da21010dde0d73730e277acaa5c52d2a6364fd3e2a"},
{file = "levenshtein-0.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:55b1024516c59df55f1cf1a8651659a568f2c5929d863d3da1ce8893753153bd"},
{file = "levenshtein-0.26.1-cp311-cp311-win_arm64.whl", hash = "sha256:e52575cbc6b9764ea138a6f82d73d3b1bc685fe62e207ff46a963d4c773799f6"},
{file = "levenshtein-0.26.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc741ca406d3704dc331a69c04b061fc952509a069b79cab8287413f434684bd"},
{file = "levenshtein-0.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:821ace3b4e1c2e02b43cf5dc61aac2ea43bdb39837ac890919c225a2c3f2fea4"},
{file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92694c9396f55d4c91087efacf81297bef152893806fc54c289fc0254b45384"},
{file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51ba374de7a1797d04a14a4f0ad3602d2d71fef4206bb20a6baaa6b6a502da58"},
{file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7aa5c3327dda4ef952769bacec09c09ff5bf426e07fdc94478c37955681885b"},
{file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e2517e8d3c221de2d1183f400aed64211fcfc77077b291ed9f3bb64f141cdc"},
{file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9092b622765c7649dd1d8af0f43354723dd6f4e570ac079ffd90b41033957438"},
{file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc16796c85d7d8b259881d59cc8b5e22e940901928c2ff6924b2c967924e8a0b"},
{file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4370733967f5994ceeed8dc211089bedd45832ee688cecea17bfd35a9eb22b9"},
{file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3535ecfd88c9b283976b5bc61265855f59bba361881e92ed2b5367b6990c93fe"},
{file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:90236e93d98bdfd708883a6767826fafd976dac8af8fc4a0fb423d4fa08e1bf0"},
{file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04b7cabb82edf566b1579b3ed60aac0eec116655af75a3c551fee8754ffce2ea"},
{file = "levenshtein-0.26.1-cp312-cp312-win32.whl", hash = "sha256:ae382af8c76f6d2a040c0d9ca978baf461702ceb3f79a0a3f6da8d596a484c5b"},
{file = "levenshtein-0.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd091209798cfdce53746f5769987b4108fe941c54fb2e058c016ffc47872918"},
{file = "levenshtein-0.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:7e82f2ea44a81ad6b30d92a110e04cd3c8c7c6034b629aca30a3067fa174ae89"},
{file = "levenshtein-0.26.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:790374a9f5d2cbdb30ee780403a62e59bef51453ac020668c1564d1e43438f0e"},
{file = "levenshtein-0.26.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b05c0415c386d00efda83d48db9db68edd02878d6dbc6df01194f12062be1bb"},
{file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3114586032361722ddededf28401ce5baf1cf617f9f49fb86b8766a45a423ff"},
{file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2532f8a13b68bf09f152d906f118a88da2063da22f44c90e904b142b0a53d534"},
{file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:219c30be6aa734bf927188d1208b7d78d202a3eb017b1c5f01ab2034d2d4ccca"},
{file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397e245e77f87836308bd56305bba630010cd8298c34c4c44bd94990cdb3b7b1"},
{file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeff6ea3576f72e26901544c6c55c72a7b79b9983b6f913cba0e9edbf2f87a97"},
{file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a19862e3539a697df722a08793994e334cd12791e8144851e8a1dee95a17ff63"},
{file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dc3b5a64f57c3c078d58b1e447f7d68cad7ae1b23abe689215d03fc434f8f176"},
{file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bb6c7347424a91317c5e1b68041677e4c8ed3e7823b5bbaedb95bffb3c3497ea"},
{file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b817376de4195a207cc0e4ca37754c0e1e1078c2a2d35a6ae502afde87212f9e"},
{file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b50c3620ff47c9887debbb4c154aaaac3e46be7fc2e5789ee8dbe128bce6a17"},
{file = "levenshtein-0.26.1-cp313-cp313-win32.whl", hash = "sha256:9fb859da90262eb474c190b3ca1e61dee83add022c676520f5c05fdd60df902a"},
{file = "levenshtein-0.26.1-cp313-cp313-win_amd64.whl", hash = "sha256:8adcc90e3a5bfb0a463581d85e599d950fe3c2938ac6247b29388b64997f6e2d"},
{file = "levenshtein-0.26.1-cp313-cp313-win_arm64.whl", hash = "sha256:c2599407e029865dc66d210b8804c7768cbdbf60f061d993bb488d5242b0b73e"},
{file = "levenshtein-0.26.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc54ced948fc3feafce8ad4ba4239d8ffc733a0d70e40c0363ac2a7ab2b7251e"},
{file = "levenshtein-0.26.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6516f69213ae393a220e904332f1a6bfc299ba22cf27a6520a1663a08eba0fb"},
{file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4cfea4eada1746d0c75a864bc7e9e63d4a6e987c852d6cec8d9cb0c83afe25b"},
{file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a323161dfeeac6800eb13cfe76a8194aec589cd948bcf1cdc03f66cc3ec26b72"},
{file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c23e749b68ebc9a20b9047317b5cd2053b5856315bc8636037a8adcbb98bed1"},
{file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f80dd7432d4b6cf493d012d22148db7af769017deb31273e43406b1fb7f091c"},
{file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ae7cd6e4312c6ef34b2e273836d18f9fff518d84d823feff5ad7c49668256e0"},
{file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dcdad740e841d791b805421c2b20e859b4ed556396d3063b3aa64cd055be648c"},
{file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e07afb1613d6f5fd99abd4e53ad3b446b4efaa0f0d8e9dfb1d6d1b9f3f884d32"},
{file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f1add8f1d83099a98ae4ac472d896b7e36db48c39d3db25adf12b373823cdeff"},
{file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1010814b1d7a60833a951f2756dfc5c10b61d09976ce96a0edae8fecdfb0ea7c"},
{file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33fa329d1bb65ce85e83ceda281aea31cee9f2f6e167092cea54f922080bcc66"},
{file = "levenshtein-0.26.1-cp39-cp39-win32.whl", hash = "sha256:488a945312f2f16460ab61df5b4beb1ea2254c521668fd142ce6298006296c98"},
{file = "levenshtein-0.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:9f942104adfddd4b336c3997050121328c39479f69de702d7d144abb69ea7ab9"},
{file = "levenshtein-0.26.1-cp39-cp39-win_arm64.whl", hash = "sha256:c1d8f85b2672939f85086ed75effcf768f6077516a3e299c2ba1f91bc4644c22"},
{file = "levenshtein-0.26.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6cf8f1efaf90ca585640c5d418c30b7d66d9ac215cee114593957161f63acde0"},
{file = "levenshtein-0.26.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d5b2953978b8c158dd5cd93af8216a5cfddbf9de66cf5481c2955f44bb20767a"},
{file = "levenshtein-0.26.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b952b3732c4631c49917d4b15d78cb4a2aa006c1d5c12e2a23ba8e18a307a055"},
{file = "levenshtein-0.26.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07227281e12071168e6ae59238918a56d2a0682e529f747b5431664f302c0b42"},
{file = "levenshtein-0.26.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8191241cd8934feaf4d05d0cc0e5e72877cbb17c53bbf8c92af9f1aedaa247e9"},
{file = "levenshtein-0.26.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9e70d7ee157a9b698c73014f6e2b160830e7d2d64d2e342fefc3079af3c356fc"},
{file = "levenshtein-0.26.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0eb3059f826f6cb0a5bca4a85928070f01e8202e7ccafcba94453470f83e49d4"},
{file = "levenshtein-0.26.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:6c389e44da12d6fb1d7ba0a709a32a96c9391e9be4160ccb9269f37e040599ee"},
{file = "levenshtein-0.26.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e9de292f2c51a7d34a0ae23bec05391b8f61f35781cd3e4c6d0533e06250c55"},
{file = "levenshtein-0.26.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d87215113259efdca8716e53b6d59ab6d6009e119d95d45eccc083148855f33"},
{file = "levenshtein-0.26.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f00a3eebf68a82fb651d8d0e810c10bfaa60c555d21dde3ff81350c74fb4c2"},
{file = "levenshtein-0.26.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b3554c1b59de63d05075577380340c185ff41b028e541c0888fddab3c259a2b4"},
{file = "levenshtein-0.26.1.tar.gz", hash = "sha256:0d19ba22330d50609b2349021ec3cf7d905c6fe21195a2d0d876a146e7ed2575"},
{file = "levenshtein-0.27.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13d6f617cb6fe63714c4794861cfaacd398db58a292f930edb7f12aad931dace"},
{file = "levenshtein-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca9d54d41075e130c390e61360bec80f116b62d6ae973aec502e77e921e95334"},
{file = "levenshtein-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1f822b5c9a20d10411f779dfd7181ce3407261436f8470008a98276a9d07f"},
{file = "levenshtein-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81270392c2e45d1a7e1b3047c3a272d5e28bb4f1eff0137637980064948929b7"},
{file = "levenshtein-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d30c3ea23a94dddd56dbe323e1fa8a29ceb24da18e2daa8d0abf78b269a5ad1"},
{file = "levenshtein-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3e0bea76695b9045bbf9ad5f67ad4cc01c11f783368f34760e068f19b6a6bc"},
{file = "levenshtein-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdd190e468a68c31a5943368a5eaf4e130256a8707886d23ab5906a0cb98a43c"},
{file = "levenshtein-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c3121314bb4b676c011c33f6a0ebb462cfdcf378ff383e6f9e4cca5618d0ba7"},
{file = "levenshtein-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f8ef378c873efcc5e978026b69b45342d841cd7a2f273447324f1c687cc4dc37"},
{file = "levenshtein-0.27.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff18d78c5c16bea20876425e1bf5af56c25918fb01bc0f2532db1317d4c0e157"},
{file = "levenshtein-0.27.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:13412ff805afbfe619d070280d1a76eb4198c60c5445cd5478bd4c7055bb3d51"},
{file = "levenshtein-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2adb9f263557f7fb13e19eb2f34595d86929a44c250b2fca6e9b65971e51e20"},
{file = "levenshtein-0.27.1-cp310-cp310-win32.whl", hash = "sha256:6278a33d2e0e909d8829b5a72191419c86dd3bb45b82399c7efc53dabe870c35"},
{file = "levenshtein-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b602b8428ee5dc88432a55c5303a739ee2be7c15175bd67c29476a9d942f48e"},
{file = "levenshtein-0.27.1-cp310-cp310-win_arm64.whl", hash = "sha256:48334081fddaa0c259ba01ee898640a2cf8ede62e5f7e25fefece1c64d34837f"},
{file = "levenshtein-0.27.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6f1760108319a108dceb2f02bc7cdb78807ad1f9c673c95eaa1d0fe5dfcaae"},
{file = "levenshtein-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c4ed8400d94ab348099395e050b8ed9dd6a5d6b5b9e75e78b2b3d0b5f5b10f38"},
{file = "levenshtein-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7826efe51be8ff58bc44a633e022fdd4b9fc07396375a6dbc4945a3bffc7bf8f"},
{file = "levenshtein-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff5afb78719659d353055863c7cb31599fbea6865c0890b2d840ee40214b3ddb"},
{file = "levenshtein-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:201dafd5c004cd52018560cf3213da799534d130cf0e4db839b51f3f06771de0"},
{file = "levenshtein-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ddd59f3cfaec216811ee67544779d9e2d6ed33f79337492a248245d6379e3d"},
{file = "levenshtein-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6afc241d27ecf5b921063b796812c55b0115423ca6fa4827aa4b1581643d0a65"},
{file = "levenshtein-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee2e766277cceb8ca9e584ea03b8dc064449ba588d3e24c1923e4b07576db574"},
{file = "levenshtein-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:920b23d6109453913ce78ec451bc402ff19d020ee8be4722e9d11192ec2fac6f"},
{file = "levenshtein-0.27.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:560d7edba126e2eea3ac3f2f12e7bd8bc9c6904089d12b5b23b6dfa98810b209"},
{file = "levenshtein-0.27.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8d5362b6c7aa4896dc0cb1e7470a4ad3c06124e0af055dda30d81d3c5549346b"},
{file = "levenshtein-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:65ba880815b0f80a80a293aeebac0fab8069d03ad2d6f967a886063458f9d7a1"},
{file = "levenshtein-0.27.1-cp311-cp311-win32.whl", hash = "sha256:fcc08effe77fec0bc5b0f6f10ff20b9802b961c4a69047b5499f383119ddbe24"},
{file = "levenshtein-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:0ed402d8902be7df212ac598fc189f9b2d520817fdbc6a05e2ce44f7f3ef6857"},
{file = "levenshtein-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:7fdaab29af81a8eb981043737f42450efca64b9761ca29385487b29c506da5b5"},
{file = "levenshtein-0.27.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25fb540d8c55d1dc7bdc59b7de518ea5ed9df92eb2077e74bcb9bb6de7b06f69"},
{file = "levenshtein-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f09cfab6387e9c908c7b37961c045e8e10eb9b7ec4a700367f8e080ee803a562"},
{file = "levenshtein-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dafa29c0e616f322b574e0b2aeb5b1ff2f8d9a1a6550f22321f3bd9bb81036e3"},
{file = "levenshtein-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be7a7642ea64392fa1e6ef7968c2e50ef2152c60948f95d0793361ed97cf8a6f"},
{file = "levenshtein-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:060b48c45ed54bcea9582ce79c6365b20a1a7473767e0b3d6be712fa3a22929c"},
{file = "levenshtein-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:712f562c5e64dd0398d3570fe99f8fbb88acec7cc431f101cb66c9d22d74c542"},
{file = "levenshtein-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6141ad65cab49aa4527a3342d76c30c48adb2393b6cdfeca65caae8d25cb4b8"},
{file = "levenshtein-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:799b8d73cda3265331116f62932f553804eae16c706ceb35aaf16fc2a704791b"},
{file = "levenshtein-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ec99871d98e517e1cc4a15659c62d6ea63ee5a2d72c5ddbebd7bae8b9e2670c8"},
{file = "levenshtein-0.27.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8799164e1f83588dbdde07f728ea80796ea72196ea23484d78d891470241b222"},
{file = "levenshtein-0.27.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:583943813898326516ab451a83f734c6f07488cda5c361676150d3e3e8b47927"},
{file = "levenshtein-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bb22956af44bb4eade93546bf95be610c8939b9a9d4d28b2dfa94abf454fed7"},
{file = "levenshtein-0.27.1-cp312-cp312-win32.whl", hash = "sha256:d9099ed1bcfa7ccc5540e8ad27b5dc6f23d16addcbe21fdd82af6440f4ed2b6d"},
{file = "levenshtein-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:7f071ecdb50aa6c15fd8ae5bcb67e9da46ba1df7bba7c6bf6803a54c7a41fd96"},
{file = "levenshtein-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:83b9033a984ccace7703f35b688f3907d55490182fd39b33a8e434d7b2e249e6"},
{file = "levenshtein-0.27.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ab00c2cae2889166afb7e1af64af2d4e8c1b126f3902d13ef3740df00e54032d"},
{file = "levenshtein-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c27e00bc7527e282f7c437817081df8da4eb7054e7ef9055b851fa3947896560"},
{file = "levenshtein-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5b07de42bfc051136cc8e7f1e7ba2cb73666aa0429930f4218efabfdc5837ad"},
{file = "levenshtein-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb11ad3c9dae3063405aa50d9c96923722ab17bb606c776b6817d70b51fd7e07"},
{file = "levenshtein-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c5986fb46cb0c063305fd45b0a79924abf2959a6d984bbac2b511d3ab259f3f"},
{file = "levenshtein-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75191e469269ddef2859bc64c4a8cfd6c9e063302766b5cb7e1e67f38cc7051a"},
{file = "levenshtein-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b3a7b2266933babc04e4d9821a495142eebd6ef709f90e24bc532b52b81385"},
{file = "levenshtein-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbac509794afc3e2a9e73284c9e3d0aab5b1d928643f42b172969c3eefa1f2a3"},
{file = "levenshtein-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d68714785178347ecb272b94e85cbf7e638165895c4dd17ab57e7742d8872ec"},
{file = "levenshtein-0.27.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8ee74ee31a5ab8f61cd6c6c6e9ade4488dde1285f3c12207afc018393c9b8d14"},
{file = "levenshtein-0.27.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f2441b6365453ec89640b85344afd3d602b0d9972840b693508074c613486ce7"},
{file = "levenshtein-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a9be39640a46d8a0f9be729e641651d16a62b2c07d3f4468c36e1cc66b0183b9"},
{file = "levenshtein-0.27.1-cp313-cp313-win32.whl", hash = "sha256:a520af67d976761eb6580e7c026a07eb8f74f910f17ce60e98d6e492a1f126c7"},
{file = "levenshtein-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:7dd60aa49c2d8d23e0ef6452c8329029f5d092f386a177e3385d315cabb78f2a"},
{file = "levenshtein-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:149cd4f0baf5884ac5df625b7b0d281721b15de00f447080e38f5188106e1167"},
{file = "levenshtein-0.27.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c9231ac7c705a689f12f4fc70286fa698b9c9f06091fcb0daddb245e9259cbe"},
{file = "levenshtein-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf9ba080b1a8659d35c11dcfffc7f8c001028c2a3a7b7e6832348cdd60c53329"},
{file = "levenshtein-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:164e3184385caca94ef7da49d373edd7fb52d4253bcc5bd5b780213dae307dfb"},
{file = "levenshtein-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6024d67de6efbd32aaaafd964864c7fee0569b960556de326c3619d1eeb2ba4"},
{file = "levenshtein-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb234b3b04e04f7b3a2f678e24fd873c86c543d541e9df3ac9ec1cc809e732"},
{file = "levenshtein-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffdd9056c7afb29aea00b85acdb93a3524e43852b934ebb9126c901506d7a1ed"},
{file = "levenshtein-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1a0918243a313f481f4ba6a61f35767c1230395a187caeecf0be87a7c8f0624"},
{file = "levenshtein-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c57655b20690ffa5168df7f4b7c6207c4ca917b700fb1b142a49749eb1cf37bb"},
{file = "levenshtein-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:079cc78de05d3ded6cf1c5e2c3eadeb1232e12d49be7d5824d66c92b28c3555a"},
{file = "levenshtein-0.27.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ac28c4ced134c0fe2941230ce4fd5c423aa66339e735321665fb9ae970f03a32"},
{file = "levenshtein-0.27.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a2f7688355b22db27588f53c922b4583b8b627c83a8340191bbae1fbbc0f5f56"},
{file = "levenshtein-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:654e8f016cb64ad27263d3364c6536e7644205f20d94748c8b94c586e3362a23"},
{file = "levenshtein-0.27.1-cp39-cp39-win32.whl", hash = "sha256:145e6e8744643a3764fed9ab4ab9d3e2b8e5f05d2bcd0ad7df6f22f27a9fbcd4"},
{file = "levenshtein-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:612f0c90201c318dd113e7e97bd677e6e3e27eb740f242b7ae1a83f13c892b7e"},
{file = "levenshtein-0.27.1-cp39-cp39-win_arm64.whl", hash = "sha256:cde09ec5b3cc84a6737113b47e45392b331c136a9e8a8ead8626f3eacae936f8"},
{file = "levenshtein-0.27.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c92a222ab95b8d903eae6d5e7d51fe6c999be021b647715c18d04d0b0880f463"},
{file = "levenshtein-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:71afc36b4ee950fa1140aff22ffda9e5e23280285858e1303260dbb2eabf342d"},
{file = "levenshtein-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b1daeebfc148a571f09cfe18c16911ea1eaaa9e51065c5f7e7acbc4b866afa"},
{file = "levenshtein-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:105edcb14797d95c77f69bad23104314715a64cafbf4b0e79d354a33d7b54d8d"},
{file = "levenshtein-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c58fb1ef8bdc8773d705fbacf628e12c3bb63ee4d065dda18a76e86042444a"},
{file = "levenshtein-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e52270591854af67217103955a36bd7436b57c801e3354e73ba44d689ed93697"},
{file = "levenshtein-0.27.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:909b7b6bce27a4ec90576c9a9bd9af5a41308dfecf364b410e80b58038277bbe"},
{file = "levenshtein-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d193a7f97b8c6a350e36ec58e41a627c06fa4157c3ce4b2b11d90cfc3c2ebb8f"},
{file = "levenshtein-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:614be316e3c06118705fae1f717f9072d35108e5fd4e66a7dd0e80356135340b"},
{file = "levenshtein-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31fc0a5bb070722bdabb6f7e14955a294a4a968c68202d294699817f21545d22"},
{file = "levenshtein-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9415aa5257227af543be65768a80c7a75e266c3c818468ce6914812f88f9c3df"},
{file = "levenshtein-0.27.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7987ef006a3cf56a4532bd4c90c2d3b7b4ca9ad3bf8ae1ee5713c4a3bdfda913"},
{file = "levenshtein-0.27.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e67750653459a8567b5bb10e56e7069b83428d42ff5f306be821ef033b92d1a8"},
{file = "levenshtein-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:93344c2c3812f21fdc46bd9e57171684fc53dd107dae2f648d65ea6225d5ceaf"},
{file = "levenshtein-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4baef7e7460691006dd2ca6b9e371aecf135130f72fddfe1620ae740b68d94"},
{file = "levenshtein-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8141c8e5bf2bd76ae214c348ba382045d7ed9d0e7ce060a36fc59c6af4b41d48"},
{file = "levenshtein-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:773aa120be48c71e25c08d92a2108786e6537a24081049664463715926c76b86"},
{file = "levenshtein-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f12a99138fb09eb5606ab9de61dd234dd82a7babba8f227b5dce0e3ae3a9eaf4"},
{file = "levenshtein-0.27.1.tar.gz", hash = "sha256:3e18b73564cfc846eec94dd13fab6cb006b5d2e0cc56bad1fd7d5585881302e3"},
]
[package.dependencies]
@@ -4367,14 +4374,14 @@ files = [
[[package]]
name = "modal"
version = "0.73.110"
version = "0.73.112"
description = "Python client library for Modal"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
files = [
{file = "modal-0.73.110-py3-none-any.whl", hash = "sha256:5ccdf9ce6e5fbf953738670819a63f02059b65333e270a3fd19a9230b8a6d505"},
{file = "modal-0.73.110.tar.gz", hash = "sha256:d4110c223c975ddd4adbe9e2b9040c4cdbf6dd20625343d1e839b3f1881b33a8"},
{file = "modal-0.73.112-py3-none-any.whl", hash = "sha256:2845b54c6c3920f6d9e23785212295951fe39ba63eb78a8517371cc3bccc7285"},
{file = "modal-0.73.112.tar.gz", hash = "sha256:71947a78c7db41aa942e1f62c4cd6a448f59027263dac75844d68bcd1ba53459"},
]
[package.dependencies]
@@ -6335,18 +6342,18 @@ dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_list
[[package]]
name = "python-levenshtein"
version = "0.26.1"
version = "0.27.1"
description = "Python extension for computing string edit distances and similarities."
optional = false
python-versions = ">=3.9"
groups = ["testgeneval"]
files = [
{file = "python_Levenshtein-0.26.1-py3-none-any.whl", hash = "sha256:8ef5e529dd640fb00f05ee62d998d2ee862f19566b641ace775d5ae16167b2ef"},
{file = "python_levenshtein-0.26.1.tar.gz", hash = "sha256:24ba578e28058ebb4afa2700057e1678d7adf27e43cd1f17700c09a9009d5d3a"},
{file = "python_levenshtein-0.27.1-py3-none-any.whl", hash = "sha256:e1a4bc2a70284b2ebc4c505646142fecd0f831e49aa04ed972995895aec57396"},
{file = "python_levenshtein-0.27.1.tar.gz", hash = "sha256:3a5314a011016d373d309a68e875fd029caaa692ad3f32e78319299648045f11"},
]
[package.dependencies]
Levenshtein = "0.26.1"
Levenshtein = "0.27.1"
[[package]]
name = "python-multipart"
@@ -9309,4 +9316,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "31c10902e2e52ca3ef7e3b0c7239f1ffa65f68a51fabaaa6b175124318a51d7b"
content-hash = "d3ec6b8a6c7e48420d76b7e17d5f1a3f253fa603205f90d4a8e4a614ab5e2c67"
+5 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openhands-ai"
version = "0.29.0"
version = "0.29.1"
description = "OpenHands: Code Less, Make More"
authors = ["OpenHands"]
license = "MIT"
@@ -76,7 +76,7 @@ stripe = "^11.5.0"
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"
memory-profiler = "^0.61.0"
daytona-sdk = "0.10.2"
daytona-sdk = "0.10.4"
python-json-logger = "^3.2.1"
[tool.poetry.group.dev.dependencies]
@@ -99,6 +99,7 @@ reportlab = "*"
[tool.coverage.run]
concurrency = ["gevent"]
[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
@@ -127,6 +128,7 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"
@@ -156,5 +158,5 @@ openhands = "openhands.core.cli:main"
[tool.poetry.group.testgeneval.dependencies]
fuzzywuzzy = "^0.18.0"
rouge = "^1.0.1"
python-levenshtein = "^0.26.1"
python-levenshtein = ">=0.26.1,<0.28.0"
tree-sitter-python = "^0.23.6"
+339
View File
@@ -0,0 +1,339 @@
import asyncio
import os
import pytest
from litellm.types.utils import ModelResponse
from openhands.agenthub.browsing_agent.browsing_agent import BrowsingAgent
from openhands.agenthub.codeact_agent.codeact_agent import CodeActAgent
from openhands.controller.agent_controller import AgentController
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig, LLMConfig
from openhands.core.message import Message, TextContent
from openhands.events import EventSource, EventStream
from openhands.events.action import (
AgentDelegateAction,
AgentFinishAction,
MessageAction,
)
from openhands.llm.llm import LLM
from openhands.llm.metrics import Metrics
from openhands.storage.memory import InMemoryFileStore
class MockLLM(LLM):
"""Base class for mock LLMs used in testing."""
def __init__(self, config: LLMConfig, completion_response: dict):
super().__init__(config)
self._completion_response = completion_response
self._function_calling_active = True
self.metrics = Metrics()
def _completion(self, **kwargs) -> dict:
return self._completion_response
def vision_is_active(self) -> bool:
return False
def is_caching_prompt_active(self) -> bool:
return False
def format_messages_for_llm(self, messages: list) -> list:
return messages
def _post_completion(self, response: ModelResponse) -> float:
return 0.0
@pytest.fixture
def mock_llm():
"""Creates a mock LLM for testing."""
completion_response = {
'choices': [
{
'message': {
'role': 'assistant',
'content': "I'll help with that task.",
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'delegate',
'arguments': '{"agent": "BrowsingAgent", "inputs": {"task": "search for OpenHands repository"}}',
},
}
],
}
}
]
}
return MockLLM(LLMConfig(), completion_response)
@pytest.fixture
def mock_browsing_llm():
"""Creates a mock LLM for the browsing agent."""
completion_response = {
'choices': [
{
'message': {
'role': 'assistant',
'content': "I've completed the search task.",
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'finish',
'arguments': '{"message": "Found the repository at github.com/All-Hands-AI/OpenHands", "task_completed": "true"}',
},
}
],
}
}
]
}
return MockLLM(LLMConfig(), completion_response)
@pytest.fixture
def mock_writer_llm():
"""Creates a mock LLM for the writer CodeAct agent."""
completion_response = {
'choices': [
{
'message': {
'role': 'assistant',
'content': "I'll help with that task.",
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'delegate',
'arguments': '{"agent": "CodeActAgent", "inputs": {"task": "analyze the code in /workspace/example.py"}}',
},
}
],
}
}
]
}
return MockLLM(LLMConfig(), completion_response)
@pytest.fixture
def mock_reader_llm():
"""Creates a mock LLM for the reader CodeAct agent."""
completion_response = {
'choices': [
{
'message': {
'role': 'assistant',
'content': "I've analyzed the code.",
'tool_calls': [
{
'id': 'call_1',
'type': 'function',
'function': {
'name': 'finish',
'arguments': '{"message": "The code has been analyzed. It contains a simple function.", "task_completed": "true"}',
},
}
],
}
}
]
}
return MockLLM(LLMConfig(), completion_response)
@pytest.mark.asyncio
async def test_codeact_to_browsing_delegation(mock_llm, mock_browsing_llm):
"""
Test delegation from CodeAct agent to BrowsingAgent.
This test verifies that:
1. CodeAct agent can delegate tasks to BrowsingAgent
2. BrowsingAgent can receive and process the delegated task
3. The delegation flow works end-to-end with proper state management
"""
# Setup event stream
sid = 'test-delegation'
file_store = InMemoryFileStore({})
event_stream = EventStream(sid=sid, file_store=file_store)
# Create parent CodeAct agent
parent_config = AgentConfig()
parent_config.codeact_enable_browsing = (
True # Enable browsing to allow delegation to BrowsingAgent
)
parent_agent = CodeActAgent(mock_llm, parent_config)
parent_state = State(max_iterations=10)
parent_controller = AgentController(
agent=parent_agent,
event_stream=event_stream,
max_iterations=10,
sid='parent',
confirmation_mode=False,
headless_mode=True,
initial_state=parent_state,
)
# Create child BrowsingAgent
child_config = AgentConfig()
child_agent = BrowsingAgent(mock_browsing_llm, child_config)
child_state = State(max_iterations=10)
# Note: We don't need to store the child_controller since it's managed by the parent's delegate
AgentController(
agent=child_agent,
event_stream=event_stream,
max_iterations=10,
sid='child',
confirmation_mode=False,
headless_mode=True,
initial_state=child_state,
)
# Simulate a user message to trigger delegation
message = Message(
role='user',
content=[TextContent(text='Please search for the OpenHands repository')],
)
message_action = MessageAction(content=message.content[0].text)
message_action._source = EventSource.USER
# Process the message
await parent_controller._on_event(message_action)
await asyncio.sleep(0.5) # Give time for processing
# Verify delegation occurred
events = list(event_stream.get_events())
delegate_actions = [e for e in events if isinstance(e, AgentDelegateAction)]
assert len(delegate_actions) == 1, 'Expected one delegation action'
delegate_action = delegate_actions[0]
assert delegate_action.agent == 'BrowsingAgent'
assert 'search' in str(delegate_action.inputs)
# Verify parent has a delegate controller
assert parent_controller.delegate is not None
assert parent_controller.delegate.agent.name == 'BrowsingAgent'
# Let the child agent process its task
child_message = Message(
role='user', content=[TextContent(text=str(delegate_action.inputs))]
)
child_message_action = MessageAction(content=child_message.content[0].text)
child_message_action._source = EventSource.USER
await parent_controller.delegate._on_event(child_message_action)
await asyncio.sleep(0.5)
# Verify child completed its task
events = list(event_stream.get_events())
finish_actions = [e for e in events if isinstance(e, AgentFinishAction)]
assert len(finish_actions) == 1, 'Expected one finish action'
# Verify parent's delegate is cleared after child finishes
assert parent_controller.delegate is None
# Cleanup
await parent_controller.close()
@pytest.mark.asyncio
async def test_codeact_to_codeact_delegation(mock_writer_llm, mock_reader_llm):
"""
Test delegation between two CodeAct agents, where one is in read-only mode.
This test verifies that:
1. A CodeAct agent can delegate tasks to another CodeAct agent
2. The reader CodeAct agent can operate in read-only mode
3. The delegation flow works end-to-end with proper state management
"""
# Setup event stream
sid = 'test-codeact-delegation'
file_store = InMemoryFileStore({})
event_stream = EventStream(sid=sid, file_store=file_store)
# Create example.py for testing
os.makedirs('/workspace', exist_ok=True)
with open('/workspace/example.py', 'w') as f:
f.write('def hello():\n print("Hello, World!")\n')
# Create parent CodeAct agent with full capabilities
parent_config = AgentConfig()
parent_config.codeact_enable_jupyter = True
parent_config.codeact_enable_llm_editor = True
parent_config.codeact_enable_browsing = True # Enable browsing to allow delegation
parent_agent = CodeActAgent(mock_writer_llm, parent_config)
parent_state = State(max_iterations=10)
parent_controller = AgentController(
agent=parent_agent,
event_stream=event_stream,
max_iterations=10,
sid='parent',
confirmation_mode=False,
headless_mode=True,
initial_state=parent_state,
)
# Create child CodeAct agent in read-only mode
child_config = AgentConfig()
child_config.codeact_enable_jupyter = True # Enable Python execution
child_config.codeact_enable_llm_editor = False # Disable file editing
child_agent = CodeActAgent(mock_reader_llm, child_config)
child_state = State(max_iterations=10)
# Note: We don't need to store the child_controller since it's managed by the parent's delegate
AgentController(
agent=child_agent,
event_stream=event_stream,
max_iterations=10,
sid='child',
confirmation_mode=False,
headless_mode=True,
initial_state=child_state,
)
# Simulate a user message to trigger delegation
message = Message(
role='user', content=[TextContent(text='Please analyze the code in example.py')]
)
message_action = MessageAction(content=message.content[0].text)
message_action._source = EventSource.USER
# Process the message
await parent_controller._on_event(message_action)
await asyncio.sleep(0.5) # Give time for processing
# Verify delegation occurred
events = list(event_stream.get_events())
delegate_actions = [e for e in events if isinstance(e, AgentDelegateAction)]
assert len(delegate_actions) == 1, 'Expected one delegation action'
delegate_action = delegate_actions[0]
assert delegate_action.agent == 'CodeActAgent'
assert 'analyze' in str(delegate_action.inputs)
# Verify parent has a delegate controller
assert parent_controller.delegate is not None
assert parent_controller.delegate.agent.name == 'CodeActAgent'
# Let the child agent process its task
child_message = Message(
role='user', content=[TextContent(text=str(delegate_action.inputs))]
)
child_message_action = MessageAction(content=child_message.content[0].text)
child_message_action._source = EventSource.USER
await parent_controller.delegate._on_event(child_message_action)
await asyncio.sleep(0.5)
# Verify child completed its task
events = list(event_stream.get_events())
finish_actions = [e for e in events if isinstance(e, AgentFinishAction)]
assert len(finish_actions) == 1, 'Expected one finish action'
# Verify parent's delegate is cleared after child finishes
assert parent_controller.delegate is None
# Cleanup
await parent_controller.close()
os.remove('/workspace/example.py')
+115
View File
@@ -20,6 +20,7 @@ from openhands.events.observation import (
ErrorObservation,
)
from openhands.events.observation.agent import RecallObservation
from openhands.events.observation.empty import NullObservation
from openhands.events.serialization import event_to_dict
from openhands.llm import LLM
from openhands.llm.metrics import Metrics, TokenUsage
@@ -1038,3 +1039,117 @@ async def test_first_user_message_with_identical_content():
) # This should be False, but may be True if there's a bug
await controller.close()
async def test_agent_controller_processes_null_observation_with_cause():
"""Test that AgentController processes NullObservation events with a cause value.
And that the agent's step method is called as a result.
"""
# Create an in-memory file store and real event stream
file_store = InMemoryFileStore()
event_stream = EventStream(sid='test-session', file_store=file_store)
# Create a Memory instance - not used directly in this test but needed for setup
Memory(event_stream=event_stream, sid='test-session')
# Create a mock agent with necessary attributes
mock_agent = MagicMock(spec=Agent)
mock_agent.llm = MagicMock(spec=LLM)
mock_agent.llm.metrics = Metrics()
mock_agent.llm.config = AppConfig().get_llm_config()
# Create a controller with the mock agent
controller = AgentController(
agent=mock_agent,
event_stream=event_stream,
max_iterations=10,
sid='test-session',
)
# Patch the controller's step method to track calls
with patch.object(controller, 'step') as mock_step:
# Create and add the first user message (will have ID 0)
user_message = MessageAction(content='First user message')
user_message._source = EventSource.USER # type: ignore[attr-defined]
event_stream.add_event(user_message, EventSource.USER)
# Give it a little time to process
await asyncio.sleep(0.3)
# Get all events from the stream
events = list(event_stream.get_events())
# Events in the stream:
# Event 0: MessageAction, ID: 0, Cause: None, Source: EventSource.USER, Content: First user message
# Event 1: RecallAction, ID: 1, Cause: None, Source: EventSource.USER, Content: N/A
# Event 2: NullObservation, ID: 2, Cause: 1, Source: EventSource.ENVIRONMENT, Content:
# Event 3: AgentStateChangedObservation, ID: 3, Cause: None, Source: EventSource.ENVIRONMENT, Content:
# Find the RecallAction event (should be automatically created)
recall_actions = [event for event in events if isinstance(event, RecallAction)]
assert len(recall_actions) > 0, 'No RecallAction was created'
recall_action = recall_actions[0]
# Find any NullObservation events
null_obs_events = [
event for event in events if isinstance(event, NullObservation)
]
assert len(null_obs_events) > 0, 'No NullObservation was created'
null_observation = null_obs_events[0]
# Verify the NullObservation has a cause that points to the RecallAction
assert null_observation.cause is not None, 'NullObservation cause is None'
assert (
null_observation.cause == recall_action.id
), f'Expected cause={recall_action.id}, got cause={null_observation.cause}'
# Verify the controller's should_step method returns True for this observation
assert controller.should_step(
null_observation
), 'should_step should return True for this NullObservation'
# Verify the controller's step method was called
# This means the controller processed the NullObservation
assert mock_step.called, "Controller's step method was not called"
# Now test with a NullObservation that has cause=0
# Create a NullObservation with cause = 0 (pointing to the first user message)
null_observation_zero = NullObservation(content='Test observation with cause=0')
null_observation_zero._cause = 0 # type: ignore[attr-defined]
# Verify the controller's should_step method would return False for this observation
assert not controller.should_step(
null_observation_zero
), 'should_step should return False for NullObservation with cause=0'
def test_agent_controller_should_step_with_null_observation_cause_zero():
"""Test that AgentController's should_step method returns False for NullObservation with cause = 0."""
# Create a mock event stream
file_store = InMemoryFileStore()
event_stream = EventStream(sid='test-session', file_store=file_store)
# Create a mock agent
mock_agent = MagicMock(spec=Agent)
# Create an agent controller
controller = AgentController(
agent=mock_agent,
event_stream=event_stream,
max_iterations=10,
sid='test-session',
)
# Create a NullObservation with cause = 0
# This should not happen, but if it does, the controller shouldn't step.
null_observation = NullObservation(content='Test observation')
null_observation._cause = 0 # type: ignore[attr-defined]
# Check if should_step returns False for this observation
result = controller.should_step(null_observation)
# It should return False since we only want to step on NullObservation with cause > 0
assert (
result is False
), 'should_step should return False for NullObservation with cause = 0'
-1
View File
@@ -61,7 +61,6 @@ def prompt_dir(tmp_path):
@pytest.mark.asyncio
async def test_memory_on_event_exception_handling(memory, event_stream):
"""Test that exceptions in Memory.on_event are properly handled via status callback."""
# Create a dummy agent for the controller
agent = MagicMock(spec=Agent)
agent.llm = MagicMock(spec=LLM)
+229
View File
@@ -0,0 +1,229 @@
from types import MappingProxyType
import pytest
from pydantic import SecretStr, ValidationError
from openhands.integrations.provider import (
ProviderHandler,
ProviderToken,
ProviderType,
SecretStore,
)
from openhands.server.routes.settings import convert_to_settings
from openhands.server.settings import POSTSettingsModel, Settings
def test_provider_token_immutability():
"""Test that ProviderToken is immutable"""
token = ProviderToken(token=SecretStr('test'), user_id='user1')
# Test direct attribute modification
with pytest.raises(ValidationError):
token.token = SecretStr('new')
with pytest.raises(ValidationError):
token.user_id = 'new_user'
# Test that __setattr__ is blocked
with pytest.raises(ValidationError):
setattr(token, 'token', SecretStr('new'))
# Verify original values are unchanged
assert token.token.get_secret_value() == 'test'
assert token.user_id == 'user1'
def test_secret_store_immutability():
"""Test that SecretStore is immutable"""
store = SecretStore(
provider_tokens={ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))}
)
# Test direct attribute modification
with pytest.raises(ValidationError):
store.provider_tokens = {}
# Test dictionary mutation attempts
with pytest.raises((TypeError, AttributeError)):
store.provider_tokens[ProviderType.GITHUB] = ProviderToken(
token=SecretStr('new')
)
with pytest.raises((TypeError, AttributeError)):
store.provider_tokens.clear()
with pytest.raises((TypeError, AttributeError)):
store.provider_tokens.update(
{ProviderType.GITLAB: ProviderToken(token=SecretStr('test'))}
)
# Test nested immutability
github_token = store.provider_tokens[ProviderType.GITHUB]
with pytest.raises(ValidationError):
github_token.token = SecretStr('new')
# Verify original values are unchanged
assert store.provider_tokens[ProviderType.GITHUB].token.get_secret_value() == 'test'
def test_settings_immutability():
"""Test that Settings secrets_store is immutable"""
settings = Settings(
secrets_store=SecretStore(
provider_tokens={
ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))
}
)
)
# Test direct modification of secrets_store
with pytest.raises(ValidationError):
settings.secrets_store = SecretStore()
# Test nested modification attempts
with pytest.raises((TypeError, AttributeError)):
settings.secrets_store.provider_tokens[ProviderType.GITHUB] = ProviderToken(
token=SecretStr('new')
)
# Test model_copy creates new instance
new_store = SecretStore(
provider_tokens={
ProviderType.GITHUB: ProviderToken(token=SecretStr('new_token'))
}
)
new_settings = settings.model_copy(update={'secrets_store': new_store})
# Verify original is unchanged and new has updated values
assert (
settings.secrets_store.provider_tokens[
ProviderType.GITHUB
].token.get_secret_value()
== 'test'
)
assert (
new_settings.secrets_store.provider_tokens[
ProviderType.GITHUB
].token.get_secret_value()
== 'new_token'
)
with pytest.raises(ValidationError):
new_settings.secrets_store.provider_tokens[
ProviderType.GITHUB
].token = SecretStr('')
def test_post_settings_conversion():
"""Test that POSTSettingsModel correctly converts to Settings"""
# Create POST model with token data
post_data = POSTSettingsModel(
provider_tokens={'github': 'test_token', 'gitlab': 'gitlab_token'}
)
# Convert to settings using convert_to_settings function
settings = convert_to_settings(post_data)
# Verify tokens were converted correctly
assert (
settings.secrets_store.provider_tokens[
ProviderType.GITHUB
].token.get_secret_value()
== 'test_token'
)
assert (
settings.secrets_store.provider_tokens[
ProviderType.GITLAB
].token.get_secret_value()
== 'gitlab_token'
)
assert settings.secrets_store.provider_tokens[ProviderType.GITLAB].user_id is None
# Verify immutability of converted settings
with pytest.raises(ValidationError):
settings.secrets_store = SecretStore()
def test_provider_handler_immutability():
"""Test that ProviderHandler maintains token immutability"""
# Create initial tokens
tokens = MappingProxyType(
{ProviderType.GITHUB: ProviderToken(token=SecretStr('test'))}
)
handler = ProviderHandler(provider_tokens=tokens)
# Try to modify tokens (should raise TypeError due to frozen dict)
with pytest.raises((TypeError, AttributeError)):
handler.provider_tokens[ProviderType.GITHUB] = ProviderToken(
token=SecretStr('new')
)
# Try to modify the handler's tokens property
with pytest.raises((ValidationError, TypeError, AttributeError)):
handler.provider_tokens = {}
# Original token should be unchanged
assert (
handler.provider_tokens[ProviderType.GITHUB].token.get_secret_value() == 'test'
)
def test_token_conversion():
"""Test token conversion in SecretStore.create"""
# Test with string token
store1 = Settings(
secrets_store=SecretStore(
provider_tokens={
ProviderType.GITHUB: ProviderToken(token=SecretStr('test_token'))
}
)
)
assert (
store1.secrets_store.provider_tokens[
ProviderType.GITHUB
].token.get_secret_value()
== 'test_token'
)
assert store1.secrets_store.provider_tokens[ProviderType.GITHUB].user_id is None
# Test with dict token
store2 = SecretStore(
provider_tokens={'github': {'token': 'test_token', 'user_id': 'user1'}}
)
assert (
store2.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
== 'test_token'
)
assert store2.provider_tokens[ProviderType.GITHUB].user_id == 'user1'
# Test with ProviderToken
token = ProviderToken(token=SecretStr('test_token'), user_id='user2')
store3 = SecretStore(provider_tokens={ProviderType.GITHUB: token})
assert (
store3.provider_tokens[ProviderType.GITHUB].token.get_secret_value()
== 'test_token'
)
assert store3.provider_tokens[ProviderType.GITHUB].user_id == 'user2'
store4 = SecretStore(
provider_tokens={
ProviderType.GITHUB: 123 # Invalid type
}
)
assert ProviderType.GITHUB not in store4.provider_tokens
# Test with empty/None token
store5 = SecretStore(provider_tokens={ProviderType.GITHUB: None})
assert ProviderType.GITHUB not in store5.provider_tokens
store6 = SecretStore(
provider_tokens={
'invalid_provider': 'test_token' # Invalid provider type
}
)
assert len(store6.provider_tokens.keys()) == 0
+9 -5
View File
@@ -6,7 +6,7 @@ from openhands.core.config.app_config import AppConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.integrations.provider import ProviderToken, ProviderType, SecretStore
from openhands.server.routes.settings import convert_to_settings
from openhands.server.settings import POSTSettingsModel, Settings
@@ -81,10 +81,14 @@ def test_settings_handles_sensitive_data():
llm_api_key='test-key',
llm_base_url='https://test.example.com',
remote_runtime_resource_factor=2,
)
settings.secrets_store.provider_tokens[ProviderType.GITHUB] = ProviderToken(
token=SecretStr('test-token'),
user_id=None,
secrets_store=SecretStore(
provider_tokens={
ProviderType.GITHUB: ProviderToken(
token=SecretStr('test-token'),
user_id=None,
)
}
),
)
assert str(settings.llm_api_key) == '**********'