Compare commits

..

15 Commits

Author SHA1 Message Date
openhands
11def95da0 CLI: Implement /clear command to start new conversations
- Modified /clear command to create new conversation and runner instances instead of just clearing screen
- Updated /new command to match /clear functionality for consistency
- Updated help text: '/clear': 'Start a new conversation from scratch'
- Added error handling for conversation setup failures
- Added test to verify /clear command description is correct
- Applied code formatting with ruff

Fixes #11121

Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-25 16:01:29 +00:00
Xingyao Wang
27512ee72c v1 cli: provide information on CWD (#11108)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-09-25 11:11:00 +08:00
Rohit Malhotra
8a50164c45 CLI(V1): risk based security analyzer (#11079)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-09-24 15:11:40 -04:00
Rohit Malhotra
1c54f333c5 Chore: Merge latest main to V1 (#11106)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Mislav Lukach <mislavlukach@gmail.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: tksrmz <38581613+tksrmz@users.noreply.github.com>
Co-authored-by: Kaushik Ashodiya <kashodiya@gmail.com>
Co-authored-by: Eliot Jones <eliot.k.jones@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Alona <alona@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: juanmichelini <juan@juan.com.uy>
Co-authored-by: Xinyi He <52363993+Betty1202@users.noreply.github.com>
Co-authored-by: BenYao21 <cyao22@asu.edu>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tejas Goyal <83608316+tejas-goyal@users.noreply.github.com>
Co-authored-by: Tejas Goyal <tejas@Tejass-MacBook-Pro.local>
2025-09-24 14:33:05 -04:00
Rohit Malhotra
e6ddf09897 Fix CLI directory separation and bash tool spec configuration (#11070)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 16:09:42 -04:00
Rohit Malhotra
d9f311a398 CLI(V1): advanced settings (#10991)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-09-22 12:19:44 -04:00
Rohit Malhotra
f3d74ab807 Port test improvements from OpenHands-CLI PR #48 (#10976)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 15:27:06 -04:00
Rohit Malhotra
6dbbf76231 CLI(V1): binary speedup (#11006)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 10:19:07 -07:00
Rohit Malhotra
1231b78aea CLI(V1): Profiler (#11007) 2025-09-17 13:16:16 -07:00
Rohit Malhotra
9003f40096 CLI(V1): update agent sdk sha (#10994) 2025-09-16 18:22:34 -07:00
Rohit Malhotra
f70f649745 CLI(V1): Pattern for settings screen + persistence (#10979)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-16 09:27:58 -07:00
Rohit Malhotra
7939bd694b CLI(V1: update agent state handling (#10975)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-16 06:17:27 +08:00
Rohit Malhotra
916bb85244 CLI(V1): Visualize LLM settings (#10962) 2025-09-12 16:36:02 -04:00
Rohit Malhotra
4ef1dde5f6 CLI(V1): Update agent-sdk sha (#10923) 2025-09-10 17:16:46 -04:00
Rohit Malhotra
cf982e0134 Refactor(V1): OpenHands CLI + Agent SDK (#10905)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 21:51:55 +08:00
280 changed files with 11100 additions and 2577 deletions

58
.github/workflows/cli-build-test.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
# Workflow that builds and tests the CLI binary executable
name: CLI - Build and Test Binary
# Run on pushes to main branch and all pull requests, but only when CLI files change
on:
push:
branches:
- main
paths:
- "openhands-cli/**"
pull_request:
paths:
- "openhands-cli/**"
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
build-and-test-binary:
name: Build and test binary executable
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: openhands-cli
run: |
uv sync
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
echo "Full output:"
cat output.log
if grep -q "❌" output.log; then
echo "❌ Found failure marker in output"
exit 1
fi
echo "✅ Build & test finished without ❌ markers"

View File

@@ -46,7 +46,6 @@ jobs:
else
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ghcr.io/all-hands-ai/python-nodejs:python3.13-nodejs22-trixie", tag: "trixie" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
]')
fi
@@ -137,7 +136,6 @@ jobs:
if: github.event.pull_request.head.repo.fork != true
shell: bash
run: |
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} -t ${{ matrix.base_image.tag }} --dry
DOCKER_BUILD_JSON=$(jq -c . < docker-build-dry.json)
@@ -213,8 +211,6 @@ jobs:
latest=auto
prefix=
suffix=
env:
DOCKER_METADATA_PR_HEAD_SHA: true
- name: Determine app image tag
shell: bash
run: |

View File

@@ -37,7 +37,7 @@ jobs:
npm run make-i18n && tsc
npm run check-translation-completeness
# Run lint on the python code
# Run lint on the python code (excluding CLI and enterprise)
lint-python:
name: Lint python
runs-on: blacksmith-4vcpu-ubuntu-2204
@@ -73,6 +73,24 @@ jobs:
working-directory: ./enterprise
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
lint-cli-python:
name: Lint CLI python
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up python
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: "pip"
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Run pre-commit hooks
working-directory: ./openhands-cli
run: pre-commit run --all-files --config ../dev_config/python/.pre-commit-config.yaml
# Check version consistency across documentation
check-version-consistency:
name: Check version consistency

View File

@@ -19,16 +19,12 @@ jobs:
# Run python tests on Linux
test-on-linux:
name: Python Tests on Linux
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2204
env:
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ["3.12"]
permissions:
# For coverage comment and python-coverage-comment-action branch
pull-requests: write
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
@@ -52,21 +48,10 @@ jobs:
- name: Build Environment
run: make build
- name: Run Unit Tests
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -s ./tests/unit --cov=openhands --cov-branch
env:
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv ./tests/unit
- name: Run Runtime Tests with CLIRuntime
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
env:
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-openhands
path: |
.coverage.${{ matrix.python_version }}
.coverage.runtime.${{ matrix.python_version }}
include-hidden-files: true
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
# Run specific Windows python tests
test-on-windows:
name: Python Tests on Windows
@@ -100,7 +85,7 @@ jobs:
DEBUG: "1"
test-enterprise:
name: Enterprise Python Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
python-version: ["3.12"]
@@ -117,37 +102,35 @@ jobs:
working-directory: ./enterprise
run: poetry install --with dev,test
- name: Run Unit Tests
# Use base working directory for coverage paths to line up.
run: PYTHONPATH=".:$PYTHONPATH" poetry run --project=enterprise pytest --forked -n auto -s -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./enterprise/tests/unit --cov=enterprise --cov-branch
env:
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-enterprise
path: ".coverage.enterprise.${{ matrix.python_version }}"
include-hidden-files: true
coverage-comment:
name: Coverage Comment
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [test-on-linux, test-enterprise]
working-directory: ./enterprise
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./tests/unit
permissions:
pull-requests: write
contents: write
# Run CLI unit tests
test-cli-python:
name: CLI Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v5
id: download
- name: Checkout repository
uses: actions/checkout@v4
with:
pattern: coverage-*
merge-multiple: true
fetch-depth: 0
- name: Coverage comment
id: coverage_comment
uses: py-cov-action/python-coverage-comment-action@v3
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
GITHUB_TOKEN: ${{ github.token }}
MERGE_COVERAGE_FILES: true
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: ./openhands-cli
run: |
uv sync --group dev
- name: Run CLI unit tests
working-directory: ./openhands-cli
run: |
uv run pytest -v

View File

@@ -1,7 +1,7 @@
# Publishes the OpenHands PyPi package
name: Publish PyPi Package
# Triggered manually
on:
workflow_dispatch:
inputs:
@@ -9,9 +9,6 @@ on:
description: 'Reason for manual trigger'
required: true
default: ''
push:
tags:
- "*"
jobs:
release:

3
.gitignore vendored
View File

@@ -31,7 +31,8 @@ requirements.txt
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Note: openhands-cli.spec is intentionally tracked for CLI builds
# *.spec
# Installer logs
pip-log.txt

View File

@@ -113,19 +113,19 @@ individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
### Slack Etiquettes
### Slack and Discord Etiquettes
These Slack etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Lets work together to build a supportive and welcoming community!
These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Lets work together to build a supportive and welcoming community!
- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be difficult to interpret in text.
- Use threads for specific discussions to keep channels organized and easier to follow.
- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize disruptions.
- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions.
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions, [discord - #general](https://discord.com/channels/1222935860639563850/1222935861386018885)).
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to provide context.
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the matter is sensitive or private.
- Always adhere to [our standards](https://github.com/All-Hands-AI/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to alert you only when “LLMs” appears in messages.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to alert you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that best describes your need.
## Attribution

View File

@@ -12,6 +12,7 @@
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
@@ -43,6 +44,8 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
![App screenshot](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $20 in free credits for new users.
@@ -100,7 +103,7 @@ docker run -it --rm --pull=always \
### Getting Started
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude Sonnet 4.5](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-5-20250929`)
[Anthropic's Claude Sonnet 4](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-20250514`)
works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
@@ -137,9 +140,10 @@ troubleshooting resources, and advanced configuration options.
## 🤝 How to Join the Community
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Github:
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://dub.sh/openhands) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md).

148
README_CN.md Normal file
View File

@@ -0,0 +1,148 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: 少写代码,多做事</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
<hr>
</div>
欢迎使用OpenHands前身为OpenDevin这是一个由AI驱动的软件开发代理平台。
OpenHands代理可以完成人类开发者能做的任何事情修改代码、运行命令、浏览网页、调用API甚至从StackOverflow复制代码片段。
在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息,或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
> [!IMPORTANT]
> 在工作中使用OpenHands我们很想与您交流填写
> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
![应用截图](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上,
新用户可获得$50的免费额度。
## 💻 在本地运行OpenHands
OpenHands也可以使用Docker在本地系统上运行。
查看[运行OpenHands](https://docs.all-hands.dev/usage/installation)指南了解
系统要求和更多信息。
> [!WARNING]
> 在公共网络上?请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
> 通过限制网络绑定和实施其他安全措施来保护您的部署。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
打开应用程序时您将被要求选择一个LLM提供商并添加API密钥。
[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)`anthropic/claude-sonnet-4-20250514`
效果最佳,但您还有[许多选择](https://docs.all-hands.dev/usage/llms)。
## 💡 运行OpenHands的其他方式
> [!CAUTION]
> OpenHands旨在由单个用户在其本地工作站上运行。
> 它不适合多租户部署,即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
>
> 如果您有兴趣在多租户环境中运行OpenHands
> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 了解高级部署选项。
您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem)
以可编程的[无头模式](https://docs.all-hands.dev/usage/how-to/headless-mode)运行OpenHands
通过[友好的CLI](https://docs.all-hands.dev/usage/how-to/cli-mode)与其交互,
或使用[GitHub Action](https://docs.all-hands.dev/usage/how-to/github-action)在标记的问题上运行它。
访问[运行OpenHands](https://docs.all-hands.dev/usage/installation)获取更多信息和设置说明。
如果您想修改OpenHands源代码请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
遇到问题?[故障排除指南](https://docs.all-hands.dev/usage/troubleshooting)可以提供帮助。
## 📖 文档
<a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
要了解有关项目的更多信息以及使用OpenHands的技巧
请查看我们的[文档](https://docs.all-hands.dev/usage/getting-started)。
在那里您将找到有关如何使用不同LLM提供商、
故障排除资源和高级配置选项的资源。
## 🤝 如何加入社区
OpenHands是一个社区驱动的项目我们欢迎每个人的贡献。我们大部分沟通
通过Slack进行因此这是开始的最佳场所但我们也很乐意您通过Discord或Github与我们联系
- [加入我们的Slack工作空间](https://dub.sh/openhands) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息,或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
## 📈 进展
在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图每月月底在维护者会议上更新
<p align="center">
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
</a>
</p>
## 📜 许可证
根据MIT许可证分发。有关更多信息请参阅[`LICENSE`](./LICENSE)。
## 🙏 致谢
OpenHands由大量贡献者构建每一份贡献都备受感谢我们还借鉴了其他开源项目对他们的工作深表感谢。
有关OpenHands中使用的开源项目和许可证列表请参阅我们的[CREDITS.md](./CREDITS.md)文件。
## 📚 引用
```
@misc{openhands,
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
year={2024},
eprint={2407.16741},
archivePrefix={arXiv},
primaryClass={cs.SE},
url={https://arxiv.org/abs/2407.16741},
}
```

60
README_JA.md Normal file
View File

@@ -0,0 +1,60 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: コードを減らして、もっと作ろう</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Discordコミュニティに参加"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="クレジット"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="ドキュメントを見る"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv論文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="評価ベンチマークスコア"></a>
<hr>
</div>
OpenHands旧OpenDevinへようこそ。これはAIが駆動するソフトウェア開発エージェントのプラットフォームです。
OpenHandsのエージェントは人間の開発者ができることは何でもこなします。コードを修正し、コマンドを実行し、ウェブを閲覧し、APIを呼び出し、StackOverflowからコードスニペットをコピーすることさえできます。
詳細は[docs.all-hands.dev](https://docs.all-hands.dev)をご覧いただくか、[OpenHands Cloud](https://app.all-hands.dev)に登録して始めましょう。
> [!IMPORTANT]
> 仕事でOpenHandsを使っていますかぜひお話を聞かせてください。[こちらの短いフォーム](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)にご記入いただき、Design Partnerプログラムにご参加ください。商用機能の早期アクセスや製品ロードマップへのフィードバックの機会を提供します。
![アプリのスクリーンショット](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
OpenHandsを始める最も簡単な方法は[OpenHands Cloud](https://app.all-hands.dev)を利用することです。新規ユーザーには50ドル分の無料クレジットが付与されます。
## 💻 OpenHandsをローカルで実行する
OpenHandsはDockerを利用してローカル環境でも実行できます。システム要件や詳細については[Running OpenHands](https://docs.all-hands.dev/usage/installation)ガイドをご覧ください。
> [!WARNING]
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
OpenHandsは[http://localhost:3000](http://localhost:3000)で起動します!

View File

@@ -1,5 +1,5 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:24.8-trixie-slim AS frontend-builder
FROM node:24.3.0-bookworm-slim AS frontend-builder
WORKDIR /app
@@ -9,7 +9,7 @@ RUN npm ci
COPY frontend ./
RUN npm run build
FROM python:3.13.7-slim-trixie AS base
FROM python:3.12.10-slim AS base
FROM base AS backend-builder
WORKDIR /app

View File

@@ -3,9 +3,9 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
- id: end-of-file-fixer
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
@@ -28,12 +28,12 @@ repos:
entry: ruff check --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
args: [--fix, --unsafe-fixes]
exclude: ^(third_party/|enterprise/)
exclude: ^(third_party/|enterprise/|openhands-cli/)
# Run the formatter.
- id: ruff-format
entry: ruff format --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
exclude: ^(third_party/|enterprise/)
exclude: ^(third_party/|enterprise/|openhands-cli/)
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0

View File

@@ -31,7 +31,6 @@
"group": "OpenHands Cloud",
"pages": [
"usage/cloud/openhands-cloud",
"usage/cloud/pro-subscription",
{
"group": "Integrations",
"pages": [
@@ -110,7 +109,8 @@
},
"usage/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup"
"usage/search-engine-setup",
"usage/mcp"
]
}
]
@@ -118,13 +118,7 @@
{
"group": "Customizations & Settings",
"pages": [
{
"group": "OpenHands Settings",
"pages": [
"usage/settings/secrets-settings",
"usage/settings/mcp-settings"
]
},
"usage/common-settings",
"usage/prompting/repository",
{
"group": "Microagents",

BIN
docs/static/img/api-key-generation.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 144 KiB

BIN
docs/static/img/screenshot.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

View File

@@ -8,21 +8,9 @@ description: This guide walks you through the process of installing OpenHands Cl
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a Bitbucket account](/usage/cloud/openhands-cloud).
## Adding Bitbucket Repository Access
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
## Working With Bitbucket Repos in Openhands Cloud
After signing in with a Bitbucket account, use the `Open Repository` section to select the appropriate repository and
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
## IP Whitelisting
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow
OpenHands to access your repositories:
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow OpenHands to access your repositories:
### Core App IP
```
@@ -43,6 +31,17 @@ OpenHands to access your repositories:
34.60.55.59
```
## Adding Bitbucket Repository Access
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
## Working With Bitbucket Repos in Openhands Cloud
After signing in with a Bitbucket account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo-no-github.png)
## Next Steps
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).

View File

@@ -12,10 +12,13 @@ For the available API endpoints, refer to the
To use the OpenHands Cloud API, you'll need to generate an API key:
1. Log in to your [OpenHands Cloud](https://app.all-hands.dev) account.
2. Navigate to the [Settings > API Keys](https://app.all-hands.dev/settings/api-keys) page.
3. Click `Create API Key`.
4. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
5. Copy the generated API key and store it securely. It will only be shown once.
2. Navigate to the [Settings page](https://app.all-hands.dev/settings).
3. Select the `API Keys` tab.
4. Click `Create API Key`.
5. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
6. Copy the generated API key and store it securely. It will only be shown once.
![API Key Generation](/static/img/api-key-generation.png)
## API Usage

View File

@@ -8,39 +8,24 @@ description: The Cloud UI provides a web interface for interacting with OpenHand
The landing page is where you can:
- [Add GitHub repository access](/usage/cloud/github-installation#adding-github-repository-access) to OpenHands.
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud),
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) or
[a Bitbucket repo](/usage/cloud/bitbucket-installation#working-with-bitbucket-repos-in-openhands-cloud) to start working on.
- Launch an empty conversation using `New Conversation`.
- See `Suggested Tasks` for repositories that OpenHands has access to.
- See your `Recent Conversations`.
- Launch an empty conversation using `Launch from Scratch`.
## Settings
Settings are divided across tabs, with each tab focusing on a specific area of configuration.
The Settings page allows you to:
- `User`
- Change your email address.
- `Integrations`
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
- `Application`
- Set your preferred language, notifications and other preferences.
- Toggle task suggestions on GitHub.
- Toggle Solvability Analysis.
- Set a maximum budget per conversation.
- Configure the username and email that OpenHands uses for commits.
- `LLM` (Available for [Pro subscription users](/usage/cloud/pro-subscription))
- Choose to use another LLM or use different models from the OpenHands provider.
- `Billing`
- Add credits for using the OpenHands provider.
- Cancel your `Pro subscription`.
- `Secrets`
- [Manage secrets](/usage/settings/secrets-settings).
- `API Keys`
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
- `MCP`
- [Setup an MCP server](/usage/settings/mcp-settings)
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
- Set application settings like your preferred language, notifications and other preferences.
- Add credits to your account.
- [Generate custom secrets](/usage/common-settings#secrets-management).
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
- Change your email address.
## Key Features

View File

@@ -12,7 +12,7 @@ description: This guide walks you through the process of installing OpenHands Cl
You can grant OpenHands access to specific GitHub repositories:
1. Click on `+ Add GitHub Repos` in the repository selection dropdown.
1. Click on `Add GitHub repos` on the landing page.
2. Select your organization and choose the specific repositories to grant OpenHands access to.
<Accordion title="OpenHands permissions">
- OpenHands requests short-lived tokens (8-hour expiration) with these permissions:
@@ -34,22 +34,20 @@ You can grant OpenHands access to specific GitHub repositories:
## Modifying Repository Access
You can modify GitHub repository access at any time by:
- Selecting `+ Add GitHub Repos` in the repository selection dropdown or
- Visiting the `Settings > Integrations` page and selecting `Configure GitHub Repositories`
- Selecting `Add GitHub repos` on the landing page or
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Integrations` tab
## Working With GitHub Repos in Openhands Cloud
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the
`Open Repository` section to select the appropriate repository and branch you'd like OpenHands to work on. Then click
on `Launch` to start the conversation!
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the `select a repo`
and `select a branch` dropdowns to select the appropriate repository and branch you'd like OpenHands to work on. Then
click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
## Working on GitHub Issues and Pull Requests Using Openhands
## Working on Github Issues and Pull Requests Using Openhands
To allow OpenHands to work directly from GitHub directly, you must
[give OpenHands access to your repository](/usage/cloud/github-installation#modifying-repository-access). Once access is
given, you can use OpenHands by labeling the issue or by tagging `@openhands`.
Giving GitHub repository access to OpenHands also allows you to work on GitHub issues and pull requests directly.
### Working with Issues
@@ -66,12 +64,7 @@ To get OpenHands to work on pull requests, mention `@openhands` in the comments
- Request updates
- Get code explanations
<Note>
The `@openhands` mention functionality in pull requests only works if the pull request is both
*to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate
permissions to access both repositories.
</Note>
**Important Note**: The `@openhands` mention functionality in pull requests only works if the pull request is both *to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate permissions to access both repositories.
## Next Steps

View File

@@ -14,17 +14,16 @@ Upon signing into OpenHands Cloud with a GitLab account, OpenHands will have acc
## Working With GitLab Repos in Openhands Cloud
After signing in with a Gitlab account, use the `Open Repository` section to select the appropriate repository and
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
After signing in with a Gitlab account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
![Connect Repo](/static/img/connect-repo-no-github.png)
## Using Tokens with Reduced Scopes
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
To restrict the agent's permissions, [you can define a custom secret](/usage/settings/secrets-settings) `GITLAB_TOKEN`,
which will override the default token assigned to the agent. While the high-permission API token is still requested
and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
## Working on GitLab Issues and Merge Requests Using Openhands
@@ -33,8 +32,7 @@ This feature works for personal projects and is available for group projects wit
[Premium or Ultimate tier subscription](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).
A webhook is automatically installed within a few minutes after the owner/maintainer of the project or group logs into
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but
we are planning to improve this in a future release.
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but we are planning to improve this in a future release.
</Note>
Giving GitLab repository access to OpenHands also allows you to work on GitLab issues and merge requests directly.

View File

@@ -1,48 +0,0 @@
---
title: "Pro Subscription"
description: "Learn about OpenHands Cloud Pro Subscription features and pricing"
---
The OpenHands Pro Subscription unlocks additional features and better pricing when you run OpenHands conversations in
OpenHands Cloud.
## Base Features
All users start on the Pay-as-you-go plan and have access to these base features when they sign up:
* **Run multiple OpenHands conversations on OpenHands Cloud runtimes.**
* **API keys to the OpenHands LLM provider for use in OpenHands CLI or when running OpenHands on your own**
* **$20 in initial OpenHands Cloud credits to get started.**
* **Support for GitHub, GitLab, Bitbucket, Slack, and more.**
## What you get with a Pro Subscription
The $20/month Pro Subscription covers the cost of runtime compute in OpenHands Cloud, plus enables the following
features:
* **Bring Your Own LLM Keys:** Bring your own API keys from OpenAI, Anthropic, Mistral, and other providers.
* **Model Choice:** Unlocks access to OpenHands LLM provider models for use within OpenHands Cloud.
* **No Markup Pricing on LLM usage:** When you use the OpenHands LLM provider in OpenHands Cloud, you pay for
LLM usage at-cost (zero markup) based on API prices.
## Plan Comparison
Here are the key differences between Pay-as-you-go and Pro subscriptions:
### When running OpenHands conversations in OpenHands Cloud
| | Pay-as-you-go | Pro Subscription |
| :---- | ----- | ----- |
| Monthly price | None \- no commitment | $20/month |
| Can I bring my own LLM key? | No | ✅ Yes |
| Do I pay for LLM usage? | ✅ Yes | ✅ Yes |
| Can I select from different LLMs without bringing my own LLM key? | No \- defaults to Claude Sonnet 4 | ✅ Yes \- via OpenHands LLM provider <br/><br/>[*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
| How much am I charged for LLM usage? | **Marked up pricing** \- 2x Claude Sonnet 4 API prices. *This markup helps cover the cost of runtime compute.* | **No markup** \- 1x API prices. *The $20 monthly subscription covers the cost of runtime compute.* |
### When using the OpenHands LLM Provider outside of OpenHands Cloud
The following applies to **both** the Pay-as-you-go and Pro subscription:
| | Pay-as-you-go or Pro Subscription |
| :---- | :---- |
| Do I have access to multiple models via the OpenHands LLM provider? | ✅ Yes <br/><br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
| Can I generate and refresh OpenHands LLM API keys? | ✅ Yes |
| How much am I charged for LLM usage when I use the OpenHands LLM provider in other AI coding tools? | **No markup** \- pay 1x API prices <br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) <br/><br/> *Usage is deducted from your OpenHands Cloud credit balance.* <br/><br/> *The OpenHands LLM provider is available to all OpenHands Cloud users, and LLM usage is billed at-cost (zero markup). Use these models with OpenHands CLI, running OpenHands on your own, or even other AI coding agents\! [Learn more.](https://www.all-hands.dev/blog/access-state-of-the-art-llm-models-at-cost-via-openhands-gui-and-cli)* |

View File

@@ -13,9 +13,7 @@ description: This guide walks you through installing the OpenHands Slack app.
</iframe>
<Info>
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete.
While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to
validate critical information independently.
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete. While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to validate critical information independently.
</Info>
## Prerequisites
@@ -41,7 +39,7 @@ validate critical information independently.
**Make sure your Slack workspace admin/owner has installed OpenHands Slack App first.**
Every user in the Slack workspace (including admins/owners) must link their OpenHands Cloud account to the OpenHands Slack App. To do this:
1. Visit the [Settings > Integrations](https://app.all-hands.dev/settings/integrations) page in OpenHands Cloud.
1. Visit [integrations settings](https://app.all-hands.dev/settings/integrations) in OpenHands Cloud.
2. Click `Install OpenHands Slack App`.
3. In the top right corner, select the workspace to install the OpenHands Slack app.
4. Review permissions and click allow.
@@ -59,8 +57,7 @@ To start a new conversation, you can mention `@openhands` in a new message or a
Once a conversation is started, all thread messages underneath it will be follow-up messages to OpenHands.
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message.
You must be the user who started the conversation.
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message. You must be the user who started the conversation.
## Example conversation

View File

@@ -1,19 +1,28 @@
---
title: Secrets Management
description: How to manage secrets in OpenHands.
title: OpenHands Settings
description: Overview of some of the settings available in OpenHands.
---
## Overview
## Openhands Cloud vs Running on Your Own
There are some differences between the settings available in OpenHands Cloud and those available when running OpenHands
on your own:
* [OpenHands Cloud settings](/usage/cloud/cloud-ui#settings)
* [Settings available when running on your own](/usage/how-to/gui-mode#settings)
Refer to these pages for more detailed information.
## Secrets Management
OpenHands provides a secrets manager that allows you to securely store and manage sensitive information that can be
accessed by the agent during runtime, such as API keys. These secrets are automatically exported as environment
variables in the agent's runtime environment.
## Accessing the Secrets Manager
### Accessing the Secrets Manager
Navigate to the `Settings > Secrets` page. Here, you'll see a list of all your existing custom secrets.
In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of all your existing custom secrets.
## Adding a New Secret
### Adding a New Secret
1. Click `Add a new secret`.
2. Fill in the following fields:
- **Name**: A unique identifier for your secret (e.g., `AWS_ACCESS_KEY`). This will be the environment variable name.
@@ -21,7 +30,7 @@ Navigate to the `Settings > Secrets` page. Here, you'll see a list of all your e
- **Description** (optional): A brief description of what the secret is used for, which is also provided to the agent.
3. Click `Add secret` to save.
## Editing a Secret
### Editing a Secret
1. Click the `Edit` button next to the secret you want to modify.
2. You can update the name and description of the secret.
@@ -30,13 +39,14 @@ Navigate to the `Settings > Secrets` page. Here, you'll see a list of all your e
value, delete the secret and create a new one.
</Note>
## Deleting a Secret
### Deleting a Secret
1. Click the `Delete` button next to the secret you want to remove.
2. Select `Confirm` to delete the secret.
## Using Secrets in the Agent
### Using Secrets in the Agent
- All custom secrets are automatically exported as environment variables in the agent's runtime environment.
- You can access them in your code using standard environment variable access methods. For example, if you create a
secret named `OPENAI_API_KEY`, you can access it in your code as `process.env.OPENAI_API_KEY` in JavaScript or
`os.environ['OPENAI_API_KEY']` in Python.
- You can access them in your code using standard environment variable access methods
(e.g., `os.environ['SECRET_NAME']` in Python).
- Example: If you create a secret named `OPENAI_API_KEY`, you can access it in your code as
`process.env.OPENAI_API_KEY` in JavaScript or `os.environ['OPENAI_API_KEY']` in Python.

View File

@@ -105,7 +105,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
1. Set the following environment variables in your terminal:
- `SANDBOX_VOLUMES` to specify the directory you want OpenHands to access ([See using SANDBOX_VOLUMES for more info](../runtimes/docker#using-sandbox_volumes))
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"` or `export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929"`)
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"`)
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
2. Run the following command:

View File

@@ -85,11 +85,11 @@ You can use the Settings page at any time to:
- Setup the LLM provider and model for OpenHands.
- [Setup the search engine](/usage/search-engine-setup).
- [Configure MCP servers](/usage/settings/mcp-settings).
- [Configure MCP servers](/usage/mcp).
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup), [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
and [connect to Bitbucket](/usage/how-to/gui-mode#bitbucket-setup).
- Set application settings like your preferred language, notifications and other preferences.
- [Manage custom secrets](/usage/settings/secrets-settings).
- [Manage custom secrets](/usage/common-settings#secrets-management).
#### GitHub Setup

View File

@@ -53,7 +53,7 @@ Set environment variables and run the Docker command:
```bash
# Set required environment variables
export SANDBOX_VOLUMES="/path/to/workspace:/workspace:rw" # Format: host_path:container_path:mode
export LLM_MODEL="anthropic/claude-sonnet-4-20250514" # or "anthropic/claude-sonnet-4-5-20250929"
export LLM_MODEL="anthropic/claude-sonnet-4-20250514"
export LLM_API_KEY="your-api-key"
export SANDBOX_SELECTED_REPO="owner/repo-name" # Optional: requires GITHUB_TOKEN
export GITHUB_TOKEN="your-token" # Required for repository operations

View File

@@ -18,7 +18,6 @@ Based on these findings and community feedback, these are the latest models that
### Cloud / API-Based Models
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [anthropic/claude-sonnet-4-5-20250929](https://www.anthropic.com/api) (recommended)
- [openai/gpt-5-2025-08-07](https://openai.com/api/) (recommended)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)

View File

@@ -15,7 +15,7 @@ description: OpenHands LLM provider with access to state-of-the-art (SOTA) agent
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
- `LLM Provider` to `OpenHands`
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514 or claude-sonnet-4-5-20250929)
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514)
- `API Key` to your OpenHands LLM API key copied from above
## Using OpenHands LLM Provider in the CLI
@@ -36,7 +36,6 @@ Pricing follows official API provider rates. Below are the current pricing detai
|-------|----------------------------|-----------------------------------|------------------------------|------------------|-------------------|
| claude-opus-4-20250514 | $15.00 | $1.50 | $75.00 | 200,000 | 32,000 |
| claude-sonnet-4-20250514 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
| claude-sonnet-4-5-20250929 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
| devstral-medium-2507 | $0.40 | N/A | $2.00 | 128,000 | 128,000 |
| devstral-small-2505 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
| devstral-small-2507 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |

View File

@@ -10,15 +10,12 @@ Model Context Protocol (MCP) is a mechanism that allows OpenHands to communicate
servers can provide additional functionality to the agent, such as specialized data processing, external API access,
or custom tools. MCP is based on the open standard defined at [modelcontextprotocol.io](https://modelcontextprotocol.io).
## Supported MCPs
OpenHands supports the following MCP transport protocols:
<Note>
MCP is currently not available on OpenHands Cloud. This feature is only available when running OpenHands locally.
</Note>
* [Server-Sent Events (SSE)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse)
* [Streamable HTTP (SHTTP)](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http)
* [Standard Input/Output (stdio)](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio)
## How MCP Works
### How MCP Works
When OpenHands starts, it:
@@ -36,90 +33,15 @@ The agent can then use these tools just like any built-in tool. When the agent c
## Configuration
MCP configuration can be defined in:
* The OpenHands UI in the `Settings > MCP` page.
* The OpenHands UI through the Settings under the `MCP` tab.
* The `config.toml` file under the `[mcp]` section if not using the UI.
### Configuration Options
#### SSE Servers
SSE servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SSE server.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
#### SHTTP Servers
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
- `timeout` (optional)
- Type: `int`
- Default: `60`
- Range: `1-3600` seconds (1 hour maximum)
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
- **Use Cases:**
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries.
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls.
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations.
<Note>
This timeout only applies to individual tool calls, not server connection establishment.
</Note>
#### Stdio Servers
<Note>
While stdio servers are supported, [we recommend using MCP proxies](/usage/settings/mcp-settings#configuration-examples) for
better reliability and performance.
</Note>
Stdio servers are configured using an object with the following properties:
- `name` (required)
- Type: `str`
- Description: A unique name for the server.
- `command` (required)
- Type: `str`
- Description: The command to run the server.
- `args` (optional)
- Type: `list of str`
- Default: `[]`
- Description: Command-line arguments to pass to the server.
- `env` (optional)
- Type: `dict of str to str`
- Default: `{}`
- Description: Environment variables to set for the server process.
##### When to Use Direct Stdio
Direct stdio connections may still be appropriate in these scenarios:
- **Development and testing**: Quick prototyping of MCP servers.
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access.
- **Local-only environments**: When you don't want to manage additional proxy processes.
### Configuration Examples
#### Recommended: Using Proxy Servers (SSE/HTTP)
For stdio-based MCP servers, we recommend using MCP proxy tools like
[`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to
HTTP/SSE endpoints.
For stdio-based MCP servers, we recommend using MCP proxy tools like [`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to HTTP/SSE endpoints:
Start the proxy servers separately:
```bash
@@ -150,7 +72,7 @@ sse_servers = [
shttp_servers = [
# Basic SHTTP server with default 60s timeout
"https://api.example.com/mcp/shttp",
# Server with custom timeout for heavy operations
{
url = "https://files.example.com/mcp/shttp",
@@ -160,6 +82,8 @@ shttp_servers = [
]
```
#### Alternative: Direct Stdio Servers (Not Recommended for Production)
```toml
@@ -181,12 +105,138 @@ stdio_servers = [
]
```
## Configuration Options
### SSE Servers
SSE servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SSE server
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication
### SHTTP Servers
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication
- `timeout` (optional)
- Type: `int`
- Default: `60`
- Range: `1-3600` seconds (1 hour maximum)
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
- **Use Cases:**
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations
- **Note**: This timeout only applies to individual tool calls, not server connection establishment.
### Stdio Servers
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
Stdio servers are configured using an object with the following properties:
- `name` (required)
- Type: `str`
- Description: A unique name for the server
- `command` (required)
- Type: `str`
- Description: The command to run the server
- `args` (optional)
- Type: `list of str`
- Default: `[]`
- Description: Command-line arguments to pass to the server
- `env` (optional)
- Type: `dict of str to str`
- Default: `{}`
- Description: Environment variables to set for the server process
#### When to Use Direct Stdio
Direct stdio connections may still be appropriate in these scenarios:
- **Development and testing**: Quick prototyping of MCP servers
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access
- **Local-only environments**: When you don't want to manage additional proxy processes
For production use, we recommend using proxy tools like SuperGateway.
### Other Proxy Tools
Other options include:
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers.
- **Docker-based proxies**: Containerized solutions for better isolation.
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints.
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers
- **Docker-based proxies**: Containerized solutions for better isolation
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints
### Troubleshooting MCP Connections
#### Common Issues with Stdio Servers
- **Process crashes**: Stdio processes may crash without proper error handling
- **Deadlocks**: Stdio communication can deadlock under high load
- **Resource leaks**: Zombie processes if not properly managed
- **Debugging difficulty**: Hard to inspect stdio communication
#### Benefits of Using Proxies
- **HTTP status codes**: Clear error reporting via standard HTTP responses
- **Request logging**: Easy to log and monitor HTTP requests
- **Load balancing**: Can distribute requests across multiple server instances
- **Health checks**: HTTP endpoints can provide health status
- **CORS support**: Better integration with web-based tools
## Transport Protocols
OpenHands supports three different MCP transport protocols:
### Server-Sent Events (SSE)
SSE is a legacy HTTP-based transport that uses Server-Sent Events for server-to-client communication and HTTP POST requests for client-to-server communication. This transport is suitable for basic streaming scenarios but has limitations in session management and connection resumability.
### Streamable HTTP (SHTTP)
SHTTP is the modern HTTP-based transport protocol that provides enhanced features over SSE:
- **Improved Session Management**: Supports stateful sessions with session IDs for maintaining context across requests
- **Connection Resumability**: Can resume broken connections and replay missed messages using event IDs
- **Bidirectional Communication**: Uses HTTP POST for client-to-server and optional SSE streams for server-to-client communication
- **Better Error Handling**: Enhanced error reporting and recovery mechanisms
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
#### SHTTP Timeout Best Practices
When configuring SHTTP timeouts, consider these guidelines:
**Timeout Selection:**
- **Database queries**: 30-60 seconds
- **File operations**: 60-300 seconds (depending on file size)
- **Web scraping**: 60-120 seconds
- **Complex calculations**: 300-1800 seconds
- **Batch processing**: 1800-3600 seconds (maximum)
**Error Handling:**
When a tool call exceeds the configured timeout:
- The operation is cancelled with an `asyncio.TimeoutError`
- The agent receives a timeout error message
- The server connection remains active for subsequent requests
**Monitoring:**
- Set timeouts based on your tool's actual performance characteristics
- Monitor timeout occurrences to optimize timeout values
- Consider implementing server-side timeout handling for graceful degradation
### Standard Input/Output (stdio)
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.

View File

@@ -17,7 +17,8 @@ class SaaSExperimentManager(ExperimentManager):
def run_conversation_variant_test(
user_id, conversation_id, conversation_settings
) -> ConversationInitData:
"""Run conversation variant test and potentially modify the conversation settings
"""
Run conversation variant test and potentially modify the conversation settings
based on the PostHog feature flags.
Args:
@@ -52,7 +53,8 @@ class SaaSExperimentManager(ExperimentManager):
def run_config_variant_test(
user_id: str | None, conversation_id: str, config: OpenHandsConfig
) -> OpenHandsConfig:
"""Run agent config variant test and potentially modify the OpenHands config
"""
Run agent config variant test and potentially modify the OpenHands config
based on the current experiment type and PostHog feature flags.
Args:

View File

@@ -1,4 +1,5 @@
"""LiteLLM model experiment handler.
"""
LiteLLM model experiment handler.
This module contains the handler for the LiteLLM model experiment.
"""
@@ -17,7 +18,8 @@ from openhands.core.logger import openhands_logger as logger
def handle_litellm_default_model_experiment(
user_id, conversation_id, conversation_settings
):
"""Handle the LiteLLM model experiment.
"""
Handle the LiteLLM model experiment.
Args:
user_id: The user ID

View File

@@ -1,4 +1,5 @@
"""System prompt experiment handler.
"""
System prompt experiment handler.
This module contains the handler for the system prompt experiment that uses
the PostHog variant as the system prompt filename.
@@ -16,7 +17,8 @@ from openhands.core.logger import openhands_logger as logger
def _get_system_prompt_variant(user_id, conversation_id):
"""Get the system prompt variant for the experiment.
"""
Get the system prompt variant for the experiment.
Args:
user_id: The user ID
@@ -117,7 +119,8 @@ def _get_system_prompt_variant(user_id, conversation_id):
def handle_system_prompt_experiment(
user_id, conversation_id, config: OpenHandsConfig
) -> OpenHandsConfig:
"""Handle the system prompt experiment for OpenHands config.
"""
Handle the system prompt experiment for OpenHands config.
Args:
user_id: The user ID

View File

@@ -1,4 +1,5 @@
"""LiteLLM model experiment handler.
"""
LiteLLM model experiment handler.
This module contains the handler for the LiteLLM model experiment.
"""
@@ -109,7 +110,8 @@ def handle_claude4_vs_gpt5_experiment(
conversation_id: str,
conversation_settings: ConversationInitData,
) -> ConversationInitData:
"""Handle the LiteLLM model experiment.
"""
Handle the LiteLLM model experiment.
Args:
user_id: The user ID
@@ -119,6 +121,7 @@ def handle_claude4_vs_gpt5_experiment(
Returns:
Modified conversation settings
"""
enabled_variant = _get_model_variant(user_id, conversation_id)
if not enabled_variant:

View File

@@ -1,4 +1,5 @@
"""Condenser max step experiment handler.
"""
Condenser max step experiment handler.
This module contains the handler for the condenser max step experiment that tests
different max_size values for the condenser configuration.
@@ -14,7 +15,8 @@ from openhands.server.session.conversation_init_data import ConversationInitData
def _get_condenser_max_step_variant(user_id, conversation_id):
"""Get the condenser max step variant for the experiment.
"""
Get the condenser max step variant for the experiment.
Args:
user_id: The user ID
@@ -117,7 +119,8 @@ def handle_condenser_max_step_experiment(
conversation_id: str,
conversation_settings: ConversationInitData,
) -> ConversationInitData:
"""Handle the condenser max step experiment for conversation settings.
"""
Handle the condenser max step experiment for conversation settings.
We should not modify persistent user settings. Instead, apply the experiment
variant to the conversation's in-memory settings object for this session only.
@@ -128,6 +131,7 @@ def handle_condenser_max_step_experiment(
Returns the (potentially) modified conversation_settings.
"""
enabled_variant = _get_condenser_max_step_variant(user_id, conversation_id)
if enabled_variant is None:

View File

@@ -1,4 +1,5 @@
"""Experiment versions package.
"""
Experiment versions package.
This package contains handlers for different experiment versions.
"""

View File

@@ -43,7 +43,8 @@ class TriggerType(str, Enum):
class GitHubDataCollector:
"""Saves data on Cloud Resolver Interactions
"""
Saves data on Cloud Resolver Interactions
1. We always save
- Resolver trigger (comment or label)
@@ -88,7 +89,8 @@ class GitHubDataCollector:
self.conversation_id = None
async def _get_repo_node_id(self, repo_id: str, gh_client) -> str:
"""Get the new GitHub GraphQL node ID for a repository using the GitHub client.
"""
Get the new GitHub GraphQL node ID for a repository using the GitHub client.
Args:
repo_id: Numeric repository ID as string (e.g., "123456789")
@@ -134,7 +136,10 @@ class GitHubDataCollector:
def _get_issue_comments(
self, installation_id: str, repo_name: str, issue_number: int, conversation_id
) -> list[dict[str, Any]]:
"""Retrieve all comments from an issue until a comment with conversation_id is found"""
"""
Retrieve all comments from an issue until a comment with conversation_id is found
"""
try:
installation_token = self._get_installation_access_token(installation_id)
@@ -170,16 +175,18 @@ class GitHubDataCollector:
github_view: GithubIssue,
trigger_type: TriggerType,
) -> None:
"""Save issue data when it's labeled with openhands
1. Save under {conversation_dir}/{conversation_id}/github_data/issue_{issue_number}.json
2. Save issue snapshot (title, body, comments)
3. Save trigger type (label)
4. Save PR opened (if exists, this information comes later when agent has finished its task)
- Save commit shas
- Save author info
5. Was PR merged or closed
"""
Save issue data when it's labeled with openhands
1. Save under {conversation_dir}/{conversation_id}/github_data/issue_{issue_number}.json
2. Save issue snapshot (title, body, comments)
3. Save trigger type (label)
4. Save PR opened (if exists, this information comes later when agent has finished its task)
- Save commit shas
- Save author info
5. Was PR merged or closed
"""
conversation_id = github_view.conversation_id
if not conversation_id:
@@ -378,6 +385,7 @@ class GitHubDataCollector:
openhands_general_comment_count: int = 0,
) -> dict:
"""Build the final data structure for JSON storage"""
is_merged = pr_data['merged']
merged_by = None
merge_commit_sha = None
@@ -411,7 +419,8 @@ class GitHubDataCollector:
}
async def save_full_pr(self, openhands_pr: OpenhandsPR) -> None:
"""Save PR information including metadata and commit details using GraphQL
"""
Save PR information including metadata and commit details using GraphQL
Saves:
- Repo metadata (repo name, languages, contributors)
@@ -597,12 +606,17 @@ class GitHubDataCollector:
return None
def _is_pr_closed_or_merged(self, payload):
"""Check if PR was closed (regardless of conversation URL)"""
"""
Check if PR was closed (regardless of conversation URL)
"""
action = payload.get('action', '')
return action == 'closed' and 'pull_request' in payload
def _track_closed_or_merged_pr(self, payload):
"""Track PR closed/merged event"""
"""
Track PR closed/merged event
"""
repo_id = str(payload['repository']['id'])
pr_number = payload['number']
installation_id = str(payload['installation']['id'])

View File

@@ -103,7 +103,8 @@ class SaaSGitHubService(GitHubService):
}
async def get_repository_node_id(self, repo_id: str) -> str:
"""Get the new GitHub GraphQL node ID for a repository using REST API.
"""
Get the new GitHub GraphQL node ID for a repository using REST API.
Args:
repo_id: Numeric repository ID as string (e.g., "123456789")

View File

@@ -39,6 +39,7 @@ def fetch_github_issue_context(
Returns:
A comprehensive string containing the issue/PR context
"""
# Build context string
context_parts = []

View File

@@ -55,6 +55,7 @@ async def get_user_proactive_conversation_setting(user_id: str | None) -> bool:
This function checks both the global environment variable kill switch AND
the user's individual setting. Both must be true for the function to return true.
"""
# If no user ID is provided, we can't check user settings
if not user_id:
return False

View File

@@ -43,7 +43,8 @@ class GitlabManager(Manager):
async def _user_has_write_access_to_repo(
self, project_id: str, user_id: str
) -> bool:
"""Check if the user has write access to the repository (can pull/push changes and open merge requests).
"""
Check if the user has write access to the repository (can pull/push changes and open merge requests).
Args:
project_id: The ID of the GitLab project
@@ -53,6 +54,7 @@ class GitlabManager(Manager):
Returns:
bool: True if the user has write access to the repository, False otherwise
"""
keycloak_user_id = await self.token_manager.get_user_id_from_idp_user_id(
user_id, ProviderType.GITLAB
)
@@ -115,7 +117,8 @@ class GitlabManager(Manager):
return has_write_access
async def send_message(self, message: Message, gitlab_view: ResolverViewInterface):
"""Send a message to GitLab based on the view type.
"""
Send a message to GitLab based on the view type.
Args:
message: The message to send
@@ -162,7 +165,8 @@ class GitlabManager(Manager):
)
async def start_job(self, gitlab_view: GitlabViewType):
"""Start a job for the GitLab view.
"""
Start a job for the GitLab view.
Args:
gitlab_view: The GitLab view object containing issue/PR/comment info

View File

@@ -81,7 +81,8 @@ class SaaSGitLabService(GitLabService):
return gitlab_token
async def get_owned_groups(self) -> list[dict]:
"""Get all groups for which the current user is the owner.
"""
Get all groups for which the current user is the owner.
Returns:
list[dict]: A list of groups owned by the current user.
@@ -97,7 +98,8 @@ class SaaSGitLabService(GitLabService):
return []
async def add_owned_projects_and_groups_to_db(self, owned_personal_projects):
"""Add owned projects and groups to the database for webhook tracking.
"""
Add owned projects and groups to the database for webhook tracking.
Args:
owned_personal_projects: List of personal projects owned by the user
@@ -145,7 +147,8 @@ class SaaSGitLabService(GitLabService):
async def store_repository_data(
self, users_personal_projects: list[dict], repositories: list[Repository]
) -> None:
"""Store repository data in the database.
"""
Store repository data in the database.
This function combines the functionality of add_owned_projects_and_groups_to_db and store_repositories_in_db.
Args:
@@ -168,7 +171,8 @@ class SaaSGitLabService(GitLabService):
async def get_all_repositories(
self, sort: str, app_mode: AppMode, store_in_background: bool = True
) -> list[Repository]:
"""Get repositories for the authenticated user, including information about the kind of project.
"""
Get repositories for the authenticated user, including information about the kind of project.
Also collects repositories where the kind is "user" and the user is the owner.
Args:
@@ -266,7 +270,8 @@ class SaaSGitLabService(GitLabService):
async def check_resource_exists(
self, resource_type: GitLabResourceType, resource_id: str
) -> tuple[bool, WebhookStatus | None]:
"""Check if resource exists and the user has access to it.
"""
Check if resource exists and the user has access to it.
Args:
resource_type: The type of resource
@@ -277,6 +282,7 @@ class SaaSGitLabService(GitLabService):
- bool: True if the resource exists and the user has access to it, False otherwise
- str: A reason message explaining the result
"""
if resource_type == GitLabResourceType.GROUP:
url = f'{self.BASE_URL}/groups/{resource_id}'
else:
@@ -295,7 +301,8 @@ class SaaSGitLabService(GitLabService):
async def check_webhook_exists_on_resource(
self, resource_type: GitLabResourceType, resource_id: str, webhook_url: str
) -> tuple[bool, WebhookStatus | None]:
"""Check if a webhook already exists for resource with a specific URL.
"""
Check if a webhook already exists for resource with a specific URL.
Args:
resource_type: The type of resource
@@ -307,6 +314,7 @@ class SaaSGitLabService(GitLabService):
- bool: True if the webhook exists, False otherwise
- str: A reason message explaining the result
"""
# Construct the URL based on the resource type
if resource_type == GitLabResourceType.GROUP:
url = f'{self.BASE_URL}/groups/{resource_id}/hooks'
@@ -335,7 +343,8 @@ class SaaSGitLabService(GitLabService):
async def check_user_has_admin_access_to_resource(
self, resource_type: GitLabResourceType, resource_id: str
) -> tuple[bool, WebhookStatus | None]:
"""Check if the user has admin access to resource (is either an owner or maintainer)
"""
Check if the user has admin access to resource (is either an owner or maintainer)
Args:
resource_type: The type of resource
@@ -346,6 +355,7 @@ class SaaSGitLabService(GitLabService):
- bool: True if the user has admin access to the resource (owner or maintainer), False otherwise
- str: A reason message explaining the result
"""
# For groups, we need to check if the user is an owner or maintainer
if resource_type == GitLabResourceType.GROUP:
url = f'{self.BASE_URL}/groups/{resource_id}/members/all'
@@ -402,7 +412,8 @@ class SaaSGitLabService(GitLabService):
webhook_uuid: str,
scopes: list[str],
) -> tuple[str | None, WebhookStatus | None]:
"""Install webhook for user's group or project
"""
Install webhook for user's group or project
Args:
resource_type: The type of resource
@@ -417,6 +428,7 @@ class SaaSGitLabService(GitLabService):
- bool: True if installation was successful, False otherwise
- str: A reason message explaining the result
"""
description = 'Cloud OpenHands Resolver'
# Set up webhook parameters
@@ -488,7 +500,9 @@ class SaaSGitLabService(GitLabService):
async def reply_to_issue(
self, project_id: str, issue_number: str, discussion_id: str | None, body: str
):
"""Either create new comment thread, or reply to comment thread (depending on discussion_id param)"""
"""
Either create new comment thread, or reply to comment thread (depending on discussion_id param)
"""
try:
if discussion_id:
url = f'{self.BASE_URL}/projects/{project_id}/issues/{issue_number}/discussions/{discussion_id}/notes'
@@ -503,7 +517,9 @@ class SaaSGitLabService(GitLabService):
async def reply_to_mr(
self, project_id: str, merge_request_iid: str, discussion_id: str, body: str
):
"""Reply to comment thread on MR"""
"""
Reply to comment thread on MR
"""
try:
url = f'{self.BASE_URL}/projects/{project_id}/merge_requests/{merge_request_iid}/discussions/{discussion_id}/notes'
params = {'body': body}

View File

@@ -48,6 +48,7 @@ class JiraManager(Manager):
self, jira_user_id: str, workspace_id: int
) -> tuple[JiraUser | None, UserAuth | None]:
"""Authenticate Jira user and get their OpenHands user auth."""
# Find active Jira user by Keycloak user ID and workspace ID
jira_user = await self.integration_store.get_active_user(
jira_user_id, workspace_id
@@ -205,6 +206,7 @@ class JiraManager(Manager):
async def receive_message(self, message: Message):
"""Process incoming Jira webhook message."""
payload = message.message.get('payload', {})
job_context = self.parse_webhook(payload)
@@ -297,7 +299,10 @@ class JiraManager(Manager):
async def is_job_requested(
self, message: Message, jira_view: JiraViewInterface
) -> bool:
"""Check if a job is requested and handle repository selection."""
"""
Check if a job is requested and handle repository selection.
"""
if isinstance(jira_view, JiraExistingConversationView):
return True

View File

@@ -35,6 +35,7 @@ class JiraNewConversationView(JiraViewInterface):
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
instructions_template = jinja_env.get_template('jira_instructions.j2')
instructions = instructions_template.render()
@@ -51,6 +52,7 @@ class JiraNewConversationView(JiraViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Create a new Jira conversation"""
if not self.selected_repo:
raise StartingConvoException('No repository selected for this conversation')
@@ -110,6 +112,7 @@ class JiraExistingConversationView(JiraViewInterface):
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
user_msg_template = jinja_env.get_template('jira_existing_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.job_context.issue_key,
@@ -122,6 +125,7 @@ class JiraExistingConversationView(JiraViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Update an existing Jira conversation"""
user_id = self.jira_user.keycloak_user_id
try:
@@ -187,6 +191,7 @@ class JiraFactory:
jira_workspace: JiraWorkspace,
) -> JiraViewInterface:
"""Create appropriate Jira view based on the message and user state"""
if not jira_user or not saas_user_auth or not jira_workspace:
raise StartingConvoException('User not authenticated with Jira integration')

View File

@@ -48,6 +48,7 @@ class JiraDcManager(Manager):
self, user_email: str, jira_dc_user_id: str, workspace_id: int
) -> tuple[JiraDcUser | None, UserAuth | None]:
"""Authenticate Jira DC user and get their OpenHands user auth."""
if not jira_dc_user_id or jira_dc_user_id == 'none':
# Get Keycloak user ID from email
keycloak_user_id = await self.token_manager.get_user_id_from_user_email(
@@ -220,6 +221,7 @@ class JiraDcManager(Manager):
async def receive_message(self, message: Message):
"""Process incoming Jira DC webhook message."""
payload = message.message.get('payload', {})
job_context = self.parse_webhook(payload)
@@ -313,7 +315,10 @@ class JiraDcManager(Manager):
async def is_job_requested(
self, message: Message, jira_dc_view: JiraDcViewInterface
) -> bool:
"""Check if a job is requested and handle repository selection."""
"""
Check if a job is requested and handle repository selection.
"""
if isinstance(jira_dc_view, JiraDcExistingConversationView):
return True

View File

@@ -38,6 +38,7 @@ class JiraDcNewConversationView(JiraDcViewInterface):
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
instructions_template = jinja_env.get_template('jira_dc_instructions.j2')
instructions = instructions_template.render()
@@ -54,6 +55,7 @@ class JiraDcNewConversationView(JiraDcViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Create a new Jira DC conversation"""
if not self.selected_repo:
raise StartingConvoException('No repository selected for this conversation')
@@ -113,6 +115,7 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
user_msg_template = jinja_env.get_template('jira_dc_existing_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.job_context.issue_key,
@@ -125,6 +128,7 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Update an existing Jira conversation"""
user_id = self.jira_dc_user.keycloak_user_id
try:
@@ -191,6 +195,7 @@ class JiraDcFactory:
jira_dc_workspace: JiraDcWorkspace,
) -> JiraDcViewInterface:
"""Create appropriate Jira DC view based on the payload."""
if not jira_dc_user or not saas_user_auth or not jira_dc_workspace:
raise StartingConvoException('User not authenticated with Jira integration')

View File

@@ -46,6 +46,7 @@ class LinearManager(Manager):
self, linear_user_id: str, workspace_id: int
) -> tuple[LinearUser | None, UserAuth | None]:
"""Authenticate Linear user and get their OpenHands user auth."""
# Find active Linear user by Linear user ID and workspace ID
linear_user = await self.integration_store.get_active_user(
linear_user_id, workspace_id
@@ -304,7 +305,10 @@ class LinearManager(Manager):
async def is_job_requested(
self, message: Message, linear_view: LinearViewInterface
) -> bool:
"""Check if a job is requested and handle repository selection."""
"""
Check if a job is requested and handle repository selection.
"""
if isinstance(linear_view, LinearExistingConversationView):
return True

View File

@@ -35,6 +35,7 @@ class LinearNewConversationView(LinearViewInterface):
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
instructions_template = jinja_env.get_template('linear_instructions.j2')
instructions = instructions_template.render()
@@ -51,6 +52,7 @@ class LinearNewConversationView(LinearViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Create a new Linear conversation"""
if not self.selected_repo:
raise StartingConvoException('No repository selected for this conversation')
@@ -110,6 +112,7 @@ class LinearExistingConversationView(LinearViewInterface):
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
user_msg_template = jinja_env.get_template('linear_existing_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.job_context.issue_key,
@@ -122,6 +125,7 @@ class LinearExistingConversationView(LinearViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Update an existing Linear conversation"""
user_id = self.linear_user.keycloak_user_id
try:
@@ -188,6 +192,7 @@ class LinearFactory:
linear_workspace: LinearWorkspace,
) -> LinearViewInterface:
"""Create appropriate Linear view based on the message and user state"""
if not linear_user or not saas_user_auth or not linear_workspace:
raise StartingConvoException(
'User not authenticated with Linear integration'

View File

@@ -8,22 +8,22 @@ class Manager(ABC):
@abstractmethod
async def receive_message(self, message: Message):
"""Receive message from integration"""
"Receive message from integration"
raise NotImplementedError
@abstractmethod
def send_message(self, message: Message):
"""Send message to integration from Openhands server"""
"Send message to integration from Openhands server"
raise NotImplementedError
@abstractmethod
async def is_job_requested(self, message: Message) -> bool:
"""Confirm that a job is being requested"""
"Confirm that a job is being requested"
raise NotImplementedError
@abstractmethod
def start_job(self):
"""Kick off a job with openhands agent"""
"Kick off a job with openhands agent"
raise NotImplementedError
def create_outgoing_message(self, msg: str | dict, ephemeral: bool = False):

View File

@@ -244,11 +244,13 @@ class SlackManager(Manager):
async def is_job_requested(
self, message: Message, slack_view: SlackViewInterface
) -> bool:
"""A job is always request we only receive webhooks for events associated with the slack bot
"""
A job is always request we only receive webhooks for events associated with the slack bot
This method really just checks
1. Is the user is authenticated
2. Do we have the necessary information to start a job (either by inferring the selected repo, otherwise asking the user)
"""
# Infer repo from user message is not needed; user selected repo from the form or is updating existing convo
if isinstance(slack_view, SlackUpdateExistingConversationView):
return True

View File

@@ -24,17 +24,17 @@ class SlackViewInterface(SummaryExtractionTracker, ABC):
@abstractmethod
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
"Instructions passed when conversation is first initialized"
pass
@abstractmethod
async def create_or_update_conversation(self, jinja_env: Environment):
"""Create a new conversation"""
"Create a new conversation"
pass
@abstractmethod
def get_callback_id(self) -> str:
"""Unique callback id for subscribription made to EventStream for fetching agent summary"""
"Unique callback id for subscribription made to EventStream for fetching agent summary"
pass
@abstractmethod
@@ -43,4 +43,6 @@ class SlackViewInterface(SummaryExtractionTracker, ABC):
class StartingConvoException(Exception):
"""Raised when trying to send message to a conversation that's is still starting up"""
"""
Raised when trying to send message to a conversation that's is still starting up
"""

View File

@@ -95,7 +95,8 @@ class SlackNewConversationView(SlackViewInterface):
return ''
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
"Instructions passed when conversation is first initialized"
user_info: SlackUser = self.slack_to_openhands_user
messages = []
@@ -178,7 +179,9 @@ class SlackNewConversationView(SlackViewInterface):
await slack_conversation_store.create_slack_conversation(slack_conversation)
async def create_or_update_conversation(self, jinja: Environment) -> str:
"""Only creates a new conversation"""
"""
Only creates a new conversation
"""
self._verify_necessary_values_are_set()
provider_tokens = await self.saas_user_auth.get_provider_tokens()
@@ -243,7 +246,9 @@ class SlackUpdateExistingConversationView(SlackNewConversationView):
return user_message, ''
async def create_or_update_conversation(self, jinja: Environment) -> str:
"""Send new user message to converation"""
"""
Send new user message to converation
"""
user_info: SlackUser = self.slack_to_openhands_user
saas_user_auth: UserAuth = self.saas_user_auth
user_id = user_info.keycloak_user_id

View File

@@ -1,4 +1,5 @@
"""Utilities for loading and managing pre-trained classifiers.
"""
Utilities for loading and managing pre-trained classifiers.
Assumes that classifiers are stored adjacent to this file in the `solvability/data` directory, using a simple
`name + .json` pattern.
@@ -10,7 +11,8 @@ from integrations.solvability.models.classifier import SolvabilityClassifier
def load_classifier(name: str) -> SolvabilityClassifier:
"""Load a classifier by name.
"""
Load a classifier by name.
Args:
name (str): The name of the classifier to load.
@@ -29,7 +31,8 @@ def load_classifier(name: str) -> SolvabilityClassifier:
def available_classifiers() -> list[str]:
"""List all available classifiers in the data directory.
"""
List all available classifiers in the data directory.
Returns:
list[str]: A list of classifier names (without the .json extension).

View File

@@ -1,4 +1,5 @@
"""Solvability Models Package
"""
Solvability Models Package
This package contains the core machine learning models and components for predicting
the solvability of GitHub issues and similar technical problems.

View File

@@ -26,7 +26,8 @@ from openhands.core.config import LLMConfig
class SolvabilityClassifier(BaseModel):
"""Machine learning pipeline for predicting the solvability of GitHub issues and similar problems.
"""
Machine learning pipeline for predicting the solvability of GitHub issues and similar problems.
This classifier combines LLM-based feature extraction with traditional ML classification:
1. Uses a Featurizer to extract semantic boolean features from issue descriptions via LLM calls
@@ -86,7 +87,9 @@ class SolvabilityClassifier(BaseModel):
@model_validator(mode='after')
def validate_random_state(self) -> SolvabilityClassifier:
"""Validate the random state configuration between this object and the classifier."""
"""
Validate the random state configuration between this object and the classifier.
"""
# If both random states are set, they definitely need to agree.
if self.random_state is not None and self.classifier.random_state is not None:
if self.random_state != self.classifier.random_state:
@@ -101,7 +104,9 @@ class SolvabilityClassifier(BaseModel):
@property
def features_(self) -> pd.DataFrame:
"""Get the features used by the classifier for the most recent inputs."""
"""
Get the features used by the classifier for the most recent inputs.
"""
if 'features_' not in self._classifier_attrs:
raise ValueError(
'SolvabilityClassifier.transform() has not yet been called.'
@@ -110,7 +115,9 @@ class SolvabilityClassifier(BaseModel):
@property
def cost_(self) -> pd.DataFrame:
"""Get the cost of the classifier for the most recent inputs."""
"""
Get the cost of the classifier for the most recent inputs.
"""
if 'cost_' not in self._classifier_attrs:
raise ValueError(
'SolvabilityClassifier.transform() has not yet been called.'
@@ -119,7 +126,9 @@ class SolvabilityClassifier(BaseModel):
@property
def feature_importances_(self) -> np.ndarray:
"""Get the feature importances for the most recent inputs."""
"""
Get the feature importances for the most recent inputs.
"""
if 'feature_importances_' not in self._classifier_attrs:
raise ValueError(
'No SolvabilityClassifier methods that produce feature importances (.fit(), .predict_proba(), and '
@@ -129,7 +138,9 @@ class SolvabilityClassifier(BaseModel):
@property
def is_fitted(self) -> bool:
"""Check if the classifier is fitted."""
"""
Check if the classifier is fitted.
"""
try:
check_is_fitted(self.classifier)
return True
@@ -137,7 +148,8 @@ class SolvabilityClassifier(BaseModel):
return False
def transform(self, issues: pd.Series, llm_config: LLMConfig) -> pd.DataFrame:
"""Transform the input issues using the featurizer to extract features.
"""
Transform the input issues using the featurizer to extract features.
This method orchestrates the feature extraction pipeline:
1. Uses the featurizer to generate embeddings for all issues
@@ -171,7 +183,8 @@ class SolvabilityClassifier(BaseModel):
def fit(
self, issues: pd.Series, labels: pd.Series, llm_config: LLMConfig
) -> SolvabilityClassifier:
"""Fit the classifier to the input issues and labels.
"""
Fit the classifier to the input issues and labels.
Args:
issues: A pandas Series containing the issue descriptions.
@@ -195,7 +208,8 @@ class SolvabilityClassifier(BaseModel):
return self
def predict_proba(self, issues: pd.Series, llm_config: LLMConfig) -> np.ndarray:
"""Predict the solvability probabilities for the input issues.
"""
Predict the solvability probabilities for the input issues.
Returns class probabilities where the second column represents the probability
of the issue being solvable (positive class).
@@ -229,7 +243,8 @@ class SolvabilityClassifier(BaseModel):
return scores # type: ignore[no-any-return]
def predict(self, issues: pd.Series, llm_config: LLMConfig) -> np.ndarray:
"""Predict the solvability of the input issues by returning binary labels.
"""
Predict the solvability of the input issues by returning binary labels.
Uses a 0.5 probability threshold to convert probabilities to binary predictions.
@@ -251,7 +266,8 @@ class SolvabilityClassifier(BaseModel):
scores: np.ndarray,
labels: np.ndarray | None = None,
) -> np.ndarray:
"""Calculate feature importance scores using the configured strategy.
"""
Calculate feature importance scores using the configured strategy.
Different strategies provide different interpretations:
- SHAP: Shapley values indicating contribution to individual predictions
@@ -297,7 +313,8 @@ class SolvabilityClassifier(BaseModel):
)
def add_features(self, features: list[Feature]) -> SolvabilityClassifier:
"""Add new features to the classifier's featurizer.
"""
Add new features to the classifier's featurizer.
Note: Adding features after training requires retraining the classifier
since the feature space will have changed.
@@ -314,7 +331,8 @@ class SolvabilityClassifier(BaseModel):
return self
def forget_features(self, features: list[Feature]) -> SolvabilityClassifier:
"""Remove features from the classifier's featurizer.
"""
Remove features from the classifier's featurizer.
Note: Removing features after training requires retraining the classifier
since the feature space will have changed.
@@ -336,13 +354,17 @@ class SolvabilityClassifier(BaseModel):
@field_serializer('classifier')
@staticmethod
def _rfc_to_json(rfc: RandomForestClassifier) -> str:
"""Convert a RandomForestClassifier to a JSON-compatible value (a string)."""
"""
Convert a RandomForestClassifier to a JSON-compatible value (a string).
"""
return base64.b64encode(pickle.dumps(rfc)).decode('utf-8')
@field_validator('classifier', mode='before')
@staticmethod
def _json_to_rfc(value: str | RandomForestClassifier) -> RandomForestClassifier:
"""Convert a JSON-compatible value (a string) back to a RandomForestClassifier."""
"""
Convert a JSON-compatible value (a string) back to a RandomForestClassifier.
"""
if isinstance(value, RandomForestClassifier):
return value
@@ -361,7 +383,8 @@ class SolvabilityClassifier(BaseModel):
def solvability_report(
self, issue: str, llm_config: LLMConfig, **kwargs: Any
) -> SolvabilityReport:
"""Generate a solvability report for the given issue.
"""
Generate a solvability report for the given issue.
Args:
issue: The issue description for which to generate the report.
@@ -404,5 +427,7 @@ class SolvabilityClassifier(BaseModel):
def __call__(
self, issue: str, llm_config: LLMConfig, **kwargs: Any
) -> SolvabilityReport:
"""Generate a solvability report for the given issue."""
"""
Generate a solvability report for the given issue.
"""
return self.solvability_report(issue, llm_config=llm_config, **kwargs)

View File

@@ -10,7 +10,8 @@ from openhands.llm.llm import LLM
class Feature(BaseModel):
"""Represents a single boolean feature that can be extracted from issue descriptions.
"""
Represents a single boolean feature that can be extracted from issue descriptions.
Features are semantic properties of issues (e.g., "has_code_example", "requires_debugging")
that are evaluated by LLMs and used as input to the solvability classifier.
@@ -24,7 +25,8 @@ class Feature(BaseModel):
@property
def to_tool_description_field(self) -> dict[str, Any]:
"""Convert this feature to a JSON schema field for LLM tool calling.
"""
Convert this feature to a JSON schema field for LLM tool calling.
Returns:
dict: JSON schema field definition for this feature.
@@ -36,7 +38,8 @@ class Feature(BaseModel):
class EmbeddingDimension(BaseModel):
"""Represents a single dimension (feature evaluation) within a feature embedding sample.
"""
Represents a single dimension (feature evaluation) within a feature embedding sample.
Each dimension corresponds to one feature being evaluated as true/false for a given issue.
"""
@@ -57,7 +60,8 @@ Maps feature identifiers to their boolean evaluations.
class FeatureEmbedding(BaseModel):
"""Represents the complete feature embedding for a single issue, including multiple samples
"""
Represents the complete feature embedding for a single issue, including multiple samples
and associated metadata about the LLM calls used to generate it.
Multiple samples are collected to account for LLM variability and provide more robust
@@ -78,7 +82,8 @@ class FeatureEmbedding(BaseModel):
@property
def dimensions(self) -> list[str]:
"""Get all unique feature identifiers present across all samples.
"""
Get all unique feature identifiers present across all samples.
Returns:
list[str]: List of feature identifiers that appear in at least one sample.
@@ -89,7 +94,8 @@ class FeatureEmbedding(BaseModel):
return list(dims)
def coefficient(self, dimension: str) -> float | None:
"""Calculate the average coefficient (0-1) for a specific feature dimension.
"""
Calculate the average coefficient (0-1) for a specific feature dimension.
This computes the proportion of samples where the feature was evaluated as True,
providing a continuous feature value for the classifier.
@@ -111,7 +117,8 @@ class FeatureEmbedding(BaseModel):
return None
def to_row(self) -> dict[str, Any]:
"""Convert the embedding to a flat dictionary suitable for DataFrame construction.
"""
Convert the embedding to a flat dictionary suitable for DataFrame construction.
Returns:
dict[str, Any]: Dictionary with metadata fields and feature coefficients.
@@ -124,7 +131,8 @@ class FeatureEmbedding(BaseModel):
}
def sample_entropy(self) -> dict[str, float]:
"""Calculate the Shannon entropy of feature evaluations across samples.
"""
Calculate the Shannon entropy of feature evaluations across samples.
Higher entropy indicates more variability in LLM responses for a feature,
which may suggest ambiguity in the feature definition or issue description.
@@ -154,7 +162,8 @@ class FeatureEmbedding(BaseModel):
class Featurizer(BaseModel):
"""Orchestrates LLM-based feature extraction from issue descriptions.
"""
Orchestrates LLM-based feature extraction from issue descriptions.
The Featurizer uses structured LLM tool calling to evaluate boolean features
for issue descriptions. It handles prompt construction, tool schema generation,
@@ -171,7 +180,8 @@ class Featurizer(BaseModel):
"""List of features to extract from each issue description."""
def system_message(self) -> dict[str, Any]:
"""Construct the system message for LLM conversations.
"""
Construct the system message for LLM conversations.
Returns:
dict[str, Any]: System message dictionary for LLM API calls.
@@ -184,7 +194,8 @@ class Featurizer(BaseModel):
def user_message(
self, issue_description: str, set_cache: bool = True
) -> dict[str, Any]:
"""Construct the user message containing the issue description.
"""
Construct the user message containing the issue description.
Args:
issue_description: The description of the issue to analyze.
@@ -204,7 +215,8 @@ class Featurizer(BaseModel):
@property
def tool_choice(self) -> dict[str, Any]:
"""Get the tool choice configuration for forcing LLM to use the featurizer tool.
"""
Get the tool choice configuration for forcing LLM to use the featurizer tool.
Returns:
dict[str, Any]: Tool choice configuration for LLM API calls.
@@ -216,7 +228,8 @@ class Featurizer(BaseModel):
@property
def tool_description(self) -> dict[str, Any]:
"""Generate the tool schema for the featurizer function.
"""
Generate the tool schema for the featurizer function.
Creates a JSON schema that describes the featurizer tool with all configured
features as boolean parameters.
@@ -246,7 +259,8 @@ class Featurizer(BaseModel):
temperature: float = 1.0,
samples: int = 10,
) -> FeatureEmbedding:
"""Generate a feature embedding for a single issue description.
"""
Generate a feature embedding for a single issue description.
Makes multiple LLM calls to collect samples and reduce variance in feature evaluations.
Each call uses tool calling to extract structured boolean feature values.
@@ -308,7 +322,8 @@ class Featurizer(BaseModel):
temperature: float = 1.0,
samples: int = 10,
) -> list[FeatureEmbedding]:
"""Generate embeddings for a batch of issue descriptions using concurrent processing.
"""
Generate embeddings for a batch of issue descriptions using concurrent processing.
Processes multiple issues in parallel to improve throughput while maintaining
result ordering.
@@ -344,7 +359,8 @@ class Featurizer(BaseModel):
return results
def feature_identifiers(self) -> list[str]:
"""Get the identifiers of all configured features.
"""
Get the identifiers of all configured features.
Returns:
list[str]: List of feature identifiers in the order they were defined.

View File

@@ -2,7 +2,8 @@ from enum import Enum
class ImportanceStrategy(str, Enum):
"""Strategy to use for calculating feature importances, which are used to estimate the predictive power of each feature
"""
Strategy to use for calculating feature importances, which are used to estimate the predictive power of each feature
in training loops and explanations.
"""

View File

@@ -6,7 +6,8 @@ from pydantic import BaseModel, Field
class SolvabilityReport(BaseModel):
"""Comprehensive report containing solvability predictions and analysis for a single issue.
"""
Comprehensive report containing solvability predictions and analysis for a single issue.
This report includes the solvability score, extracted feature values, feature importance analysis,
cost metrics (tokens and latency), and metadata about the prediction process. It serves as the

View File

@@ -39,13 +39,13 @@ class ResolverViewInterface(SummaryExtractionTracker):
raw_payload: dict
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
"Instructions passed when conversation is first initialized"
raise NotImplementedError()
async def create_new_conversation(self, jinja_env: Environment, token: str):
"""Create a new conversation"""
"Create a new conversation"
raise NotImplementedError()
def get_callback_id(self) -> str:
"""Unique callback id for subscribription made to EventStream for fetching agent summary"""
"Unique callback id for subscribription made to EventStream for fetching agent summary"
raise NotImplementedError()

View File

@@ -215,7 +215,9 @@ def get_last_user_msg(event_store: EventStoreABC) -> list[MessageAction]:
def extract_summary_from_event_store(
event_store: EventStoreABC, conversation_id: str
) -> str:
"""Get agent summary or alternative message depending on current AgentState"""
"""
Get agent summary or alternative message depending on current AgentState
"""
conversation_link = CONVERSATION_URL.format(conversation_id)
summary_instruction = get_summary_instruction()
@@ -291,7 +293,10 @@ async def get_last_user_msg_from_conversation_manager(
async def extract_summary_from_conversation_manager(
conversation_manager: ConversationManager, conversation_id: str
) -> str:
"""Get agent summary or alternative message depending on current AgentState"""
"""
Get agent summary or alternative message depending on current AgentState
"""
event_store = await get_event_store_from_conversation_manager(
conversation_manager, conversation_id
)
@@ -300,7 +305,8 @@ async def extract_summary_from_conversation_manager(
def append_conversation_footer(message: str, conversation_id: str) -> str:
"""Append a small footer with the conversation URL to a message.
"""
Append a small footer with the conversation URL to a message.
Args:
message: The original message content
@@ -315,12 +321,14 @@ def append_conversation_footer(message: str, conversation_id: str) -> str:
async def store_repositories_in_db(repos: list[Repository], user_id: str) -> None:
"""Store repositories in DB and create user-repository mappings
"""
Store repositories in DB and create user-repository mappings
Args:
repos: List of Repository objects to store
user_id: User ID associated with these repositories
"""
# Convert Repository objects to StoredRepository objects
# Convert Repository objects to UserRepositoryMap objects
stored_repos = []
@@ -358,9 +366,9 @@ async def store_repositories_in_db(repos: list[Repository], user_id: str) -> Non
def infer_repo_from_message(user_msg: str) -> list[str]:
"""Extract all repository names in the format 'owner/repo' from various Git provider URLs
"""
Extract all repository names in the format 'owner/repo' from various Git provider URLs
and direct mentions in text. Supports GitHub, GitLab, and BitBucket.
Args:
user_msg: Input message that may contain repository references
Returns:
@@ -443,10 +451,10 @@ def filter_potential_repos_by_user_msg(
def markdown_to_jira_markup(markdown_text: str) -> str:
"""Convert markdown text to Jira Wiki Markup format.
"""
Convert markdown text to Jira Wiki Markup format.
This function handles common markdown elements and converts them to their
Jira Wiki Markup equivalents. It's designed to be exception-safe.
Args:
markdown_text: The markdown text to convert
Returns:

View File

@@ -22,7 +22,8 @@ depends_on = None
def upgrade():
"""Create maintenance tasks for all users whose user_version is less than
"""
Create maintenance tasks for all users whose user_version is less than
the current version.
This replaces the functionality of the removed admin maintenance endpoint.
@@ -88,7 +89,8 @@ def upgrade():
def downgrade():
"""No downgrade operation needed as we're just creating tasks.
"""
No downgrade operation needed as we're just creating tasks.
The tasks themselves will be processed and completed.
If needed, we could delete tasks with this processor type, but that's not necessary

140
enterprise/poetry.lock generated
View File

@@ -766,7 +766,7 @@ version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
groups = ["main", "test"]
groups = ["main"]
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@@ -836,7 +836,6 @@ files = [
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
[package.dependencies]
pycparser = "*"
@@ -1902,25 +1901,25 @@ files = [
[[package]]
name = "fastapi"
version = "0.117.1"
version = "0.116.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552"},
{file = "fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a"},
{file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
{file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.49.0"
starlette = ">=0.40.0,<0.48.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastjsonschema"
@@ -2292,72 +2291,6 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""]
tqdm = ["tqdm"]
[[package]]
name = "gevent"
version = "25.9.1"
description = "Coroutine-based network library"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"},
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"},
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"},
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"},
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"},
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"},
{file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"},
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"},
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"},
{file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"},
{file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"},
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"},
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"},
{file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"},
{file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"},
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"},
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"},
{file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"},
{file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"},
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"},
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"},
{file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"},
{file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"},
{file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"},
{file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"},
{file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"},
{file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"},
]
[package.dependencies]
cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""}
"zope.event" = "*"
"zope.interface" = "*"
[package.extras]
dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""]
docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"]
[[package]]
name = "gitdb"
version = "4.0.12"
@@ -2774,7 +2707,7 @@ version = "3.2.4"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.9"
groups = ["main", "test"]
groups = ["main"]
files = [
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
@@ -2831,7 +2764,6 @@ files = [
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
]
markers = {test = "platform_python_implementation == \"CPython\""}
[package.extras]
docs = ["Sphinx", "furo"]
@@ -5431,7 +5363,7 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]]
name = "openhands-ai"
version = "0.57.0"
version = "0.55.0"
description = "OpenHands: Code Less, Make More"
optional = false
python-versions = "^3.12,<3.14"
@@ -5465,7 +5397,7 @@ json-repair = "*"
jupyter_kernel_gateway = "*"
kubernetes = "^33.1.0"
libtmux = ">=0.37,<0.40"
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*"
litellm = "^1.74.3, !=1.64.4, !=1.67.*"
memory-profiler = "^0.61.0"
numpy = "*"
openai = "1.99.9"
@@ -5474,7 +5406,6 @@ opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1"
pexpect = "*"
pillow = "^11.3.0"
poetry = "^2.1.2"
prompt-toolkit = "^3.0.50"
protobuf = "^5.0.0,<6.0.0"
@@ -5482,7 +5413,6 @@ psutil = "*"
pygithub = "^2.5.0"
pyjwt = "^2.9.0"
pylatexenc = "*"
pypdf = "^6.0.0"
PyPDF2 = "*"
python-docx = "*"
python-dotenv = "*"
@@ -5496,17 +5426,13 @@ pyyaml = "^6.0.2"
qtconsole = "^5.6.1"
rapidfuzz = "^3.9.0"
redis = ">=5.2,<7.0"
requests = "^2.32.5"
setuptools = ">=78.1.1"
shellingham = "^1.5.4"
sse-starlette = "^3.0.2"
starlette = "^0.48.0"
sse-starlette = "^2.1.3"
tenacity = ">=8.5,<10.0"
termcolor = "*"
toml = "*"
tornado = "*"
types-toml = "*"
urllib3 = "^2.5.0"
uvicorn = "*"
whatthepatch = "^1.0.6"
zope-interface = "7.2"
@@ -6545,12 +6471,11 @@ version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "test"]
groups = ["main"]
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
[[package]]
name = "pydantic"
@@ -8340,7 +8265,7 @@ version = "80.9.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
groups = ["main", "test"]
groups = ["main"]
files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
@@ -8706,14 +8631,14 @@ sqlcipher = ["sqlcipher3_binary"]
[[package]]
name = "sse-starlette"
version = "3.0.2"
version = "2.4.1"
description = "SSE plugin for Starlette"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"},
{file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"},
{file = "sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a"},
{file = "sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926"},
]
[package.dependencies]
@@ -8721,7 +8646,7 @@ anyio = ">=4.7.0"
[package.extras]
daphne = ["daphne (>=4.2.0)"]
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio,examples] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
granian = ["granian (>=2.3.1)"]
uvicorn = ["uvicorn (>=0.34.0)"]
@@ -8777,14 +8702,14 @@ files = [
[[package]]
name = "starlette"
version = "0.48.0"
version = "0.47.3"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659"},
{file = "starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46"},
{file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"},
{file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"},
]
[package.dependencies]
@@ -9913,32 +9838,13 @@ enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[[package]]
name = "zope-event"
version = "6.0"
description = "Very basic event publishing system"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3"},
{file = "zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92"},
]
[package.dependencies]
setuptools = ">=75.8.2"
[package.extras]
docs = ["Sphinx"]
test = ["zope.testrunner (>=6.4)"]
[[package]]
name = "zope-interface"
version = "7.2"
description = "Interfaces for Python"
optional = false
python-versions = ">=3.8"
groups = ["main", "test"]
groups = ["main"]
files = [
{file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
{file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
@@ -10102,4 +10008,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "8c460070dce6bdec5ee0ee7bc0c2246fcf2602d1e64a0867b4f5e3a0e334fe93"
content-hash = "5771671ef2acc36e7b0931c73fa035ca1d329e8dac6827f7a349e1a569c3fd23"

View File

@@ -63,7 +63,6 @@ openai = "*"
opencv-python = "*"
pandas = "*"
reportlab = "*"
gevent = ">=24.2.1,<26.0.0"
[tool.poetry-dynamic-versioning]
enable = true
@@ -86,7 +85,3 @@ lint.pydocstyle.convention = "google"
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
[tool.coverage.run]
relative_files = true
omit = [ "tests/*" ]

View File

@@ -19,10 +19,7 @@ from server.auth.constants import ( # noqa: E402
from server.constants import PERMITTED_CORS_ORIGINS # noqa: E402
from server.logger import logger # noqa: E402
from server.metrics import metrics_app # noqa: E402
from server.middleware import ( # noqa: E402
LLMSettingsMiddleware,
SetAuthCookieMiddleware,
)
from server.middleware import SetAuthCookieMiddleware # noqa: E402
from server.rate_limit import setup_rate_limit_handler # noqa: E402
from server.routes.api_keys import api_router as api_keys_router # noqa: E402
from server.routes.auth import api_router, oauth_router # noqa: E402
@@ -108,7 +105,6 @@ base_app.add_middleware(
allow_headers=['*'],
)
base_app.add_middleware(CacheControlMiddleware)
base_app.middleware('http')(LLMSettingsMiddleware())
base_app.middleware('http')(SetAuthCookieMiddleware())
base_app.mount('/', SPAStaticFiles(directory=directory, html=True), name='dist')

View File

@@ -31,7 +31,6 @@ class GoogleSheetsClient:
self, spreadsheet_id: str, range_name: str
) -> Optional[List[str]]:
"""Get usernames from cache if available and not expired.
Args:
spreadsheet_id: The ID of the Google Sheet
range_name: The A1 notation of the range to fetch
@@ -57,7 +56,6 @@ class GoogleSheetsClient:
self, spreadsheet_id: str, range_name: str, usernames: List[str]
) -> None:
"""Update cache with new usernames and current timestamp.
Args:
spreadsheet_id: The ID of the Google Sheet
range_name: The A1 notation of the range to fetch
@@ -69,7 +67,6 @@ class GoogleSheetsClient:
def get_usernames(self, spreadsheet_id: str, range_name: str = 'A:A') -> List[str]:
"""Get list of usernames from specified Google Sheet.
Uses cached data if available and less than 15 seconds old.
Args:
spreadsheet_id: The ID of the Google Sheet
range_name: The A1 notation of the range to fetch

View File

@@ -483,7 +483,8 @@ class ClusteredConversationManager(StandaloneConversationManager):
await pipe.execute()
async def _disconnect_from_stopped(self):
"""Handle connections to conversations that have stopped unexpectedly.
"""
Handle connections to conversations that have stopped unexpectedly.
This method detects when a local connection is pointing to a conversation
that was running on another server that has crashed or been terminated

View File

@@ -70,7 +70,8 @@ PERMITTED_CORS_ORIGINS = [
def build_litellm_proxy_model_path(model_name: str) -> str:
"""Build the LiteLLM proxy model path based on environment and model name.
"""
Build the LiteLLM proxy model path based on environment and model name.
This utility constructs the full model path for LiteLLM proxy based on:
- Environment type (staging vs prod)
@@ -82,6 +83,7 @@ def build_litellm_proxy_model_path(model_name: str) -> str:
Returns:
The full LiteLLM proxy model path (e.g., 'litellm_proxy/prod/claude-3-7-sonnet-20250219')
"""
if 'prod' in model_name or 'litellm' in model_name or 'proxy' in model_name:
raise ValueError("Only include model name, don't include prefix")
@@ -94,7 +96,8 @@ def build_litellm_proxy_model_path(model_name: str) -> str:
def get_default_litellm_model():
"""Construct proxy for litellm model based on user settings and environment type (staging vs prod)
"""
Construct proxy for litellm model based on user settings and environment type (staging vs prod)
if not set explicitly
"""
if LITELLM_DEFAULT_MODEL:

View File

@@ -25,7 +25,8 @@ from openhands.server.shared import conversation_manager
class GithubCallbackProcessor(ConversationCallbackProcessor):
"""Processor for sending conversation summaries to GitHub.
"""
Processor for sending conversation summaries to GitHub.
This processor is used to send summaries of conversations to GitHub issues/PRs
when agent state changes occur.
@@ -35,7 +36,8 @@ class GithubCallbackProcessor(ConversationCallbackProcessor):
send_summary_instruction: bool = True
async def _send_message_to_github(self, message: str) -> None:
"""Send a message to GitHub.
"""
Send a message to GitHub.
Args:
message: The message content to send to GitHub
@@ -66,7 +68,8 @@ class GithubCallbackProcessor(ConversationCallbackProcessor):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event by sending a summary to GitHub.
"""
Process a conversation event by sending a summary to GitHub.
Args:
callback: The conversation callback

View File

@@ -28,7 +28,8 @@ gitlab_manager = GitlabManager(token_manager)
class GitlabCallbackProcessor(ConversationCallbackProcessor):
"""Processor for sending conversation summaries to GitLab.
"""
Processor for sending conversation summaries to GitLab.
This processor is used to send summaries of conversations to GitLab
when agent state changes occur.
@@ -38,7 +39,8 @@ class GitlabCallbackProcessor(ConversationCallbackProcessor):
send_summary_instruction: bool = True
async def _send_message_to_gitlab(self, message: str) -> None:
"""Send a message to GitLab.
"""
Send a message to GitLab.
Args:
message: The message content to send to GitLab
@@ -65,7 +67,8 @@ class GitlabCallbackProcessor(ConversationCallbackProcessor):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event by sending a summary to GitLab.
"""
Process a conversation event by sending a summary to GitLab.
Args:
callback: The conversation callback

View File

@@ -26,7 +26,8 @@ integration_store = jira_manager.integration_store
class JiraCallbackProcessor(ConversationCallbackProcessor):
"""Processor for sending conversation summaries to Jira.
"""
Processor for sending conversation summaries to Jira.
This processor is used to send summaries of conversations to Jira issues
when agent state changes occur.
@@ -36,7 +37,8 @@ class JiraCallbackProcessor(ConversationCallbackProcessor):
workspace_name: str
async def _send_comment_to_jira(self, message: str) -> None:
"""Send a comment to Jira issue.
"""
Send a comment to Jira issue.
Args:
message: The message content to send to Jira
@@ -77,7 +79,8 @@ class JiraCallbackProcessor(ConversationCallbackProcessor):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event by sending a summary to Jira.
"""
Process a conversation event by sending a summary to Jira.
Args:
callback: The conversation callback

View File

@@ -25,7 +25,8 @@ jira_dc_manager = JiraDcManager(token_manager)
class JiraDcCallbackProcessor(ConversationCallbackProcessor):
"""Processor for sending conversation summaries to Jira DC.
"""
Processor for sending conversation summaries to Jira DC.
This processor is used to send summaries of conversations to Jira DC issues
when agent state changes occur.
@@ -36,7 +37,8 @@ class JiraDcCallbackProcessor(ConversationCallbackProcessor):
base_api_url: str
async def _send_comment_to_jira_dc(self, message: str) -> None:
"""Send a comment to Jira DC issue.
"""
Send a comment to Jira DC issue.
Args:
message: The message content to send to Jira DC
@@ -78,7 +80,8 @@ class JiraDcCallbackProcessor(ConversationCallbackProcessor):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event by sending a summary to Jira DC.
"""
Process a conversation event by sending a summary to Jira DC.
Args:
callback: The conversation callback

View File

@@ -24,7 +24,8 @@ linear_manager = LinearManager(token_manager)
class LinearCallbackProcessor(ConversationCallbackProcessor):
"""Processor for sending conversation summaries to Linear.
"""
Processor for sending conversation summaries to Linear.
This processor is used to send summaries of conversations to Linear issues
when agent state changes occur.
@@ -35,7 +36,8 @@ class LinearCallbackProcessor(ConversationCallbackProcessor):
workspace_name: str
async def _send_comment_to_linear(self, message: str) -> None:
"""Send a comment to Linear issue.
"""
Send a comment to Linear issue.
Args:
message: The message content to send to Linear
@@ -77,7 +79,8 @@ class LinearCallbackProcessor(ConversationCallbackProcessor):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event by sending a summary to Linear.
"""
Process a conversation event by sending a summary to Linear.
Args:
callback: The conversation callback

View File

@@ -26,7 +26,8 @@ slack_manager = SlackManager(token_manager)
class SlackCallbackProcessor(ConversationCallbackProcessor):
"""Processor for sending conversation summaries to Slack.
"""
Processor for sending conversation summaries to Slack.
This processor is used to send summaries of conversations to Slack channels
when agent state changes occur.
@@ -40,7 +41,8 @@ class SlackCallbackProcessor(ConversationCallbackProcessor):
last_user_msg_id: int | None = None
async def _send_message_to_slack(self, message: str) -> None:
"""Send a message to Slack using the conversation_manager's send_to_event_stream method.
"""
Send a message to Slack using the conversation_manager's send_to_event_stream method.
Args:
message: The message content to send to Slack
@@ -81,7 +83,8 @@ class SlackCallbackProcessor(ConversationCallbackProcessor):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event by sending a summary to Slack.
"""
Process a conversation event by sending a summary to Slack.
Args:
conversation_id: The ID of the conversation to process

View File

@@ -33,7 +33,8 @@ class LegacyCacheEntry:
@dataclass
class LegacyConversationManager(ConversationManager):
"""Conversation manager for use while migrating - since existing conversations are not nested!
"""
Conversation manager for use while migrating - since existing conversations are not nested!
Separate class from SaasNestedConversationManager so it can be easliy removed in a few weeks.
(As of 2025-07-23)
"""
@@ -269,7 +270,8 @@ class LegacyConversationManager(ConversationManager):
del self._legacy_cache[key]
async def should_start_in_legacy_mode(self, conversation_id: str) -> bool:
"""Check if a conversation should run in legacy mode by directly checking the runtime.
"""
Check if a conversation should run in legacy mode by directly checking the runtime.
The /list method does not include stopped conversations even though the PVC for these
may not yet have been deleted, so we need to check /sessions/{session_id} directly.
"""
@@ -293,7 +295,8 @@ class LegacyConversationManager(ConversationManager):
return is_legacy
def is_legacy_runtime(self, runtime: dict | None) -> bool:
"""Determine if a runtime is a legacy runtime based on its command.
"""
Determine if a runtime is a legacy runtime based on its command.
Args:
runtime: The runtime dictionary or None if not found

View File

@@ -59,9 +59,11 @@ def setup_json_logger(
level: str = LOG_LEVEL,
_out: TextIO = sys.stdout,
) -> None:
"""Configure logger instance to output json for Google Cloud.
"""
Configure logger instance to output json for Google Cloud.
Existing filters should stay in place for sensitive content.
"""
# Remove existing handlers to avoid duplicate logs
for handler in logger.handlers[:]:
logger.removeHandler(handler)
@@ -82,7 +84,8 @@ def setup_json_logger(
def setup_all_loggers():
"""Setup JSON logging for all libraries that may be logging.
"""
Setup JSON logging for all libraries that may be logging.
Leave OpenHands alone since it's already configured.
"""
if LOG_JSON:

View File

@@ -13,7 +13,8 @@ from openhands.core.config import load_openhands_config
class UserVersionUpgradeProcessor(MaintenanceTaskProcessor):
"""Processor for upgrading user settings to the current version.
"""
Processor for upgrading user settings to the current version.
This processor takes a list of user IDs and upgrades any users
whose user_version is less than CURRENT_USER_SETTINGS_VERSION.
@@ -22,7 +23,8 @@ class UserVersionUpgradeProcessor(MaintenanceTaskProcessor):
user_ids: List[str]
async def __call__(self, task: MaintenanceTask) -> dict:
"""Process user version upgrades for the specified user IDs.
"""
Process user version upgrades for the specified user IDs.
Args:
task: The maintenance task being processed

View File

@@ -27,7 +27,8 @@ class SaaSOpenHandsMCPConfig(OpenHandsMCPConfig):
def create_default_mcp_server_config(
host: str, config: 'OpenHandsConfig', user_id: str | None = None
) -> tuple[MCPSHTTPServerConfig | None, list[MCPStdioServerConfig]]:
"""Create a default MCP server configuration.
"""
Create a default MCP server configuration.
Args:
host: Host string
@@ -35,6 +36,7 @@ class SaaSOpenHandsMCPConfig(OpenHandsMCPConfig):
Returns:
A tuple containing the default SSE server configuration and a list of MCP stdio server configurations
"""
api_key_store = ApiKeyStore.get_instance()
if user_id:
api_key = api_key_store.retrieve_mcp_api_key(user_id)

View File

@@ -33,7 +33,8 @@ def metrics_app() -> Callable:
metrics_callable = make_asgi_app()
async def wrapped_handler(scope, receive, send):
"""Call _update_metrics before serving Prometheus metrics endpoint.
"""
Call _update_metrics before serving Prometheus metrics endpoint.
Not wrapped in a `try`, failing would make metrics endpoint unavailable.
"""
await _update_metrics()

View File

@@ -1,8 +1,7 @@
from datetime import UTC, datetime
from typing import Callable
import jwt
from fastapi import HTTPException, Request, Response, status
from fastapi import Request, Response, status
from fastapi.responses import JSONResponse
from pydantic import SecretStr
from server.auth.auth_error import (
@@ -13,25 +12,21 @@ from server.auth.auth_error import (
)
from server.auth.gitlab_sync import schedule_gitlab_repo_sync
from server.auth.saas_user_auth import SaasUserAuth, token_manager
from server.constants import get_default_litellm_model
from server.routes.auth import (
get_cookie_domain,
get_cookie_samesite,
set_response_cookie,
)
from storage.database import session_maker
from storage.subscription_access import SubscriptionAccess
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth.user_auth import AuthType, get_user_auth
from openhands.server.utils import config
from openhands.storage.data_models.settings import Settings
class SetAuthCookieMiddleware:
"""Update the auth cookie with the current authentication state if it was refreshed before sending response to user.
Deleting invalid cookies is handled by CookieError using FastAPIs standard error handling mechanism.
"""
Update the auth cookie with the current authentication state if it was refreshed before sending response to user.
Deleting invalid cookies is handled by CookieError using FastAPIs standard error handling mechanism
"""
async def __call__(self, request: Request, call_next: Callable):
@@ -177,247 +172,3 @@ class SetAuthCookieMiddleware:
await token_manager.logout(user_auth.refresh_token.get_secret_value())
except Exception:
logger.debug('Error logging out')
class LLMSettingsMiddleware:
"""Middleware to validate LLM settings access for enterprise users.
Intercepts POST requests to /api/settings and validates that non-pro users
cannot modify LLM-related settings.
"""
async def __call__(self, request: Request, call_next: Callable):
try:
logger.warning(
f'LLM middleware called for {request.method} {request.url.path}'
)
# Check if this is a POST request to /api/settings
if request.method == 'POST' and request.url.path == '/api/settings':
logger.warning('LLM middleware intercepting POST /api/settings request')
await self._validate_llm_settings_request(request)
# Continue with the request
response: Response = await call_next(request)
return response
except HTTPException:
# Re-raise HTTPException (our 403 response)
raise
except Exception as e:
logger.warning(f'Error in LLM settings middleware: {e}')
# Let other errors pass through to be handled by the route
fallback_response: Response = await call_next(request)
return fallback_response
async def _validate_llm_settings_request(self, request: Request) -> None:
"""Validate LLM settings access for the current request."""
try:
logger.info(
f"LLM settings middleware intercepting POST /api/settings from {request.client.host if request.client else 'unknown'}"
)
# Get user authentication - this will trigger authentication if not already done
try:
user_auth = await get_user_auth(request)
except Exception as e:
logger.info(f'No valid user auth found ({e}), letting route handle request')
return # No user auth, let the route handle it
user_id = await user_auth.get_user_id()
if not user_id:
logger.info('No user ID found, letting route handle request')
return # No user ID, let the route handle it
logger.info(f'Processing settings request for user: {user_id}')
# Parse the request JSON to get new settings
try:
settings_data = await request.json()
logger.info(f'Parsed settings data keys: {list(settings_data.keys())}')
except Exception as e:
logger.warning(f'Invalid JSON in request body: {e}')
return # Invalid JSON, let the route handle it
# Convert to Settings object for validation
try:
new_settings = Settings(**settings_data)
logger.info('Successfully created Settings object from request data')
except Exception as e:
logger.warning(f'Invalid settings format: {e}')
return # Invalid settings format, let the route handle it
# Validate LLM settings access by comparing new settings against SaaS defaults
await validate_llm_settings_access(user_id, new_settings)
logger.info(f'LLM settings validation passed for user {user_id}')
except HTTPException as e:
logger.warning(
f'LLM settings validation failed: HTTP {e.status_code} - {e.detail}'
)
# Re-raise our 403 response
raise
except Exception as e:
logger.warning(f'Unexpected error validating LLM settings request: {e}')
# Let other errors pass through
def _get_saas_default_settings() -> Settings:
"""Get the default SaaS settings for comparison."""
return Settings(
language='en',
agent='CodeActAgent',
enable_proactive_conversation_starters=True,
enable_default_condenser=True,
condenser_max_size=120,
llm_model=get_default_litellm_model(), # litellm_proxy/prod/claude-sonnet-4-20250514
confirmation_mode=False,
security_analyzer='llm',
# Note: llm_api_key and llm_base_url are auto-provisioned for SaaS users,
# so we don't include them in defaults - any custom values are changes
)
def has_llm_settings_changes(user_settings: Settings, saas_defaults: Settings) -> bool:
"""Check if user settings contain changes to LLM-related settings from SaaS defaults."""
logger.info(
f"Checking LLM settings changes - User settings: {user_settings.model_dump(exclude={'secrets_store'})}"
)
logger.info(
f"Checking LLM settings changes - SaaS defaults: {saas_defaults.model_dump(exclude={'secrets_store'})}"
)
# Core LLM settings - any custom values are changes since SaaS auto-provisions these
if (
user_settings.llm_model is not None
and user_settings.llm_model != saas_defaults.llm_model
):
logger.warning(
f"LLM model change detected: user='{user_settings.llm_model}' vs default='{saas_defaults.llm_model}'"
)
return True
if user_settings.llm_api_key is not None:
# Any custom API key is a change (SaaS users get auto-provisioned keys)
logger.warning(
f'LLM API key change detected: user has custom key (length={len(user_settings.llm_api_key.get_secret_value()) if user_settings.llm_api_key else 0})'
)
return True
if user_settings.llm_base_url is not None and user_settings.llm_base_url != '':
# Any non-empty base URL is a change (SaaS users get auto-provisioned URL)
logger.warning(
f"LLM base URL change detected: user='{user_settings.llm_base_url}' (non-empty)"
)
return True
# LLM-related configuration settings
if user_settings.agent is not None and user_settings.agent != saas_defaults.agent:
logger.warning(
f"Agent change detected: user='{user_settings.agent}' vs default='{saas_defaults.agent}'"
)
return True
if (
user_settings.confirmation_mode is not None
and user_settings.confirmation_mode != saas_defaults.confirmation_mode
):
logger.warning(
f'Confirmation mode change detected: user={user_settings.confirmation_mode} vs default={saas_defaults.confirmation_mode}'
)
return True
if (
user_settings.security_analyzer is not None
and user_settings.security_analyzer != saas_defaults.security_analyzer
and user_settings.security_analyzer != ''
): # Handle empty string as None
logger.warning(
f"Security analyzer change detected: user='{user_settings.security_analyzer}' vs default='{saas_defaults.security_analyzer}'"
)
return True
if user_settings.max_budget_per_task is not None:
logger.warning(
f'Max budget per task change detected: user={user_settings.max_budget_per_task}'
)
return True
if user_settings.max_iterations is not None:
logger.warning(
f'Max iterations change detected: user={user_settings.max_iterations}'
)
return True
# Memory/context management settings
if user_settings.enable_default_condenser != saas_defaults.enable_default_condenser:
logger.warning(
f'Enable default condenser change detected: user={user_settings.enable_default_condenser} vs default={saas_defaults.enable_default_condenser}'
)
return True
if (
user_settings.condenser_max_size is not None
and user_settings.condenser_max_size != saas_defaults.condenser_max_size
):
logger.warning(
f'Condenser max size change detected: user={user_settings.condenser_max_size} vs default={saas_defaults.condenser_max_size}'
)
return True
logger.info('No LLM settings changes detected')
return False
def _has_active_subscription(user_id: str) -> bool:
"""Check if user has an active subscription (pro user)."""
with session_maker() as session:
now = datetime.now(UTC)
logger.info(f'Checking subscription for user {user_id} at time {now}')
subscription_access = (
session.query(SubscriptionAccess)
.filter(SubscriptionAccess.status == 'ACTIVE')
.filter(SubscriptionAccess.user_id == user_id)
.filter(SubscriptionAccess.start_at <= now)
.filter(SubscriptionAccess.end_at >= now)
.first()
)
if subscription_access:
logger.info(
f'Found active subscription for user {user_id}: starts={subscription_access.start_at}, ends={subscription_access.end_at}'
)
else:
logger.info(f'No active subscription found for user {user_id}')
return subscription_access is not None
async def validate_llm_settings_access(
user_id: str, user_settings: Settings, saas_defaults: Settings | None = None
) -> None:
"""Validate that user has permission to change LLM settings.
Raises HTTPException with 403 status if non-pro user tries to change LLM settings.
"""
if saas_defaults is None:
saas_defaults = _get_saas_default_settings()
logger.info(f'Validating LLM settings access for user: {user_id}')
# Check if user is trying to change LLM settings
if has_llm_settings_changes(user_settings, saas_defaults):
logger.warning(f'User {user_id} attempting to change LLM settings')
# Check if user has active subscription (is pro user)
has_subscription = _has_active_subscription(user_id)
logger.info(
f"User {user_id} subscription status: {'active' if has_subscription else 'none'}"
)
if not has_subscription:
logger.warning(
f'Blocking non-pro user {user_id} from changing LLM settings'
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='LLM settings can only be modified by pro users',
)
else:
logger.info(f'Allowing pro user {user_id} to change LLM settings')
else:
logger.info(f'User {user_id} making non-LLM settings changes only - allowing')

View File

@@ -1,4 +1,5 @@
"""Usage:
"""
Usage:
Call setup_rate_limit_handler on your FastAPI app to add the exception handler
@@ -22,7 +23,9 @@ from openhands.core.logger import openhands_logger as logger
def setup_rate_limit_handler(app: Starlette):
"""Add exception handler that"""
"""
Add exception handler that
"""
app.add_exception_handler(RateLimitException, _rate_limit_exceeded_handler)
@@ -53,7 +56,8 @@ class RateLimiter:
self.limit_items = limits.parse_many(windows)
async def hit(self, namespace: str, key: str):
"""Raises RateLimitException when limit is hit.
"""
Raises RateLimitException when limit is hit.
Logs and swallows exceptions and logs if lookup fails.
"""
for lim in self.limit_items:
@@ -76,7 +80,9 @@ class RateLimiter:
async def _get_stats_as_result(
self, lim: limits.RateLimitItem, namespace: str, key: str
) -> RateLimitResult:
"""Lookup rate limit window stats and return a RateLimitResult with the data needed for response headers."""
"""
Lookup rate limit window stats and return a RateLimitResult with the data needed for response headers.
"""
stats: limits.WindowStats = await self.strategy.get_window_stats(
lim, namespace, key
)
@@ -91,7 +97,8 @@ class RateLimiter:
def create_redis_rate_limiter(windows: str) -> RateLimiter:
"""Create a RateLimiter with the Redis backend and "Fixed Window" strategy.
"""
Create a RateLimiter with the Redis backend and "Fixed Window" strategy.
windows arg example: "10/second; 100/minute"
"""
backend = limits.aio.storage.RedisStorage(f'async+{get_redis_authed_url()}')
@@ -100,7 +107,9 @@ def create_redis_rate_limiter(windows: str) -> RateLimiter:
class RateLimitException(HTTPException):
"""exception raised when a rate limit is hit."""
"""
exception raised when a rate limit is hit.
"""
result: RateLimitResult
@@ -112,7 +121,9 @@ class RateLimitException(HTTPException):
def _rate_limit_exceeded_handler(request: Request, exc: Exception) -> Response:
"""Build a simple JSON response that includes the details of the rate limit that was hit."""
"""
Build a simple JSON response that includes the details of the rate limit that was hit.
"""
logger.info(exc.__class__.__name__)
if isinstance(exc, RateLimitException):
response = JSONResponse(

View File

@@ -17,7 +17,8 @@ ADD_DEBUGGING_ROUTES = os.environ.get('ADD_DEBUGGING_ROUTES') in ('1', 'true')
def add_debugging_routes(api: FastAPI):
"""# HERE BE DRAGONS!
"""
# HERE BE DRAGONS!
Chaos scripts for debugging and stress testing the system.
This module contains endpoints that deliberately stress test and potentially break
@@ -30,6 +31,7 @@ def add_debugging_routes(api: FastAPI):
- Testing async vs sync database access patterns
- Simulating event loop blocking
"""
if not ADD_DEBUGGING_ROUTES:
return
@@ -37,7 +39,8 @@ def add_debugging_routes(api: FastAPI):
@chaos_router.get('/pool-stats')
def pool_stats() -> dict[str, int]:
"""Returns current database connection pool statistics.
"""
Returns current database connection pool statistics.
This endpoint provides real-time metrics about the SQLAlchemy connection pool:
- checked_in: Number of connections currently available in the pool
@@ -52,7 +55,8 @@ def add_debugging_routes(api: FastAPI):
@chaos_router.get('/test-db')
def test_db(num_tests: int = 10, delay: int = 1) -> str:
"""Stress tests the database connection pool using multiple threads.
"""
Stress tests the database connection pool using multiple threads.
Creates multiple threads that each open a database connection, perform a query,
hold the connection for the specified delay, and then release it.
@@ -73,7 +77,8 @@ def add_debugging_routes(api: FastAPI):
@chaos_router.get('/a-test-db')
async def a_chaos_monkey(num_tests: int = 10, delay: int = 1) -> str:
"""Stress tests the async database connection pool.
"""
Stress tests the async database connection pool.
Similar to /test-db but uses async connections and coroutines instead of threads.
This endpoint helps compare the behavior of async vs sync connection pools
@@ -88,7 +93,8 @@ def add_debugging_routes(api: FastAPI):
@chaos_router.get('/lock-main-runloop')
async def lock_main_runloop(duration: int = 10) -> str:
"""Deliberately blocks the main asyncio event loop.
"""
Deliberately blocks the main asyncio event loop.
This endpoint uses a synchronous sleep operation in an async function,
which blocks the entire FastAPI server's event loop for the specified duration.
@@ -107,7 +113,8 @@ def add_debugging_routes(api: FastAPI):
def _db_check(delay: int):
"""Executes a single request against the database with an artificial delay.
"""
Executes a single request against the database with an artificial delay.
This helper function:
1. Opens a database connection from the pool
@@ -134,7 +141,8 @@ def _db_check(delay: int):
async def _a_db_check(delay: int):
"""Executes a single async request against the database with an artificial delay.
"""
Executes a single async request against the database with an artificial delay.
This is the async version of _db_check that:
1. Opens an async database connection from the pool

View File

@@ -73,7 +73,8 @@ class FeedbackRequest(BaseModel):
@router.post('/conversation', status_code=status.HTTP_201_CREATED)
async def submit_conversation_feedback(feedback: FeedbackRequest):
"""Submit feedback for a conversation.
"""
Submit feedback for a conversation.
This endpoint accepts a rating (1-5) and optional reason for the feedback.
The feedback is associated with a specific conversation and optionally a specific event.
@@ -107,7 +108,8 @@ async def submit_conversation_feedback(feedback: FeedbackRequest):
@router.get('/conversation/{conversation_id}/batch')
async def get_batch_feedback(conversation_id: str, user_id: str = Depends(get_user_id)):
"""Get feedback for all events in a conversation.
"""
Get feedback for all events in a conversation.
Returns feedback status for each event, including whether feedback exists
and if so, the rating and reason.

View File

@@ -16,7 +16,8 @@ GITHUB_PROXY_ENDPOINTS = bool(os.environ.get('GITHUB_PROXY_ENDPOINTS'))
def add_github_proxy_routes(app: FastAPI):
"""Authentication endpoints for feature branches.
"""
Authentication endpoints for feature branches.
# Requirements
* This should never be enabled in prod!

View File

@@ -23,10 +23,14 @@ AGENT_SESSION_START_HISTOGRAM = Histogram(
class SaaSMonitoringListener(MonitoringListener):
"""Forward app signals to Prometheus."""
"""
Forward app signals to Prometheus.
"""
def on_session_event(self, event: Event) -> None:
"""Track metrics about events being added to a Session's EventStream."""
"""
Track metrics about events being added to a Session's EventStream.
"""
if (
isinstance(event, AgentStateChangedObservation)
and event.agent_state == AgentState.ERROR
@@ -38,7 +42,8 @@ class SaaSMonitoringListener(MonitoringListener):
)
def on_agent_session_start(self, success: bool, duration: float) -> None:
"""Track an agent session start.
"""
Track an agent session start.
Success is true if startup completed without error.
Duration is start time in seconds observed by AgentSession.
"""
@@ -53,7 +58,8 @@ class SaaSMonitoringListener(MonitoringListener):
)
def on_create_conversation(self) -> None:
"""Track the beginning of conversation creation.
"""
Track the beginning of conversation creation.
Does not currently capture whether it succeed.
"""
CREATE_CONVERSATION_COUNT.inc()

View File

@@ -131,7 +131,9 @@ class SaasNestedConversationManager(ConversationManager):
async def get_running_agent_loops(
self, user_id: str | None = None, filter_to_sids: set[str] | None = None
) -> set[str]:
"""Get the running agent loops directly from the remote runtime."""
"""
Get the running agent loops directly from the remote runtime.
"""
conversation_ids = await self._get_all_running_conversation_ids()
if filter_to_sids is not None:
@@ -480,7 +482,10 @@ class SaasNestedConversationManager(ConversationManager):
)
def _get_user_id_from_conversation(self, conversation_id: str) -> str:
"""Get user_id from conversation_id."""
"""
Get user_id from conversation_id.
"""
with session_maker() as session:
conversation_metadata = (
session.query(StoredConversationMetadata)

View File

@@ -34,7 +34,8 @@ file_store = get_file_store(config.file_store, config.file_store_path)
async def process_event(
user_id: str, conversation_id: str, subpath: str, content: dict
):
"""Process a conversation event and invoke any registered callbacks.
"""
Process a conversation event and invoke any registered callbacks.
Args:
user_id: The user ID associated with the conversation
@@ -71,7 +72,8 @@ async def process_event(
async def invoke_conversation_callbacks(
conversation_id: str, observation: AgentStateChangedObservation
):
"""Load and invoke all active callbacks for a conversation.
"""
Load and invoke all active callbacks for a conversation.
Args:
conversation_id: The conversation ID to process callbacks for
@@ -117,7 +119,8 @@ async def invoke_conversation_callbacks(
def update_conversation_metadata(conversation_id: str, content: dict):
"""Update conversation metadata with new content.
"""
Update conversation metadata with new content.
Args:
conversation_id: The conversation ID to update
@@ -156,7 +159,8 @@ def update_conversation_metadata(conversation_id: str, content: dict):
def register_callback_processor(
conversation_id: str, processor: ConversationCallbackProcessor
) -> int:
"""Register a callback processor for a conversation.
"""
Register a callback processor for a conversation.
Args:
conversation_id: The conversation ID to register the callback for
@@ -178,7 +182,8 @@ def register_callback_processor(
def update_active_working_seconds(
event_store: EventStore, conversation_id: str, user_id: str, file_store: FileStore
):
"""Calculate and update the total active working seconds for a conversation.
"""
Calculate and update the total active working seconds for a conversation.
This function reads all events for the conversation, looks for AgentStateChanged
observations, and calculates the total time spent in a running state.
@@ -258,7 +263,8 @@ def update_active_working_seconds(
def update_agent_state(user_id: str, conversation_id: str, content: bytes):
"""Update agent state file for a conversation.
"""
Update agent state file for a conversation.
Args:
user_id: The user ID associated with the conversation

View File

@@ -3,7 +3,9 @@ from storage.base import Base
class ApiKey(Base):
"""Represents an API key for a user."""
"""
Represents an API key for a user.
"""
__tablename__ = 'api_keys'
id = Column(Integer, primary_key=True, autoincrement=True)

View File

@@ -73,7 +73,8 @@ class AuthTokenStore:
]
| None = None,
) -> Dict[str, str | int] | None:
"""Load authentication tokens from the database and refresh them if necessary.
"""
Load authentication tokens from the database and refresh them if necessary.
This method retrieves the current authentication tokens for the user and checks if they have expired.
It uses the provided `check_expiration_and_refresh` function to determine if the tokens need

View File

@@ -1,4 +1,6 @@
"""Unified SQLAlchemy declarative base for all models."""
"""
Unified SQLAlchemy declarative base for all models.
"""
from sqlalchemy.orm import declarative_base

View File

@@ -5,7 +5,8 @@ from storage.base import Base
class BillingSession(Base): # type: ignore
"""Represents a Stripe billing session for credit purchases.
"""
Represents a Stripe billing session for credit purchases.
Tracks the status of payment transactions and associated user information.
"""

View File

@@ -15,7 +15,8 @@ from openhands.utils.import_utils import get_impl
class ConversationCallbackProcessor(BaseModel, ABC):
"""Abstract base class for conversation callback processors.
"""
Abstract base class for conversation callback processors.
Conversation processors are invoked when events occur in a conversation
to perform additional processing, notifications, or integrations.
@@ -34,7 +35,8 @@ class ConversationCallbackProcessor(BaseModel, ABC):
callback: ConversationCallback,
observation: AgentStateChangedObservation,
) -> None:
"""Process a conversation event.
"""
Process a conversation event.
Args:
conversation_id: The ID of the conversation to process
@@ -52,7 +54,9 @@ class CallbackStatus(Enum):
class ConversationCallback(Base): # type: ignore
"""Model for storing conversation callbacks that process conversation events."""
"""
Model for storing conversation callbacks that process conversation events.
"""
__tablename__ = 'conversation_callbacks'
@@ -81,7 +85,8 @@ class ConversationCallback(Base): # type: ignore
)
def get_processor(self) -> ConversationCallbackProcessor:
"""Get the processor instance from the stored processor type and JSON data.
"""
Get the processor instance from the stored processor type and JSON data.
Returns:
ConversationCallbackProcessor: The processor instance
@@ -94,7 +99,8 @@ class ConversationCallback(Base): # type: ignore
return processor
def set_processor(self, processor: ConversationCallbackProcessor) -> None:
"""Set the processor instance, storing its type and JSON representation.
"""
Set the processor instance, storing its type and JSON representation.
Args:
processor: The ConversationCallbackProcessor instance to store

View File

@@ -1,4 +1,5 @@
"""Database model for experiment assignments.
"""
Database model for experiment assignments.
This model tracks which experiments a conversation is assigned to and what variant
they received from PostHog feature flags.

View File

@@ -1,4 +1,5 @@
"""Store for managing experiment assignments.
"""
Store for managing experiment assignments.
This store handles creating and updating experiment assignments for conversations.
"""
@@ -19,7 +20,8 @@ class ExperimentAssignmentStore:
experiment_name: str,
variant: str,
) -> None:
"""Update the variant for a specific experiment.
"""
Update the variant for a specific experiment.
Args:
conversation_id: The conversation ID

View File

@@ -3,7 +3,9 @@ from storage.base import Base
class GithubAppInstallation(Base): # type: ignore
"""Represents a Github App Installation with associated token."""
"""
Represents a Github App Installation with associated token.
"""
__tablename__ = 'github_app_installations'
id = Column(Integer, primary_key=True, autoincrement=True)

View File

@@ -13,7 +13,9 @@ class WebhookStatus(IntEnum):
class GitlabWebhook(Base): # type: ignore
"""Represents a Gitlab webhook configuration for a repository or group."""
"""
Represents a Gitlab webhook configuration for a repository or group.
"""
__tablename__ = 'gitlab_webhook'
id = Column(Integer, primary_key=True, autoincrement=True)

View File

@@ -86,6 +86,7 @@ class GitlabWebhookStore:
Raises:
ValueError: If neither project_id nor group_id is provided, or if both are provided.
"""
resource_type, resource_id = GitlabWebhookStore.determine_resource_type(webhook)
async with self.a_session_maker() as session:
async with session.begin():
@@ -109,6 +110,7 @@ class GitlabWebhookStore:
Raises:
ValueError: If neither project_id nor group_id is provided, or if both are provided.
"""
resource_type, resource_id = GitlabWebhookStore.determine_resource_type(webhook)
logger.info(
@@ -182,6 +184,7 @@ class GitlabWebhookStore:
Returns:
List of GitlabWebhook objects that need processing
"""
async with self.a_session_maker() as session:
query = (
select(GitlabWebhook)
@@ -195,7 +198,9 @@ class GitlabWebhookStore:
return list(webhooks)
async def get_webhook_secret(self, webhook_uuid: str, user_id: str) -> str | None:
"""Get's webhook secret given the webhook uuid and admin keycloak user id"""
"""
Get's webhook secret given the webhook uuid and admin keycloak user id
"""
async with self.a_session_maker() as session:
query = (
select(GitlabWebhook)

View File

@@ -23,6 +23,7 @@ class JiraDcIntegrationStore:
status: str = 'active',
) -> JiraDcWorkspace:
"""Create a new Jira DC workspace with encrypted sensitive data."""
with session_maker() as session:
workspace = JiraDcWorkspace(
name=name.lower(),
@@ -82,6 +83,7 @@ class JiraDcIntegrationStore:
status: str = 'active',
) -> JiraDcUser:
"""Create a new Jira DC workspace link."""
jira_dc_user = JiraDcUser(
keycloak_user_id=keycloak_user_id,
jira_dc_user_id=jira_dc_user_id,
@@ -123,6 +125,7 @@ class JiraDcIntegrationStore:
self, keycloak_user_id: str
) -> Optional[JiraDcUser]:
"""Retrieve user by Keycloak user ID."""
with session_maker() as session:
return (
session.query(JiraDcUser)
@@ -181,6 +184,7 @@ class JiraDcIntegrationStore:
self, keycloak_user_id: str, status: str
) -> JiraDcUser:
"""Update the status of a Jira DC user mapping."""
with session_maker() as session:
user = (
session.query(JiraDcUser)

View File

@@ -24,6 +24,7 @@ class JiraIntegrationStore:
status: str = 'active',
) -> JiraWorkspace:
"""Create a new Jira workspace with encrypted sensitive data."""
workspace = JiraWorkspace(
name=name.lower(),
jira_cloud_id=jira_cloud_id,
@@ -90,6 +91,7 @@ class JiraIntegrationStore:
status: str = 'active',
) -> JiraUser:
"""Create a new Jira workspace link."""
jira_user = JiraUser(
keycloak_user_id=keycloak_user_id,
jira_user_id=jira_user_id,

Some files were not shown because too many files have changed in this diff Show More