@openhands/extensions 0.0.1-alpha → 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,681 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
OPENHANDS_BASE_URL = os.environ.get("OPENHANDS_BASE_URL", "https://app.all-hands.dev")
|
|
18
|
+
REPOSITORY_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$")
|
|
19
|
+
GITHUB_API_BASE_URL = os.environ.get("GITHUB_API_BASE_URL", "https://api.github.com")
|
|
20
|
+
GITHUB_BASE_URL = os.environ.get("GITHUB_BASE_URL", "https://github.com")
|
|
21
|
+
FAILED_EXECUTION_STATUSES = {
|
|
22
|
+
"error",
|
|
23
|
+
"errored",
|
|
24
|
+
"failed",
|
|
25
|
+
"stopped",
|
|
26
|
+
}
|
|
27
|
+
SUCCESSFUL_TERMINAL_EXECUTION_STATUSES = {
|
|
28
|
+
"completed",
|
|
29
|
+
"finished",
|
|
30
|
+
}
|
|
31
|
+
TERMINAL_EXECUTION_STATUSES = (
|
|
32
|
+
FAILED_EXECUTION_STATUSES | SUCCESSFUL_TERMINAL_EXECUTION_STATUSES
|
|
33
|
+
)
|
|
34
|
+
EVENT_SEARCH_LIMIT = 1000
|
|
35
|
+
EVENT_SEARCH_LIMIT_HIT_MESSAGE = (
|
|
36
|
+
f"Event search returned at least {EVENT_SEARCH_LIMIT} events; results may be "
|
|
37
|
+
"incomplete"
|
|
38
|
+
)
|
|
39
|
+
OPENHANDS_DEBUG_KEYS = (
|
|
40
|
+
"id",
|
|
41
|
+
"status",
|
|
42
|
+
"app_conversation_id",
|
|
43
|
+
"execution_status",
|
|
44
|
+
"conversation_url",
|
|
45
|
+
"error",
|
|
46
|
+
"error_detail",
|
|
47
|
+
"detail",
|
|
48
|
+
"message",
|
|
49
|
+
)
|
|
50
|
+
OPENHANDS_SENSITIVE_KEYS = frozenset({"session_api_key"})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class HTTPError(RuntimeError):
|
|
54
|
+
def __init__(self, method: str, url: str, status_code: int, body: str) -> None:
|
|
55
|
+
self.method = method
|
|
56
|
+
self.url = url
|
|
57
|
+
self.status_code = status_code
|
|
58
|
+
self.body = body
|
|
59
|
+
super().__init__(f"{method} {url} failed with HTTP {status_code}: {body}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_args() -> argparse.Namespace:
|
|
63
|
+
parser = argparse.ArgumentParser(
|
|
64
|
+
description=(
|
|
65
|
+
"Start an OpenHands Cloud conversation that checks a GitHub issue "
|
|
66
|
+
"for duplicates."
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--repository", required=True, help="Repository in owner/repo form"
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--issue-number", required=True, type=int, help="Issue number to inspect"
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--output",
|
|
77
|
+
default="duplicate-check-result.json",
|
|
78
|
+
help="Path where the JSON result should be written",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--poll-interval-seconds",
|
|
82
|
+
default=5,
|
|
83
|
+
type=int,
|
|
84
|
+
help="Polling interval while waiting for the conversation to finish",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--max-wait-seconds",
|
|
88
|
+
default=900,
|
|
89
|
+
type=int,
|
|
90
|
+
help=(
|
|
91
|
+
"Maximum time to wait per polling phase; if a start task must be awaited "
|
|
92
|
+
"first, the total runtime can approach twice this value"
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
return parser.parse_args()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def github_headers() -> dict[str, str]:
|
|
99
|
+
github_token = os.environ.get("GITHUB_TOKEN")
|
|
100
|
+
if not github_token:
|
|
101
|
+
raise RuntimeError("GITHUB_TOKEN environment variable is required")
|
|
102
|
+
return {
|
|
103
|
+
"Authorization": f"Bearer {github_token}",
|
|
104
|
+
"Accept": "application/vnd.github+json",
|
|
105
|
+
"User-Agent": "openhands-issue-duplicate-check",
|
|
106
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def openhands_headers() -> dict[str, str]:
|
|
111
|
+
api_key = os.environ.get("OPENHANDS_API_KEY")
|
|
112
|
+
if not api_key:
|
|
113
|
+
raise RuntimeError("OPENHANDS_API_KEY environment variable is required")
|
|
114
|
+
return {
|
|
115
|
+
"Authorization": f"Bearer {api_key}",
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def request_json(
|
|
121
|
+
base_url: str,
|
|
122
|
+
path: str,
|
|
123
|
+
*,
|
|
124
|
+
method: str = "GET",
|
|
125
|
+
headers: dict[str, str] | None = None,
|
|
126
|
+
body: dict[str, Any] | None = None,
|
|
127
|
+
) -> Any:
|
|
128
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
129
|
+
url = f"{base_url}{path}"
|
|
130
|
+
request = urllib.request.Request(
|
|
131
|
+
url,
|
|
132
|
+
data=data,
|
|
133
|
+
headers=headers or {},
|
|
134
|
+
method=method,
|
|
135
|
+
)
|
|
136
|
+
try:
|
|
137
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
138
|
+
return json.load(response)
|
|
139
|
+
except urllib.error.HTTPError as exc:
|
|
140
|
+
error_body = exc.read().decode("utf-8", errors="replace")
|
|
141
|
+
raise HTTPError(method, url, exc.code, error_body) from exc
|
|
142
|
+
except json.JSONDecodeError as exc:
|
|
143
|
+
raise RuntimeError(f"Failed to parse JSON from {method} {url}: {exc}") from exc
|
|
144
|
+
except urllib.error.URLError as exc:
|
|
145
|
+
raise RuntimeError(f"{method} {url} failed: {exc}") from exc
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def fetch_issue(repository: str, issue_number: int) -> dict[str, Any]:
|
|
149
|
+
if not REPOSITORY_PATTERN.fullmatch(repository):
|
|
150
|
+
raise ValueError(f"Invalid repository format: {repository}")
|
|
151
|
+
return request_json(
|
|
152
|
+
GITHUB_API_BASE_URL,
|
|
153
|
+
f"/repos/{repository}/issues/{issue_number}",
|
|
154
|
+
headers=github_headers(),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def escape_json_text(value: str | None) -> str:
|
|
159
|
+
return json.dumps(value or "", ensure_ascii=False)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_prompt(repository: str, issue: dict[str, Any]) -> str:
|
|
163
|
+
issue_number = issue["number"]
|
|
164
|
+
issue_title = issue.get("title", "")
|
|
165
|
+
issue_body = issue.get("body") or ""
|
|
166
|
+
issue_url = issue.get("html_url", "")
|
|
167
|
+
issue_title_json = escape_json_text(issue_title)
|
|
168
|
+
issue_body_json = escape_json_text(issue_body)
|
|
169
|
+
|
|
170
|
+
return "\n".join(
|
|
171
|
+
[
|
|
172
|
+
"You are investigating whether a GitHub issue should be redirected "
|
|
173
|
+
"to an existing issue because it is either:",
|
|
174
|
+
"- an exact or near-exact duplicate, or",
|
|
175
|
+
"- so overlapping in scope that discussion or fix planning would "
|
|
176
|
+
"likely be better kept in one canonical issue.",
|
|
177
|
+
"",
|
|
178
|
+
"Be conservative about auto-close decisions, but do investigate "
|
|
179
|
+
"seriously before deciding.",
|
|
180
|
+
"",
|
|
181
|
+
f"Repository: {repository}",
|
|
182
|
+
f"New issue number: #{issue_number}",
|
|
183
|
+
f"New issue URL: {issue_url}",
|
|
184
|
+
f"New issue title (JSON-escaped string): {issue_title_json}",
|
|
185
|
+
f"New issue body (JSON-escaped string): {issue_body_json}",
|
|
186
|
+
"",
|
|
187
|
+
"Task:",
|
|
188
|
+
"1. Understand the core problem, user-facing outcome, likely root "
|
|
189
|
+
"cause, and requested fix or behavior.",
|
|
190
|
+
"2. Investigate this repository's open issues and issues closed "
|
|
191
|
+
"in the last 90 days for exact duplicates, near-duplicates, or "
|
|
192
|
+
"strong scope overlap.",
|
|
193
|
+
"3. Use multiple search approaches with diverse keywords and "
|
|
194
|
+
"phrasings rather than a single literal search.",
|
|
195
|
+
"4. Ignore pull requests.",
|
|
196
|
+
"5. Distinguish carefully between:",
|
|
197
|
+
" - duplicate: essentially the same report, request, or root cause",
|
|
198
|
+
" - overlapping-scope: not identical, but likely to fragment "
|
|
199
|
+
"discussion or produce competing fixes",
|
|
200
|
+
" - related-but-distinct: similar area, but should stay separate",
|
|
201
|
+
" - no-match: no strong candidate worth redirecting to",
|
|
202
|
+
"6. Inspect the strongest 1-3 candidates carefully. If needed, "
|
|
203
|
+
"inspect comments on the strongest candidates to disambiguate "
|
|
204
|
+
"false positives.",
|
|
205
|
+
"7. Do not post comments, do not modify files, and do not change "
|
|
206
|
+
"repository state.",
|
|
207
|
+
"8. Useful API shapes include:",
|
|
208
|
+
f" - GET {GITHUB_API_BASE_URL}/repos/{repository}/issues?state=open&per_page=100",
|
|
209
|
+
f" - GET {GITHUB_API_BASE_URL}/repos/"
|
|
210
|
+
f"{repository}/issues?state=closed&since=<ISO-8601 timestamp>&per_page=100",
|
|
211
|
+
f" - GET {GITHUB_API_BASE_URL}/search/issues?q=<query>",
|
|
212
|
+
f" - GET {GITHUB_API_BASE_URL}/repos/{repository}/issues/<number>/comments",
|
|
213
|
+
"9. Return exactly one JSON object and nothing else. Do not wrap "
|
|
214
|
+
"it in markdown fences.",
|
|
215
|
+
"",
|
|
216
|
+
"Return schema:",
|
|
217
|
+
"{",
|
|
218
|
+
f' "issue_number": {issue_number},',
|
|
219
|
+
' "should_comment": true or false,',
|
|
220
|
+
' "is_duplicate": true or false,',
|
|
221
|
+
' "auto_close_candidate": true or false,',
|
|
222
|
+
' "classification": "duplicate" | "overlapping-scope" | '
|
|
223
|
+
'"related-but-distinct" | "no-match",',
|
|
224
|
+
' "confidence": "high" | "medium" | "low",',
|
|
225
|
+
' "summary": "short explanation",',
|
|
226
|
+
' "canonical_issue_number": 123 or null,',
|
|
227
|
+
' "candidate_issues": [',
|
|
228
|
+
" {",
|
|
229
|
+
' "number": 123,',
|
|
230
|
+
f' "url": "{GITHUB_BASE_URL}/{repository}/issues/123",',
|
|
231
|
+
' "title": "issue title",',
|
|
232
|
+
' "state": "open or closed",',
|
|
233
|
+
' "closed_at": "ISO timestamp or null",',
|
|
234
|
+
' "similarity_reason": "why it looks similar"',
|
|
235
|
+
" }",
|
|
236
|
+
" ]",
|
|
237
|
+
"}",
|
|
238
|
+
"",
|
|
239
|
+
"Rules:",
|
|
240
|
+
"- `should_comment` should be true only when redirecting the "
|
|
241
|
+
"author would likely help.",
|
|
242
|
+
"- `is_duplicate` should be true only for exact or near-exact duplicates.",
|
|
243
|
+
"- `auto_close_candidate` should be true only when:",
|
|
244
|
+
" - classification is `duplicate`",
|
|
245
|
+
" - confidence is `high`",
|
|
246
|
+
" - one canonical issue clearly stands out",
|
|
247
|
+
" - a maintainer would likely be comfortable closing this issue "
|
|
248
|
+
"after a waiting period",
|
|
249
|
+
"- For `overlapping-scope`, `auto_close_candidate` must be false.",
|
|
250
|
+
"- `candidate_issues` must contain at most 3 issues, sorted best-first.",
|
|
251
|
+
"- If no strong match exists, return `should_comment: false`, "
|
|
252
|
+
'`classification: "no-match"`, `canonical_issue_number: null`, '
|
|
253
|
+
"and an empty candidate list.",
|
|
254
|
+
"- Be especially careful not to collapse broad meta, tracking, "
|
|
255
|
+
"feedback, or umbrella issues with specific bug reports unless "
|
|
256
|
+
"the new issue clearly belongs in that exact thread.",
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def start_conversation(
|
|
262
|
+
prompt: str, repository: str, issue_number: int
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
body = {
|
|
265
|
+
"title": f"Issue duplicate check #{issue_number}",
|
|
266
|
+
"selected_repository": repository,
|
|
267
|
+
"initial_message": {
|
|
268
|
+
"content": [
|
|
269
|
+
{
|
|
270
|
+
"type": "text",
|
|
271
|
+
"text": prompt,
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
return request_json(
|
|
277
|
+
OPENHANDS_BASE_URL,
|
|
278
|
+
"/api/v1/app-conversations",
|
|
279
|
+
method="POST",
|
|
280
|
+
headers=openhands_headers(),
|
|
281
|
+
body=body,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def extract_first_item(payload: Any) -> dict[str, Any] | None:
|
|
286
|
+
if isinstance(payload, list):
|
|
287
|
+
first_item = payload[0] if payload else None
|
|
288
|
+
return first_item if isinstance(first_item, dict) else None
|
|
289
|
+
if not isinstance(payload, dict):
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
items = payload.get("items")
|
|
293
|
+
if isinstance(items, list):
|
|
294
|
+
first_item = items[0] if items else None
|
|
295
|
+
return first_item if isinstance(first_item, dict) else None
|
|
296
|
+
return payload
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def summarize_openhands_item(item: dict[str, Any]) -> str:
|
|
300
|
+
summary = {}
|
|
301
|
+
for key in OPENHANDS_DEBUG_KEYS:
|
|
302
|
+
if key not in item:
|
|
303
|
+
continue
|
|
304
|
+
value = item[key]
|
|
305
|
+
if value in (None, "", [], {}):
|
|
306
|
+
continue
|
|
307
|
+
summary[key] = value
|
|
308
|
+
|
|
309
|
+
available_keys = sorted(
|
|
310
|
+
key
|
|
311
|
+
for key in item
|
|
312
|
+
if key not in summary and key not in OPENHANDS_SENSITIVE_KEYS
|
|
313
|
+
)
|
|
314
|
+
if available_keys:
|
|
315
|
+
summary["available_keys"] = available_keys
|
|
316
|
+
sensitive_keys_present = sorted(
|
|
317
|
+
key for key in item if key in OPENHANDS_SENSITIVE_KEYS
|
|
318
|
+
)
|
|
319
|
+
if sensitive_keys_present:
|
|
320
|
+
summary["sensitive_keys_present"] = sensitive_keys_present
|
|
321
|
+
return json.dumps(summary or {"available_keys": sorted(item)}, ensure_ascii=False)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def poll_start_task(
|
|
325
|
+
start_task_id: str, poll_interval_seconds: int, max_wait_seconds: int
|
|
326
|
+
) -> dict[str, Any]:
|
|
327
|
+
deadline = time.time() + max_wait_seconds
|
|
328
|
+
while time.time() < deadline:
|
|
329
|
+
payload = request_json(
|
|
330
|
+
OPENHANDS_BASE_URL,
|
|
331
|
+
f"/api/v1/app-conversations/start-tasks?ids={urllib.parse.quote(start_task_id)}",
|
|
332
|
+
headers={"Authorization": openhands_headers()["Authorization"]},
|
|
333
|
+
)
|
|
334
|
+
item = extract_first_item(payload)
|
|
335
|
+
if item is None:
|
|
336
|
+
time.sleep(poll_interval_seconds)
|
|
337
|
+
continue
|
|
338
|
+
status = str(item.get("status") or "").lower()
|
|
339
|
+
if status == "ready" and item.get("app_conversation_id"):
|
|
340
|
+
return item
|
|
341
|
+
if status in {"error", "failed"}:
|
|
342
|
+
raise RuntimeError(
|
|
343
|
+
f"OpenHands start task failed: {summarize_openhands_item(item)}"
|
|
344
|
+
)
|
|
345
|
+
time.sleep(poll_interval_seconds)
|
|
346
|
+
raise TimeoutError(
|
|
347
|
+
f"Timed out waiting for start task {start_task_id} to become ready"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def poll_conversation(
|
|
352
|
+
app_conversation_id: str, poll_interval_seconds: int, max_wait_seconds: int
|
|
353
|
+
) -> dict[str, Any]:
|
|
354
|
+
deadline = time.time() + max_wait_seconds
|
|
355
|
+
while time.time() < deadline:
|
|
356
|
+
payload = request_json(
|
|
357
|
+
OPENHANDS_BASE_URL,
|
|
358
|
+
f"/api/v1/app-conversations?ids={app_conversation_id}",
|
|
359
|
+
headers={"Authorization": openhands_headers()["Authorization"]},
|
|
360
|
+
)
|
|
361
|
+
item = extract_first_item(payload)
|
|
362
|
+
if item is None:
|
|
363
|
+
time.sleep(poll_interval_seconds)
|
|
364
|
+
continue
|
|
365
|
+
execution_status = str(item.get("execution_status", "")).lower()
|
|
366
|
+
if execution_status in FAILED_EXECUTION_STATUSES:
|
|
367
|
+
raise RuntimeError(
|
|
368
|
+
"OpenHands conversation ended with "
|
|
369
|
+
f"{execution_status}: {summarize_openhands_item(item)}"
|
|
370
|
+
)
|
|
371
|
+
if execution_status in SUCCESSFUL_TERMINAL_EXECUTION_STATUSES:
|
|
372
|
+
return item
|
|
373
|
+
time.sleep(poll_interval_seconds)
|
|
374
|
+
raise TimeoutError(
|
|
375
|
+
f"Timed out waiting for conversation {app_conversation_id} to finish running"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def validate_event_search_results(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
380
|
+
if len(events) >= EVENT_SEARCH_LIMIT:
|
|
381
|
+
raise RuntimeError(EVENT_SEARCH_LIMIT_HIT_MESSAGE)
|
|
382
|
+
return events
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def fetch_app_server_events(app_conversation_id: str) -> list[dict[str, Any]]:
|
|
386
|
+
payload = request_json(
|
|
387
|
+
OPENHANDS_BASE_URL,
|
|
388
|
+
f"/api/v1/conversation/{app_conversation_id}/events/search?limit={EVENT_SEARCH_LIMIT}",
|
|
389
|
+
headers={"Authorization": openhands_headers()["Authorization"]},
|
|
390
|
+
)
|
|
391
|
+
if isinstance(payload, dict):
|
|
392
|
+
items = payload.get("items")
|
|
393
|
+
return validate_event_search_results(items) if isinstance(items, list) else []
|
|
394
|
+
if isinstance(payload, list):
|
|
395
|
+
return validate_event_search_results(payload)
|
|
396
|
+
return []
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def fetch_agent_server_events(
|
|
400
|
+
app_conversation_id: str, agent_server_url: str, session_api_key: str
|
|
401
|
+
) -> list[dict[str, Any]]:
|
|
402
|
+
payload = request_json(
|
|
403
|
+
agent_server_url,
|
|
404
|
+
f"/api/conversations/{urllib.parse.quote(app_conversation_id)}/events/search?limit={EVENT_SEARCH_LIMIT}",
|
|
405
|
+
headers={"X-Session-API-Key": session_api_key},
|
|
406
|
+
)
|
|
407
|
+
if isinstance(payload, dict):
|
|
408
|
+
items = payload.get("items")
|
|
409
|
+
return validate_event_search_results(items) if isinstance(items, list) else []
|
|
410
|
+
if isinstance(payload, list):
|
|
411
|
+
return validate_event_search_results(payload)
|
|
412
|
+
return []
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def fetch_agent_server_final_response(
|
|
416
|
+
app_conversation_id: str, agent_server_url: str, session_api_key: str
|
|
417
|
+
) -> str:
|
|
418
|
+
payload = request_json(
|
|
419
|
+
agent_server_url,
|
|
420
|
+
f"/api/conversations/{urllib.parse.quote(app_conversation_id)}/agent_final_response",
|
|
421
|
+
headers={"X-Session-API-Key": session_api_key},
|
|
422
|
+
)
|
|
423
|
+
if not isinstance(payload, dict):
|
|
424
|
+
return ""
|
|
425
|
+
return str(payload.get("response") or "").strip()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def extract_agent_server_url(conversation_url: str) -> str | None:
|
|
429
|
+
parsed = urllib.parse.urlparse(conversation_url)
|
|
430
|
+
if not parsed.scheme or not parsed.netloc:
|
|
431
|
+
return None
|
|
432
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def extract_last_agent_text(events: list[dict[str, Any]]) -> str:
|
|
436
|
+
agent_events = [
|
|
437
|
+
event
|
|
438
|
+
for event in events
|
|
439
|
+
if event.get("kind") == "MessageEvent" and event.get("source") == "agent"
|
|
440
|
+
]
|
|
441
|
+
if not agent_events:
|
|
442
|
+
raise RuntimeError(
|
|
443
|
+
"No assistant text message was found in the conversation events"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
llm_message = agent_events[-1].get("llm_message")
|
|
447
|
+
if not isinstance(llm_message, dict):
|
|
448
|
+
raise RuntimeError("Last agent message has no llm_message field")
|
|
449
|
+
content = llm_message.get("content")
|
|
450
|
+
if not isinstance(content, list):
|
|
451
|
+
raise RuntimeError("Last agent message content is not a list")
|
|
452
|
+
|
|
453
|
+
text_parts: list[str] = []
|
|
454
|
+
for part in content:
|
|
455
|
+
if not isinstance(part, dict):
|
|
456
|
+
continue
|
|
457
|
+
if part.get("type") == "text" and part.get("text"):
|
|
458
|
+
text_parts.append(str(part["text"]))
|
|
459
|
+
if not text_parts:
|
|
460
|
+
raise RuntimeError("Last agent message contains no text content")
|
|
461
|
+
return "".join(text_parts).strip()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def parse_agent_json(text: str) -> dict[str, Any]:
|
|
465
|
+
cleaned = text.strip()
|
|
466
|
+
try:
|
|
467
|
+
return json.loads(cleaned)
|
|
468
|
+
except json.JSONDecodeError:
|
|
469
|
+
decoder = json.JSONDecoder()
|
|
470
|
+
for start, character in enumerate(cleaned):
|
|
471
|
+
if character != "{":
|
|
472
|
+
continue
|
|
473
|
+
try:
|
|
474
|
+
candidate, end = decoder.raw_decode(cleaned[start:])
|
|
475
|
+
except json.JSONDecodeError:
|
|
476
|
+
continue
|
|
477
|
+
trailing = cleaned[start + end :].strip()
|
|
478
|
+
if trailing not in {"", "```"}:
|
|
479
|
+
continue
|
|
480
|
+
if isinstance(candidate, dict):
|
|
481
|
+
return candidate
|
|
482
|
+
raise ValueError("No valid JSON object found in the agent response")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def as_bool(value: Any) -> bool:
|
|
486
|
+
if isinstance(value, bool):
|
|
487
|
+
return value
|
|
488
|
+
if isinstance(value, str):
|
|
489
|
+
return value.strip().lower() in {"true", "1", "yes"}
|
|
490
|
+
if isinstance(value, (int, float)):
|
|
491
|
+
return bool(value)
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def normalize_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
496
|
+
normalized = dict(result)
|
|
497
|
+
normalized["should_comment"] = as_bool(normalized.get("should_comment"))
|
|
498
|
+
normalized["is_duplicate"] = as_bool(normalized.get("is_duplicate"))
|
|
499
|
+
normalized["auto_close_candidate"] = as_bool(normalized.get("auto_close_candidate"))
|
|
500
|
+
|
|
501
|
+
classification = str(normalized.get("classification") or "no-match").strip().lower()
|
|
502
|
+
if classification not in {
|
|
503
|
+
"duplicate",
|
|
504
|
+
"overlapping-scope",
|
|
505
|
+
"related-but-distinct",
|
|
506
|
+
"no-match",
|
|
507
|
+
}:
|
|
508
|
+
classification = "no-match"
|
|
509
|
+
normalized["classification"] = classification
|
|
510
|
+
|
|
511
|
+
confidence = str(normalized.get("confidence") or "low").strip().lower()
|
|
512
|
+
if confidence not in {"high", "medium", "low"}:
|
|
513
|
+
confidence = "low"
|
|
514
|
+
normalized["confidence"] = confidence
|
|
515
|
+
|
|
516
|
+
try:
|
|
517
|
+
canonical_issue_number = normalized.get("canonical_issue_number")
|
|
518
|
+
if canonical_issue_number in {None, ""}:
|
|
519
|
+
normalized["canonical_issue_number"] = None
|
|
520
|
+
else:
|
|
521
|
+
normalized["canonical_issue_number"] = int(str(canonical_issue_number))
|
|
522
|
+
except (TypeError, ValueError):
|
|
523
|
+
normalized["canonical_issue_number"] = None
|
|
524
|
+
|
|
525
|
+
candidate_issues = normalized.get("candidate_issues")
|
|
526
|
+
if not isinstance(candidate_issues, list):
|
|
527
|
+
candidate_issues = []
|
|
528
|
+
normalized["candidate_issues"] = candidate_issues[:3]
|
|
529
|
+
if not normalized["candidate_issues"]:
|
|
530
|
+
normalized["should_comment"] = False
|
|
531
|
+
|
|
532
|
+
if classification not in {"duplicate", "overlapping-scope"}:
|
|
533
|
+
normalized["should_comment"] = False
|
|
534
|
+
if classification != "duplicate":
|
|
535
|
+
normalized["is_duplicate"] = False
|
|
536
|
+
normalized["auto_close_candidate"] = False
|
|
537
|
+
if normalized["should_comment"] and confidence not in {"high", "medium"}:
|
|
538
|
+
normalized["should_comment"] = False
|
|
539
|
+
if normalized["auto_close_candidate"] and not normalized["should_comment"]:
|
|
540
|
+
normalized["auto_close_candidate"] = False
|
|
541
|
+
if normalized["auto_close_candidate"] and confidence != "high":
|
|
542
|
+
normalized["auto_close_candidate"] = False
|
|
543
|
+
if normalized["auto_close_candidate"] and not normalized["candidate_issues"]:
|
|
544
|
+
normalized["auto_close_candidate"] = False
|
|
545
|
+
if (
|
|
546
|
+
normalized["auto_close_candidate"]
|
|
547
|
+
and normalized["canonical_issue_number"] is None
|
|
548
|
+
):
|
|
549
|
+
first_candidate = (
|
|
550
|
+
normalized["candidate_issues"][0] if normalized["candidate_issues"] else {}
|
|
551
|
+
)
|
|
552
|
+
candidate_number = first_candidate.get("number")
|
|
553
|
+
try:
|
|
554
|
+
if candidate_number is None:
|
|
555
|
+
raise ValueError("candidate number is missing")
|
|
556
|
+
normalized["canonical_issue_number"] = int(str(candidate_number))
|
|
557
|
+
except (TypeError, ValueError, AttributeError):
|
|
558
|
+
normalized["auto_close_candidate"] = False
|
|
559
|
+
|
|
560
|
+
normalized["summary"] = str(normalized.get("summary") or "").strip()
|
|
561
|
+
return normalized
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def main() -> int:
|
|
565
|
+
args = parse_args()
|
|
566
|
+
issue = fetch_issue(args.repository, args.issue_number)
|
|
567
|
+
if issue.get("pull_request"):
|
|
568
|
+
raise RuntimeError(f"#{args.issue_number} is a pull request, not an issue")
|
|
569
|
+
|
|
570
|
+
prompt = build_prompt(args.repository, issue)
|
|
571
|
+
start_task = start_conversation(prompt, args.repository, args.issue_number)
|
|
572
|
+
app_conversation_id = start_task.get("app_conversation_id")
|
|
573
|
+
conversation_url = ""
|
|
574
|
+
|
|
575
|
+
if not app_conversation_id:
|
|
576
|
+
task_id = start_task.get("id")
|
|
577
|
+
if not task_id:
|
|
578
|
+
raise RuntimeError(
|
|
579
|
+
"Missing id in start task response: "
|
|
580
|
+
f"{summarize_openhands_item(start_task)}"
|
|
581
|
+
)
|
|
582
|
+
ready_task = poll_start_task(
|
|
583
|
+
task_id,
|
|
584
|
+
args.poll_interval_seconds,
|
|
585
|
+
args.max_wait_seconds,
|
|
586
|
+
)
|
|
587
|
+
app_conversation_id = ready_task.get("app_conversation_id")
|
|
588
|
+
if not app_conversation_id:
|
|
589
|
+
raise RuntimeError(
|
|
590
|
+
"Missing app_conversation_id in response: "
|
|
591
|
+
f"{summarize_openhands_item(ready_task)}"
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
conversation = poll_conversation(
|
|
595
|
+
app_conversation_id,
|
|
596
|
+
args.poll_interval_seconds,
|
|
597
|
+
args.max_wait_seconds,
|
|
598
|
+
)
|
|
599
|
+
conversation_url = (
|
|
600
|
+
conversation.get("conversation_url")
|
|
601
|
+
or f"{OPENHANDS_BASE_URL}/conversations/{app_conversation_id}"
|
|
602
|
+
)
|
|
603
|
+
session_api_key_value = conversation.get("session_api_key")
|
|
604
|
+
if session_api_key_value and not isinstance(session_api_key_value, str):
|
|
605
|
+
raise RuntimeError(
|
|
606
|
+
"session_api_key had unexpected type in the OpenHands conversation: "
|
|
607
|
+
f"{type(session_api_key_value).__name__}"
|
|
608
|
+
)
|
|
609
|
+
session_api_key = session_api_key_value or ""
|
|
610
|
+
agent_server_url = extract_agent_server_url(conversation_url)
|
|
611
|
+
|
|
612
|
+
agent_text = ""
|
|
613
|
+
if agent_server_url and session_api_key:
|
|
614
|
+
try:
|
|
615
|
+
agent_text = fetch_agent_server_final_response(
|
|
616
|
+
app_conversation_id,
|
|
617
|
+
agent_server_url,
|
|
618
|
+
session_api_key,
|
|
619
|
+
)
|
|
620
|
+
except RuntimeError:
|
|
621
|
+
agent_text = ""
|
|
622
|
+
if not agent_text:
|
|
623
|
+
events = fetch_app_server_events(app_conversation_id)
|
|
624
|
+
try:
|
|
625
|
+
agent_text = extract_last_agent_text(events)
|
|
626
|
+
except RuntimeError as exc:
|
|
627
|
+
if not session_api_key:
|
|
628
|
+
raise RuntimeError(
|
|
629
|
+
"App server events did not contain assistant text and "
|
|
630
|
+
"session_api_key was missing from the OpenHands conversation"
|
|
631
|
+
) from exc
|
|
632
|
+
if not agent_server_url:
|
|
633
|
+
raise RuntimeError(
|
|
634
|
+
"App server events did not contain assistant text and cannot "
|
|
635
|
+
"extract agent server URL from conversation URL: "
|
|
636
|
+
f"{conversation_url}"
|
|
637
|
+
) from exc
|
|
638
|
+
events = fetch_agent_server_events(
|
|
639
|
+
app_conversation_id,
|
|
640
|
+
agent_server_url,
|
|
641
|
+
session_api_key,
|
|
642
|
+
)
|
|
643
|
+
agent_text = extract_last_agent_text(events)
|
|
644
|
+
result = normalize_result(parse_agent_json(agent_text))
|
|
645
|
+
|
|
646
|
+
result["issue_number"] = args.issue_number
|
|
647
|
+
result["repository"] = args.repository
|
|
648
|
+
result["app_conversation_id"] = app_conversation_id
|
|
649
|
+
result["conversation_url"] = conversation_url
|
|
650
|
+
result["agent_response"] = agent_text
|
|
651
|
+
|
|
652
|
+
output_path = Path(args.output)
|
|
653
|
+
try:
|
|
654
|
+
output_path.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
|
655
|
+
except OSError as exc:
|
|
656
|
+
raise RuntimeError(f"Failed to write output to {output_path}: {exc}") from exc
|
|
657
|
+
|
|
658
|
+
print(
|
|
659
|
+
json.dumps(
|
|
660
|
+
{
|
|
661
|
+
"issue_number": result.get("issue_number"),
|
|
662
|
+
"should_comment": result.get("should_comment"),
|
|
663
|
+
"is_duplicate": result.get("is_duplicate"),
|
|
664
|
+
"auto_close_candidate": result.get("auto_close_candidate"),
|
|
665
|
+
"classification": result.get("classification"),
|
|
666
|
+
"confidence": result.get("confidence"),
|
|
667
|
+
"conversation_url": result.get("conversation_url"),
|
|
668
|
+
"output": str(output_path),
|
|
669
|
+
},
|
|
670
|
+
ensure_ascii=False,
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
return 0
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
if __name__ == "__main__":
|
|
677
|
+
try:
|
|
678
|
+
raise SystemExit(main())
|
|
679
|
+
except Exception as exc: # noqa: BLE001
|
|
680
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
681
|
+
raise
|