mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9abb05b59 | |||
| c1e08dc548 | |||
| 4c62e76e5e | |||
| 34f65cb7c5 | |||
| 9e42e4bff1 | |||
| e4ccd4057d | |||
| c3d60b31d1 | |||
| 35b70ca915 | |||
| a8d65c11e0 | |||
| a4746a53d8 | |||
| 13bb474623 | |||
| 09aa62f1c3 | |||
| cbc26a5e40 | |||
| 6824d14ed8 | |||
| d9e40f721c | |||
| 8a73184801 | |||
| 06e8c4dad4 | |||
| e2521743b6 | |||
| f2a54f4e23 | |||
| a594595fea | |||
| 0620679d11 | |||
| 78708efbf1 | |||
| cf06f20a0e | |||
| c68fba01a8 | |||
| f2c7f8a6da | |||
| 259140ffc9 | |||
| 3150af1ad7 |
@@ -46,7 +46,7 @@ on:
|
||||
|
||||
jobs:
|
||||
del_runs:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
+32
-34
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+31
-33
@@ -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`
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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',
|
||||
|
||||
Generated
+2
-2
@@ -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,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");
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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]):
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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) == '**********'
|
||||
|
||||
Reference in New Issue
Block a user