@openhands/extensions 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/custom-codereview-guide.md +25 -0
- package/.github/pull_request_template.md +38 -0
- package/.github/release.yml +14 -0
- package/.github/workflows/check-extensions.yml +72 -0
- package/.github/workflows/npm-publish.yml +89 -0
- package/.github/workflows/pr.yml +30 -0
- package/.github/workflows/release.yml +24 -0
- package/.github/workflows/tests.yml +25 -0
- package/.github/workflows/vulnerability-scan.yml +87 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +132 -0
- package/README.md +10 -0
- package/analysis_results.md +162 -0
- package/marketplaces/large-codebase.json +66 -0
- package/marketplaces/openhands-extensions.json +682 -0
- package/package.json +4 -10
- package/plugins/README.md +30 -0
- package/plugins/city-weather/.plugin/plugin.json +13 -0
- package/plugins/city-weather/README.md +145 -0
- package/plugins/city-weather/commands/now.md +56 -0
- package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
- package/plugins/cobol-modernization/README.md +201 -0
- package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
- package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
- package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
- package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
- package/plugins/issue-duplicate-checker/README.md +51 -0
- package/plugins/issue-duplicate-checker/action.yml +349 -0
- package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
- package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
- package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
- package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
- package/plugins/magic-test/.plugin/plugin.json +13 -0
- package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
- package/plugins/migration-scoring/.plugin/plugin.json +19 -0
- package/plugins/migration-scoring/README.md +244 -0
- package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
- package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
- package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
- package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
- package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
- package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
- package/plugins/onboarding/.plugin/plugin.json +20 -0
- package/plugins/onboarding/README.md +30 -0
- package/plugins/onboarding/references/criteria.md +144 -0
- package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
- package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
- package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
- package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
- package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
- package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
- package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
- package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
- package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
- package/plugins/openhands/.plugin/plugin.json +13 -0
- package/plugins/openhands/README.md +52 -0
- package/plugins/openhands/SKILL.md +61 -0
- package/plugins/openhands/commands/create.md +55 -0
- package/plugins/openhands/commands/openhands-cloud.md +8 -0
- package/plugins/openhands/scripts/run.sh +69 -0
- package/plugins/pr-review/.plugin/plugin.json +13 -0
- package/plugins/pr-review/README.md +393 -0
- package/plugins/pr-review/action.yml +298 -0
- package/plugins/pr-review/scripts/agent_script.py +1282 -0
- package/plugins/pr-review/scripts/evaluate_review.py +655 -0
- package/plugins/pr-review/scripts/prompt.py +260 -0
- package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
- package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
- package/plugins/qa-changes/.plugin/plugin.json +11 -0
- package/plugins/qa-changes/README.md +185 -0
- package/plugins/qa-changes/action.yml +181 -0
- package/plugins/qa-changes/scripts/agent_script.py +406 -0
- package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
- package/plugins/qa-changes/scripts/prompt.py +174 -0
- package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
- package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
- package/plugins/release-notes/.plugin/plugin.json +19 -0
- package/plugins/release-notes/README.md +283 -0
- package/plugins/release-notes/SKILL.md +83 -0
- package/plugins/release-notes/action.yml +117 -0
- package/plugins/release-notes/commands/release-notes.md +8 -0
- package/plugins/release-notes/scripts/agent_script.py +292 -0
- package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
- package/plugins/release-notes/scripts/prompt.py +90 -0
- package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
- package/plugins/release-notes/workflows/release-notes.yml +76 -0
- package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/README.md +217 -0
- package/plugins/vulnerability-remediation/action.yml +187 -0
- package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
- package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
- package/pyproject.toml +12 -0
- package/release-please-config.json +16 -0
- package/scripts/sync_extensions.py +494 -0
- package/scripts/sync_openhands_sdk_skill.py +264 -0
- package/skills/README.md +159 -0
- package/skills/add-javadoc/.plugin/plugin.json +18 -0
- package/skills/add-javadoc/README.md +40 -0
- package/skills/add-javadoc/SKILL.md +35 -0
- package/skills/add-javadoc/references/example.md +32 -0
- package/skills/add-skill/.plugin/plugin.json +18 -0
- package/skills/add-skill/README.md +67 -0
- package/skills/add-skill/SKILL.md +47 -0
- package/skills/add-skill/scripts/fetch_skill.py +259 -0
- package/skills/agent-creator/.plugin/plugin.json +20 -0
- package/skills/agent-creator/README.md +104 -0
- package/skills/agent-creator/SKILL.md +190 -0
- package/skills/agent-creator/commands/agent-creator.md +8 -0
- package/skills/agent-creator/references/fallback.md +117 -0
- package/skills/agent-memory/.plugin/plugin.json +18 -0
- package/skills/agent-memory/README.md +35 -0
- package/skills/agent-memory/SKILL.md +30 -0
- package/skills/agent-memory/commands/remember.md +8 -0
- package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/README.md +40 -0
- package/skills/agent-sdk-builder/SKILL.md +37 -0
- package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
- package/skills/azure-devops/.plugin/plugin.json +18 -0
- package/skills/azure-devops/README.md +55 -0
- package/skills/azure-devops/SKILL.md +50 -0
- package/skills/bitbucket/.plugin/plugin.json +17 -0
- package/skills/bitbucket/README.md +50 -0
- package/skills/bitbucket/SKILL.md +45 -0
- package/skills/code-review/.plugin/plugin.json +19 -0
- package/skills/code-review/README.md +18 -0
- package/skills/code-review/SKILL.md +208 -0
- package/skills/code-review/commands/codereview-roasted.md +8 -0
- package/skills/code-review/commands/codereview.md +8 -0
- package/skills/code-review/references/risk-evaluation.md +41 -0
- package/skills/code-review/references/supply-chain-security.md +31 -0
- package/skills/code-simplifier/.plugin/plugin.json +21 -0
- package/skills/code-simplifier/README.md +30 -0
- package/skills/code-simplifier/SKILL.md +91 -0
- package/skills/code-simplifier/commands/simplify.md +8 -0
- package/skills/code-simplifier/references/code-quality-review.md +86 -0
- package/skills/code-simplifier/references/code-reuse-review.md +63 -0
- package/skills/code-simplifier/references/efficiency-review.md +81 -0
- package/skills/datadog/.plugin/plugin.json +19 -0
- package/skills/datadog/README.md +100 -0
- package/skills/datadog/SKILL.md +95 -0
- package/skills/deno/.plugin/plugin.json +18 -0
- package/skills/deno/README.md +5 -0
- package/skills/deno/SKILL.md +99 -0
- package/skills/deno/references/README.md +6 -0
- package/skills/discord/.plugin/plugin.json +18 -0
- package/skills/discord/README.md +31 -0
- package/skills/discord/SKILL.md +109 -0
- package/skills/discord/__init__.py +0 -0
- package/skills/discord/references/REFERENCE.md +78 -0
- package/skills/discord/scripts/__init__.py +0 -0
- package/skills/discord/scripts/_http.py +127 -0
- package/skills/discord/scripts/post_webhook.py +106 -0
- package/skills/discord/scripts/send_message.py +102 -0
- package/skills/docker/.plugin/plugin.json +17 -0
- package/skills/docker/README.md +34 -0
- package/skills/docker/SKILL.md +29 -0
- package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
- package/skills/evidence-based-citations/README.md +31 -0
- package/skills/evidence-based-citations/SKILL.md +59 -0
- package/skills/flarglebargle/.plugin/plugin.json +16 -0
- package/skills/flarglebargle/README.md +14 -0
- package/skills/flarglebargle/SKILL.md +9 -0
- package/skills/frontend-design/.plugin/plugin.json +21 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/README.md +42 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/github/.plugin/plugin.json +19 -0
- package/skills/github/README.md +42 -0
- package/skills/github/SKILL.md +106 -0
- package/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/skills/github-pr-review/README.md +145 -0
- package/skills/github-pr-review/SKILL.md +148 -0
- package/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/README.md +34 -0
- package/skills/github-pr-reviewer/SKILL.md +89 -0
- package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
- package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/README.md +70 -0
- package/skills/github-repo-monitor/SKILL.md +316 -0
- package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
- package/skills/github-repo-monitor/references/github-api.md +241 -0
- package/skills/github-repo-monitor/references/state-schema.md +160 -0
- package/skills/github-repo-monitor/scripts/main.py +915 -0
- package/skills/github-repo-monitor/tests/test_main.py +400 -0
- package/skills/gitlab/.plugin/plugin.json +17 -0
- package/skills/gitlab/README.md +37 -0
- package/skills/gitlab/SKILL.md +32 -0
- package/skills/incident-retrospective/.plugin/plugin.json +21 -0
- package/skills/incident-retrospective/README.md +34 -0
- package/skills/incident-retrospective/SKILL.md +98 -0
- package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
- package/skills/iterate/.plugin/plugin.json +13 -0
- package/skills/iterate/README.md +25 -0
- package/skills/iterate/SKILL.md +399 -0
- package/skills/iterate/commands/babysit.md +8 -0
- package/skills/iterate/commands/iterate.md +8 -0
- package/skills/iterate/commands/verify.md +8 -0
- package/skills/iterate/references/heuristics.md +58 -0
- package/skills/iterate/references/verification.md +96 -0
- package/skills/jupyter/.plugin/plugin.json +18 -0
- package/skills/jupyter/README.md +55 -0
- package/skills/jupyter/SKILL.md +50 -0
- package/skills/kubernetes/.plugin/plugin.json +18 -0
- package/skills/kubernetes/README.md +53 -0
- package/skills/kubernetes/SKILL.md +48 -0
- package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
- package/skills/learn-from-code-review/README.md +64 -0
- package/skills/learn-from-code-review/SKILL.md +186 -0
- package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
- package/skills/linear/.plugin/plugin.json +19 -0
- package/skills/linear/README.md +58 -0
- package/skills/linear/SKILL.md +213 -0
- package/skills/linear-triage/.plugin/plugin.json +21 -0
- package/skills/linear-triage/README.md +34 -0
- package/skills/linear-triage/SKILL.md +91 -0
- package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
- package/skills/notion/.plugin/plugin.json +17 -0
- package/skills/notion/README.md +114 -0
- package/skills/notion/SKILL.md +109 -0
- package/skills/npm/.plugin/plugin.json +17 -0
- package/skills/npm/README.md +14 -0
- package/skills/npm/SKILL.md +9 -0
- package/skills/openhands-api/.plugin/plugin.json +22 -0
- package/skills/openhands-api/README.md +48 -0
- package/skills/openhands-api/SKILL.md +399 -0
- package/skills/openhands-api/references/README.md +33 -0
- package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
- package/skills/openhands-api/references/example_prompt.md +12 -0
- package/skills/openhands-api/scripts/openhands_api.py +606 -0
- package/skills/openhands-api/scripts/openhands_api.ts +252 -0
- package/skills/openhands-automation/.plugin/plugin.json +19 -0
- package/skills/openhands-automation/README.md +89 -0
- package/skills/openhands-automation/SKILL.md +875 -0
- package/skills/openhands-automation/commands/automation:create.md +8 -0
- package/skills/openhands-automation/references/ab-testing.md +185 -0
- package/skills/openhands-automation/references/custom-automation.md +644 -0
- package/skills/openhands-sdk/.plugin/plugin.json +20 -0
- package/skills/openhands-sdk/README.md +22 -0
- package/skills/openhands-sdk/SKILL.md +229 -0
- package/skills/openhands-sdk/commands/sdk.md +8 -0
- package/skills/pdflatex/.plugin/plugin.json +18 -0
- package/skills/pdflatex/README.md +39 -0
- package/skills/pdflatex/SKILL.md +34 -0
- package/skills/prd/.plugin/plugin.json +19 -0
- package/skills/prd/README.md +28 -0
- package/skills/prd/SKILL.md +237 -0
- package/skills/prd/commands/prd.md +8 -0
- package/skills/qa-changes/README.md +18 -0
- package/skills/qa-changes/SKILL.md +229 -0
- package/skills/qa-changes/commands/qa-changes.md +8 -0
- package/skills/release-notes/README.md +24 -0
- package/skills/release-notes/SKILL.md +19 -0
- package/skills/release-notes/commands/release-notes.md +8 -0
- package/skills/research-brief/.plugin/plugin.json +20 -0
- package/skills/research-brief/README.md +34 -0
- package/skills/research-brief/SKILL.md +99 -0
- package/skills/research-brief/commands/research-brief:setup.md +8 -0
- package/skills/security/.plugin/plugin.json +18 -0
- package/skills/security/README.md +38 -0
- package/skills/security/SKILL.md +33 -0
- package/skills/skill-creator/.plugin/plugin.json +17 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +182 -0
- package/skills/skill-creator/SKILL.md +545 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
- package/skills/slack-channel-monitor/README.md +91 -0
- package/skills/slack-channel-monitor/SKILL.md +276 -0
- package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
- package/skills/slack-channel-monitor/references/slack-api.md +207 -0
- package/skills/slack-channel-monitor/references/state-schema.md +180 -0
- package/skills/slack-channel-monitor/scripts/main.py +962 -0
- package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/README.md +34 -0
- package/skills/slack-standup-digest/SKILL.md +92 -0
- package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
- package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
- package/skills/spark-version-upgrade/README.md +54 -0
- package/skills/spark-version-upgrade/SKILL.md +233 -0
- package/skills/ssh/.plugin/plugin.json +18 -0
- package/skills/ssh/README.md +140 -0
- package/skills/ssh/SKILL.md +135 -0
- package/skills/swift-linux/.plugin/plugin.json +17 -0
- package/skills/swift-linux/README.md +86 -0
- package/skills/swift-linux/SKILL.md +81 -0
- package/skills/theme-factory/.plugin/plugin.json +19 -0
- package/skills/theme-factory/LICENSE.txt +202 -0
- package/skills/theme-factory/README.md +58 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/uv/.plugin/plugin.json +18 -0
- package/skills/uv/README.md +5 -0
- package/skills/uv/SKILL.md +95 -0
- package/skills/uv/references/README.md +5 -0
- package/skills/vercel/.plugin/plugin.json +18 -0
- package/skills/vercel/README.md +108 -0
- package/skills/vercel/SKILL.md +103 -0
- package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
- package/tests/test_catalogs.py +109 -0
- package/tests/test_code_review_risk_evaluation.py +94 -0
- package/tests/test_issue_duplicate_checker.py +240 -0
- package/tests/test_openhands_api_python.py +152 -0
- package/tests/test_plugin_manifest.py +83 -0
- package/tests/test_pr_review_diff_payload.py +202 -0
- package/tests/test_pr_review_feedback.py +263 -0
- package/tests/test_pr_review_prompt.py +152 -0
- package/tests/test_pr_review_review_context.py +253 -0
- package/tests/test_qa_changes.py +232 -0
- package/tests/test_qa_changes_evaluation.py +259 -0
- package/tests/test_release_notes_generator.py +990 -0
- package/tests/test_sdk_loading.py +150 -0
- package/tests/test_skill_plugin_loading.py +149 -0
- package/tests/test_skills_have_readme.py +66 -0
- package/tests/test_sync_extensions.py +292 -0
- package/tests/test_workflow_sync.py +46 -0
- package/utils/analysis/README.md +7 -0
- package/utils/analysis/laminar_signals/README.md +211 -0
- package/utils/analysis/laminar_signals/analyze.py +780 -0
- package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
- package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""OpenHands Cloud API (V1) minimal client.
|
|
2
|
+
|
|
3
|
+
This file is intentionally:
|
|
4
|
+
- small (easy to copy into other repos)
|
|
5
|
+
- dependency-light (only `httpx`)
|
|
6
|
+
- opinionated in a helpful way (defaults to OpenHands Cloud)
|
|
7
|
+
|
|
8
|
+
Audience: AI agents.
|
|
9
|
+
|
|
10
|
+
The V1 API is hosted on the OpenHands app server under:
|
|
11
|
+
{BASE_URL}/api/v1/...
|
|
12
|
+
|
|
13
|
+
Typical workflow for common operations:
|
|
14
|
+
1) Discover: GET /api/v1/users/me
|
|
15
|
+
2) List/search conversations: GET /api/v1/app-conversations/search
|
|
16
|
+
3) Start a conversation (creates sandbox): POST /api/v1/app-conversations
|
|
17
|
+
4) Monitor events for a conversation: GET /api/v1/conversation/{id}/events/search
|
|
18
|
+
5) (Optional) download trajectory: GET /api/v1/app-conversations/{id}/download
|
|
19
|
+
|
|
20
|
+
Note: Some operations happen against the *agent server* running inside a sandbox
|
|
21
|
+
(not the app server). Those endpoints use X-Session-API-Key instead of Bearer auth.
|
|
22
|
+
|
|
23
|
+
This client purposefully keeps responses as raw dicts/lists so agents can quickly
|
|
24
|
+
adapt it without strict schema maintenance.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
import zipfile
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DEFAULT_BASE_URL = "https://app.all-hands.dev"
|
|
42
|
+
PREFERRED_API_KEY_ENV_VARS = ("OPENHANDS_CLOUD_API_KEY", "OPENHANDS_API_KEY")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Start-task statuses observed in the wild. These may evolve, so keep this centralized.
|
|
46
|
+
START_TASK_TERMINAL_STATUSES = frozenset(
|
|
47
|
+
{"READY", "ERROR", "FAILED", "CANCELLED", "DONE", "COMPLETED"}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Safety cap for paging calls. Keeps responses small and consistent across clients.
|
|
52
|
+
AGENT_EVENTS_SEARCH_MAX_LIMIT = 100
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class OpenHandsAPIConfig:
|
|
57
|
+
api_key: str
|
|
58
|
+
base_url: str = DEFAULT_BASE_URL
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def api_v1_url(self) -> str:
|
|
62
|
+
return f"{self.base_url.rstrip('/')}/api/v1"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OpenHandsAPI:
|
|
66
|
+
"""Minimal OpenHands Cloud API client for the supported V1 API."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, api_key: str | None = None, base_url: str = DEFAULT_BASE_URL):
|
|
69
|
+
resolved_key = api_key
|
|
70
|
+
if not resolved_key:
|
|
71
|
+
for env_name in PREFERRED_API_KEY_ENV_VARS:
|
|
72
|
+
resolved_key = os.getenv(env_name)
|
|
73
|
+
if resolved_key:
|
|
74
|
+
break
|
|
75
|
+
if not resolved_key:
|
|
76
|
+
env_list = ", ".join(PREFERRED_API_KEY_ENV_VARS)
|
|
77
|
+
raise ValueError(f"Missing API key. Set one of: {env_list}, or pass api_key=...")
|
|
78
|
+
|
|
79
|
+
self._cfg = OpenHandsAPIConfig(api_key=resolved_key, base_url=base_url.rstrip("/"))
|
|
80
|
+
self._client = httpx.Client(
|
|
81
|
+
headers={
|
|
82
|
+
"Authorization": f"Bearer {self._cfg.api_key}",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
timeout=30,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def base_url(self) -> str:
|
|
90
|
+
return self._cfg.base_url
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def api_v1_url(self) -> str:
|
|
94
|
+
return self._cfg.api_v1_url
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
self._client.close()
|
|
98
|
+
|
|
99
|
+
# -----------------------------
|
|
100
|
+
# App server endpoints (Bearer auth)
|
|
101
|
+
# -----------------------------
|
|
102
|
+
|
|
103
|
+
def users_me(self) -> dict[str, Any]:
|
|
104
|
+
r = self._client.get(f"{self.api_v1_url}/users/me")
|
|
105
|
+
r.raise_for_status()
|
|
106
|
+
return r.json()
|
|
107
|
+
|
|
108
|
+
def app_conversations_search(self, *, limit: int = 20) -> dict[str, Any]:
|
|
109
|
+
limit = max(1, int(limit))
|
|
110
|
+
r = self._client.get(
|
|
111
|
+
f"{self.api_v1_url}/app-conversations/search", params={"limit": limit}
|
|
112
|
+
)
|
|
113
|
+
r.raise_for_status()
|
|
114
|
+
return r.json()
|
|
115
|
+
|
|
116
|
+
def app_conversations_count(self) -> dict[str, Any]:
|
|
117
|
+
r = self._client.get(f"{self.api_v1_url}/app-conversations/count")
|
|
118
|
+
r.raise_for_status()
|
|
119
|
+
return r.json()
|
|
120
|
+
|
|
121
|
+
def app_conversations_get_batch(self, *, ids: list[str]) -> list[dict[str, Any]]:
|
|
122
|
+
if not ids:
|
|
123
|
+
return []
|
|
124
|
+
r = self._client.get(f"{self.api_v1_url}/app-conversations", params={"ids": ids})
|
|
125
|
+
r.raise_for_status()
|
|
126
|
+
return r.json()
|
|
127
|
+
|
|
128
|
+
def app_conversation_get(self, conversation_id: str) -> dict[str, Any] | None:
|
|
129
|
+
items = self.app_conversations_get_batch(ids=[conversation_id])
|
|
130
|
+
return items[0] if items else None
|
|
131
|
+
|
|
132
|
+
def sandboxes_search(self, *, limit: int = 20) -> dict[str, Any]:
|
|
133
|
+
limit = max(1, int(limit))
|
|
134
|
+
r = self._client.get(f"{self.api_v1_url}/sandboxes/search", params={"limit": limit})
|
|
135
|
+
r.raise_for_status()
|
|
136
|
+
return r.json()
|
|
137
|
+
|
|
138
|
+
def sandbox_specs_search(self, *, limit: int = 20) -> dict[str, Any]:
|
|
139
|
+
limit = max(1, int(limit))
|
|
140
|
+
r = self._client.get(
|
|
141
|
+
f"{self.api_v1_url}/sandbox-specs/search", params={"limit": limit}
|
|
142
|
+
)
|
|
143
|
+
r.raise_for_status()
|
|
144
|
+
return r.json()
|
|
145
|
+
|
|
146
|
+
def conversation_events_search(
|
|
147
|
+
self, conversation_id: str, *, limit: int = 50
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
limit = max(1, int(limit))
|
|
150
|
+
r = self._client.get(
|
|
151
|
+
f"{self.api_v1_url}/conversation/{conversation_id}/events/search",
|
|
152
|
+
params={"limit": limit},
|
|
153
|
+
)
|
|
154
|
+
r.raise_for_status()
|
|
155
|
+
return r.json()
|
|
156
|
+
|
|
157
|
+
def conversation_events_count(self, conversation_id: str) -> dict[str, Any]:
|
|
158
|
+
r = self._client.get(f"{self.api_v1_url}/conversation/{conversation_id}/events/count")
|
|
159
|
+
r.raise_for_status()
|
|
160
|
+
return r.json()
|
|
161
|
+
|
|
162
|
+
def app_conversation_start(
|
|
163
|
+
self,
|
|
164
|
+
*,
|
|
165
|
+
initial_message: str,
|
|
166
|
+
selected_repository: str | None = None,
|
|
167
|
+
selected_branch: str | None = None,
|
|
168
|
+
title: str | None = None,
|
|
169
|
+
run: bool = True,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Start a new V1 app conversation.
|
|
172
|
+
|
|
173
|
+
WARNING: This typically creates a sandbox and may incur costs.
|
|
174
|
+
|
|
175
|
+
In many deployments this endpoint is **asynchronous** and returns a **start-task** dict.
|
|
176
|
+
Common fields:
|
|
177
|
+
- `id`: the *start_task_id*
|
|
178
|
+
- `app_conversation_id`: the id to use for `/download` and `/conversation/.../events/...`
|
|
179
|
+
|
|
180
|
+
If `app_conversation_id` is missing from the initial response, fetch it via:
|
|
181
|
+
- `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`
|
|
182
|
+
(see `app_conversation_start_task_get()` / `poll_start_task_until_ready()`).
|
|
183
|
+
|
|
184
|
+
The payload structure here mirrors what the V1 app server expects:
|
|
185
|
+
- initial_message.content is a list of content parts
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
payload: dict[str, Any] = {
|
|
189
|
+
"initial_message": {
|
|
190
|
+
"role": "user",
|
|
191
|
+
# V1 expects `content` as a list of parts, even for a single text message.
|
|
192
|
+
"content": [{"type": "text", "text": initial_message}],
|
|
193
|
+
"run": bool(run),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if selected_repository:
|
|
197
|
+
payload["selected_repository"] = selected_repository
|
|
198
|
+
if selected_branch:
|
|
199
|
+
payload["selected_branch"] = selected_branch
|
|
200
|
+
if title:
|
|
201
|
+
payload["title"] = title
|
|
202
|
+
|
|
203
|
+
r = self._client.post(f"{self.api_v1_url}/app-conversations", json=payload, timeout=120)
|
|
204
|
+
r.raise_for_status()
|
|
205
|
+
return r.json()
|
|
206
|
+
|
|
207
|
+
def app_conversations_start_tasks_get_batch(self, *, ids: list[str]) -> list[dict[str, Any]]:
|
|
208
|
+
if not ids:
|
|
209
|
+
return []
|
|
210
|
+
r = self._client.get(
|
|
211
|
+
f"{self.api_v1_url}/app-conversations/start-tasks", params={"ids": ids}
|
|
212
|
+
)
|
|
213
|
+
r.raise_for_status()
|
|
214
|
+
return r.json()
|
|
215
|
+
|
|
216
|
+
def app_conversation_start_task_get(self, task_id: str) -> dict[str, Any] | None:
|
|
217
|
+
items = self.app_conversations_start_tasks_get_batch(ids=[task_id])
|
|
218
|
+
return items[0] if items else None
|
|
219
|
+
|
|
220
|
+
def sandboxes_pause(self, sandbox_id: str) -> dict[str, Any]:
|
|
221
|
+
r = self._client.post(f"{self.api_v1_url}/sandboxes/{sandbox_id}/pause", timeout=60)
|
|
222
|
+
r.raise_for_status()
|
|
223
|
+
return r.json()
|
|
224
|
+
|
|
225
|
+
def sandboxes_resume(self, sandbox_id: str) -> dict[str, Any]:
|
|
226
|
+
r = self._client.post(f"{self.api_v1_url}/sandboxes/{sandbox_id}/resume", timeout=60)
|
|
227
|
+
r.raise_for_status()
|
|
228
|
+
return r.json()
|
|
229
|
+
|
|
230
|
+
def app_conversation_download_zip(
|
|
231
|
+
self, app_conversation_id: str, *, output_file: str | Path
|
|
232
|
+
) -> dict[str, Any]:
|
|
233
|
+
"""Download a conversation trajectory zip to disk.
|
|
234
|
+
|
|
235
|
+
Note: this endpoint expects the **app_conversation_id** (not the start-task id).
|
|
236
|
+
"""
|
|
237
|
+
url = f"{self.api_v1_url}/app-conversations/{app_conversation_id}/download"
|
|
238
|
+
r = self._client.get(url, timeout=60)
|
|
239
|
+
r.raise_for_status()
|
|
240
|
+
out = Path(output_file)
|
|
241
|
+
out.write_bytes(r.content)
|
|
242
|
+
return {
|
|
243
|
+
"file": str(out),
|
|
244
|
+
"size": len(r.content),
|
|
245
|
+
"content_type": r.headers.get("content-type"),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def count_events_via_trajectory_zip(
|
|
249
|
+
self,
|
|
250
|
+
app_conversation_id: str,
|
|
251
|
+
*,
|
|
252
|
+
zip_file: str | Path,
|
|
253
|
+
extract_dir: str | Path,
|
|
254
|
+
) -> dict[str, Any]:
|
|
255
|
+
"""Fallback event counting: download trajectory zip, extract, count event files.
|
|
256
|
+
|
|
257
|
+
This is heavier than calling a count endpoint, but it is still a single API call and
|
|
258
|
+
also gives you the full exported event payloads.
|
|
259
|
+
|
|
260
|
+
Cleanup (optional): this helper writes a zip file and extracts JSON events. If you
|
|
261
|
+
want to clean up afterwards, you can remove them, e.g.:
|
|
262
|
+
|
|
263
|
+
- `zip_path.unlink(missing_ok=True)`
|
|
264
|
+
- `shutil.rmtree(extract_path, ignore_errors=True)`
|
|
265
|
+
|
|
266
|
+
Returns a small summary dict including `event_count`.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
zip_path = Path(zip_file)
|
|
270
|
+
extract_path = Path(extract_dir)
|
|
271
|
+
|
|
272
|
+
download_meta = self.app_conversation_download_zip(app_conversation_id, output_file=zip_path)
|
|
273
|
+
extract_path.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
|
|
275
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
276
|
+
zf.extractall(extract_path)
|
|
277
|
+
|
|
278
|
+
event_count = len(list(extract_path.glob("event_*.json")))
|
|
279
|
+
has_meta = (extract_path / "meta.json").exists()
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"event_count": event_count,
|
|
283
|
+
"has_meta": has_meta,
|
|
284
|
+
"zip": download_meta,
|
|
285
|
+
"extract_dir": str(extract_path),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# -----------------------------
|
|
289
|
+
# Agent server endpoints (X-Session-API-Key)
|
|
290
|
+
# -----------------------------
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def agent_headers(session_api_key: str) -> dict[str, str]:
|
|
294
|
+
return {"X-Session-API-Key": session_api_key, "Content-Type": "application/json"}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _agent_event_filter_params(
|
|
299
|
+
*,
|
|
300
|
+
timestamp_gte: str | None = None,
|
|
301
|
+
timestamp_lt: str | None = None,
|
|
302
|
+
kind: str | None = None,
|
|
303
|
+
source: str | None = None,
|
|
304
|
+
body: str | None = None,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
params: dict[str, Any] = {}
|
|
307
|
+
if timestamp_gte is not None:
|
|
308
|
+
params["timestamp__gte"] = timestamp_gte
|
|
309
|
+
if timestamp_lt is not None:
|
|
310
|
+
params["timestamp__lt"] = timestamp_lt
|
|
311
|
+
if kind is not None:
|
|
312
|
+
params["kind"] = kind
|
|
313
|
+
if source is not None:
|
|
314
|
+
params["source"] = source
|
|
315
|
+
if body is not None:
|
|
316
|
+
params["body"] = body
|
|
317
|
+
return params
|
|
318
|
+
|
|
319
|
+
def agent_events_search(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
agent_server_url: str,
|
|
323
|
+
session_api_key: str,
|
|
324
|
+
conversation_id: str,
|
|
325
|
+
limit: int = 50,
|
|
326
|
+
sort_order: str | None = None,
|
|
327
|
+
timestamp_gte: str | None = None,
|
|
328
|
+
timestamp_lt: str | None = None,
|
|
329
|
+
kind: str | None = None,
|
|
330
|
+
source: str | None = None,
|
|
331
|
+
body: str | None = None,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""Search events via the sandbox agent-server.
|
|
334
|
+
|
|
335
|
+
Notes:
|
|
336
|
+
- `limit` is capped at AGENT_EVENTS_SEARCH_MAX_LIMIT to avoid huge responses.
|
|
337
|
+
- `sort_order` must be one of: "TIMESTAMP", "TIMESTAMP_DESC".
|
|
338
|
+
- timestamp filters are passed as ISO-8601 strings (e.g. "2026-02-14T21:54:00Z").
|
|
339
|
+
The server accepts both timezone-aware and naive datetimes.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
url = f"{agent_server_url.rstrip('/')}/api/conversations/{conversation_id}/events/search"
|
|
343
|
+
capped_limit = min(AGENT_EVENTS_SEARCH_MAX_LIMIT, max(1, int(limit)))
|
|
344
|
+
params: dict[str, Any] = {"limit": capped_limit}
|
|
345
|
+
if sort_order is not None:
|
|
346
|
+
params["sort_order"] = sort_order
|
|
347
|
+
params.update(
|
|
348
|
+
self._agent_event_filter_params(
|
|
349
|
+
timestamp_gte=timestamp_gte,
|
|
350
|
+
timestamp_lt=timestamp_lt,
|
|
351
|
+
kind=kind,
|
|
352
|
+
source=source,
|
|
353
|
+
body=body,
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
r = httpx.get(
|
|
358
|
+
url,
|
|
359
|
+
headers=self.agent_headers(session_api_key),
|
|
360
|
+
params=params,
|
|
361
|
+
timeout=30,
|
|
362
|
+
)
|
|
363
|
+
r.raise_for_status()
|
|
364
|
+
return r.json()
|
|
365
|
+
|
|
366
|
+
def agent_events_count(
|
|
367
|
+
self,
|
|
368
|
+
*,
|
|
369
|
+
agent_server_url: str,
|
|
370
|
+
session_api_key: str,
|
|
371
|
+
conversation_id: str,
|
|
372
|
+
timestamp_gte: str | None = None,
|
|
373
|
+
timestamp_lt: str | None = None,
|
|
374
|
+
kind: str | None = None,
|
|
375
|
+
source: str | None = None,
|
|
376
|
+
body: str | None = None,
|
|
377
|
+
) -> int:
|
|
378
|
+
"""Count events via the sandbox agent-server.
|
|
379
|
+
|
|
380
|
+
Timestamp filters are passed as ISO-8601 strings (e.g. "2026-02-14T21:54:00Z").
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
url = f"{agent_server_url.rstrip('/')}/api/conversations/{conversation_id}/events/count"
|
|
384
|
+
params = self._agent_event_filter_params(
|
|
385
|
+
timestamp_gte=timestamp_gte,
|
|
386
|
+
timestamp_lt=timestamp_lt,
|
|
387
|
+
kind=kind,
|
|
388
|
+
source=source,
|
|
389
|
+
body=body,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
r = httpx.get(
|
|
393
|
+
url,
|
|
394
|
+
headers=self.agent_headers(session_api_key),
|
|
395
|
+
params=params,
|
|
396
|
+
timeout=30,
|
|
397
|
+
)
|
|
398
|
+
r.raise_for_status()
|
|
399
|
+
return int(r.json())
|
|
400
|
+
|
|
401
|
+
def agent_execute_bash(
|
|
402
|
+
self,
|
|
403
|
+
*,
|
|
404
|
+
agent_server_url: str,
|
|
405
|
+
session_api_key: str,
|
|
406
|
+
command: str,
|
|
407
|
+
cwd: str | None = None,
|
|
408
|
+
timeout_s: int = 30,
|
|
409
|
+
) -> dict[str, Any]:
|
|
410
|
+
url = f"{agent_server_url.rstrip('/')}/api/bash/execute_bash_command"
|
|
411
|
+
payload: dict[str, Any] = {"command": command, "timeout": int(timeout_s)}
|
|
412
|
+
if cwd:
|
|
413
|
+
payload["cwd"] = cwd
|
|
414
|
+
r = httpx.post(url, headers=self.agent_headers(session_api_key), json=payload, timeout=60)
|
|
415
|
+
r.raise_for_status()
|
|
416
|
+
return r.json()
|
|
417
|
+
|
|
418
|
+
def agent_download_file(
|
|
419
|
+
self,
|
|
420
|
+
*,
|
|
421
|
+
agent_server_url: str,
|
|
422
|
+
session_api_key: str,
|
|
423
|
+
path: str,
|
|
424
|
+
output_file: str | Path,
|
|
425
|
+
) -> dict[str, Any]:
|
|
426
|
+
p = path if path.startswith("/") else f"/{path}"
|
|
427
|
+
url = f"{agent_server_url.rstrip('/')}/api/file/download{p}"
|
|
428
|
+
r = httpx.get(url, headers=self.agent_headers(session_api_key), timeout=30)
|
|
429
|
+
r.raise_for_status()
|
|
430
|
+
out = Path(output_file)
|
|
431
|
+
out.write_bytes(r.content)
|
|
432
|
+
return {"file": str(out), "size": len(r.content)}
|
|
433
|
+
|
|
434
|
+
def agent_upload_text_file(
|
|
435
|
+
self,
|
|
436
|
+
*,
|
|
437
|
+
agent_server_url: str,
|
|
438
|
+
session_api_key: str,
|
|
439
|
+
path: str,
|
|
440
|
+
content: str,
|
|
441
|
+
content_type: str = "text/plain",
|
|
442
|
+
) -> dict[str, Any]:
|
|
443
|
+
p = path if path.startswith("/") else f"/{path}"
|
|
444
|
+
url = f"{agent_server_url.rstrip('/')}/api/file/upload{p}"
|
|
445
|
+
filename = os.path.basename(p)
|
|
446
|
+
headers = {"X-Session-API-Key": session_api_key}
|
|
447
|
+
files = {"file": (filename, content.encode("utf-8"), content_type)}
|
|
448
|
+
r = httpx.post(url, headers=headers, files=files, timeout=30)
|
|
449
|
+
r.raise_for_status()
|
|
450
|
+
return r.json() if r.text else {"success": True}
|
|
451
|
+
|
|
452
|
+
# -----------------------------
|
|
453
|
+
# Convenience helpers
|
|
454
|
+
# -----------------------------
|
|
455
|
+
|
|
456
|
+
def app_conversation_start_from_prompt_files(
|
|
457
|
+
self,
|
|
458
|
+
prompt_file: str | Path,
|
|
459
|
+
*,
|
|
460
|
+
selected_repository: str | None = None,
|
|
461
|
+
selected_branch: str | None = None,
|
|
462
|
+
title: str | None = None,
|
|
463
|
+
append_file: str | Path | None = None,
|
|
464
|
+
run: bool = True,
|
|
465
|
+
) -> dict[str, Any]:
|
|
466
|
+
main_text = Path(prompt_file).read_text(encoding="utf-8")
|
|
467
|
+
if append_file and Path(append_file).exists():
|
|
468
|
+
tail = Path(append_file).read_text(encoding="utf-8")
|
|
469
|
+
initial = f"{main_text}\n\n{tail}"
|
|
470
|
+
else:
|
|
471
|
+
initial = main_text
|
|
472
|
+
|
|
473
|
+
return self.app_conversation_start(
|
|
474
|
+
initial_message=initial,
|
|
475
|
+
selected_repository=selected_repository,
|
|
476
|
+
selected_branch=selected_branch,
|
|
477
|
+
title=title,
|
|
478
|
+
run=run,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def _start_task_status(task: dict[str, Any] | None) -> str:
|
|
484
|
+
return str((task or {}).get("status") or "").upper()
|
|
485
|
+
|
|
486
|
+
def poll_start_task_until_ready(
|
|
487
|
+
self,
|
|
488
|
+
task_id: str,
|
|
489
|
+
*,
|
|
490
|
+
timeout_s: int = 10 * 60,
|
|
491
|
+
poll_interval_s: float = 2.0,
|
|
492
|
+
backoff_factor: float = 1.5,
|
|
493
|
+
max_interval_s: float = 10.0,
|
|
494
|
+
max_polls: int | None = None,
|
|
495
|
+
) -> dict[str, Any]:
|
|
496
|
+
"""Poll a start-task until it reaches a terminal state.
|
|
497
|
+
|
|
498
|
+
This is the async companion to `POST /api/v1/app-conversations`.
|
|
499
|
+
|
|
500
|
+
It is intentionally *polite*:
|
|
501
|
+
- sleeps between requests
|
|
502
|
+
- uses exponential backoff (capped by `max_interval_s`)
|
|
503
|
+
- supports `max_polls` to cap the total number of API calls
|
|
504
|
+
|
|
505
|
+
Terminal statuses are defined in START_TASK_TERMINAL_STATUSES.
|
|
506
|
+
|
|
507
|
+
Raises:
|
|
508
|
+
TimeoutError: if the task doesn't reach a terminal state in time.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
deadline = time.monotonic() + float(timeout_s)
|
|
512
|
+
interval = max(0.25, float(poll_interval_s))
|
|
513
|
+
factor = max(1.0, float(backoff_factor))
|
|
514
|
+
max_interval = max(interval, float(max_interval_s))
|
|
515
|
+
|
|
516
|
+
polls = 0
|
|
517
|
+
last: dict[str, Any] | None = None
|
|
518
|
+
|
|
519
|
+
while True:
|
|
520
|
+
if max_polls is not None and polls >= int(max_polls):
|
|
521
|
+
raise TimeoutError(
|
|
522
|
+
f"Start task {task_id} did not reach terminal state (max_polls={max_polls}, last={last})"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
remaining = deadline - time.monotonic()
|
|
526
|
+
if remaining <= 0:
|
|
527
|
+
raise TimeoutError(
|
|
528
|
+
f"Start task {task_id} did not reach terminal state in {timeout_s}s (last={last})"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
last = self.app_conversation_start_task_get(task_id)
|
|
532
|
+
polls += 1
|
|
533
|
+
|
|
534
|
+
status = self._start_task_status(last)
|
|
535
|
+
if status in START_TASK_TERMINAL_STATUSES:
|
|
536
|
+
return last or {}
|
|
537
|
+
|
|
538
|
+
sleep_s = min(interval, remaining)
|
|
539
|
+
if sleep_s > 0:
|
|
540
|
+
time.sleep(sleep_s)
|
|
541
|
+
interval = min(max_interval, interval * factor)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
OpenHandsV1API = OpenHandsAPI
|
|
545
|
+
|
|
546
|
+
def _cmd_search_conversations(args: argparse.Namespace) -> int:
|
|
547
|
+
api = OpenHandsAPI(api_key=args.api_key, base_url=args.base_url)
|
|
548
|
+
try:
|
|
549
|
+
print(json.dumps(api.app_conversations_search(limit=args.limit), indent=2))
|
|
550
|
+
return 0
|
|
551
|
+
finally:
|
|
552
|
+
api.close()
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _cmd_start_conversation(args: argparse.Namespace) -> int:
|
|
556
|
+
api = OpenHandsAPI(api_key=args.api_key, base_url=args.base_url)
|
|
557
|
+
try:
|
|
558
|
+
resp = api.app_conversation_start_from_prompt_files(
|
|
559
|
+
args.prompt_file,
|
|
560
|
+
selected_repository=args.repo,
|
|
561
|
+
selected_branch=args.branch,
|
|
562
|
+
title=args.title,
|
|
563
|
+
append_file=args.append_file,
|
|
564
|
+
run=not args.no_run,
|
|
565
|
+
)
|
|
566
|
+
print(json.dumps(resp, indent=2))
|
|
567
|
+
return 0
|
|
568
|
+
finally:
|
|
569
|
+
api.close()
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def main(argv: list[str] | None = None) -> int:
|
|
573
|
+
parser = argparse.ArgumentParser(prog="openhands_api.py")
|
|
574
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
575
|
+
|
|
576
|
+
p_search = sub.add_parser("search-conversations", help="GET /api/v1/app-conversations/search")
|
|
577
|
+
p_search.add_argument(
|
|
578
|
+
"--api-key",
|
|
579
|
+
default=None,
|
|
580
|
+
help="Defaults to OPENHANDS_CLOUD_API_KEY, then OPENHANDS_API_KEY",
|
|
581
|
+
)
|
|
582
|
+
p_search.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
|
583
|
+
p_search.add_argument("--limit", type=int, default=5)
|
|
584
|
+
p_search.set_defaults(func=_cmd_search_conversations)
|
|
585
|
+
|
|
586
|
+
p_start = sub.add_parser("start-conversation", help="POST /api/v1/app-conversations from a prompt file")
|
|
587
|
+
p_start.add_argument(
|
|
588
|
+
"--api-key",
|
|
589
|
+
default=None,
|
|
590
|
+
help="Defaults to OPENHANDS_CLOUD_API_KEY, then OPENHANDS_API_KEY",
|
|
591
|
+
)
|
|
592
|
+
p_start.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
|
593
|
+
p_start.add_argument("--prompt-file", required=True)
|
|
594
|
+
p_start.add_argument("--append-file", default=None)
|
|
595
|
+
p_start.add_argument("--repo", default=None)
|
|
596
|
+
p_start.add_argument("--branch", default=None)
|
|
597
|
+
p_start.add_argument("--title", default=None)
|
|
598
|
+
p_start.add_argument("--no-run", action="store_true", help="If set, do not auto-run after sending initial message")
|
|
599
|
+
p_start.set_defaults(func=_cmd_start_conversation)
|
|
600
|
+
|
|
601
|
+
args = parser.parse_args(argv)
|
|
602
|
+
return int(args.func(args))
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
if __name__ == "__main__":
|
|
606
|
+
raise SystemExit(main())
|