@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,780 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Laminar Signal Analysis Script
|
|
4
|
+
|
|
5
|
+
Downloads signal events from Laminar and uses an LLM to analyze patterns.
|
|
6
|
+
Supports customizable prompts via Jinja templates for different use cases.
|
|
7
|
+
Uses function calling to get structured output with trace IDs.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python analyze.py --signal "pr review suggestion and analysis"
|
|
11
|
+
python analyze.py --signal "my-signal" --prompt-file custom_prompt.j2
|
|
12
|
+
python analyze.py --signal "my-signal" --days 30 --format json
|
|
13
|
+
|
|
14
|
+
Environment Variables:
|
|
15
|
+
LMNR_PROJECT_API_KEY: Laminar project API key (required)
|
|
16
|
+
LLM_API_KEY: API key for the LLM (required)
|
|
17
|
+
LLM_MODEL: Model to use (default: gemini-3-pro-preview)
|
|
18
|
+
LLM_BASE_URL: Base URL for LLM API (default: https://llm-proxy.app.all-hands.dev)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import urllib.request
|
|
26
|
+
import urllib.error
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from jinja2 import Template
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
LAMINAR_API_URL = "https://api.lmnr.ai/v1/sql/query"
|
|
33
|
+
LAMINAR_APP_URL = "https://laminar.sh"
|
|
34
|
+
DEFAULT_LLM_BASE_URL = "https://llm-proxy.app.all-hands.dev"
|
|
35
|
+
DEFAULT_LLM_MODEL = "gemini-3-pro-preview"
|
|
36
|
+
DEFAULT_DAYS_LOOKBACK = 90
|
|
37
|
+
|
|
38
|
+
# Directory containing this script
|
|
39
|
+
SCRIPT_DIR = Path(__file__).parent
|
|
40
|
+
TEMPLATES_DIR = SCRIPT_DIR / "templates"
|
|
41
|
+
|
|
42
|
+
# Mapping of signal names to their template files
|
|
43
|
+
BUILTIN_TEMPLATES = {
|
|
44
|
+
"pr review suggestion and analysis": "pr_review.j2",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_skill_content(skill_dir: str) -> str:
|
|
49
|
+
"""Load skill/plugin content from a directory.
|
|
50
|
+
|
|
51
|
+
Looks for SKILL.md files in the directory and any subdirectories.
|
|
52
|
+
For plugins, also looks in skills/ subdirectory and scripts/prompt.py.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
skill_dir: Path to skill or plugin directory
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Combined content of all skill files found
|
|
59
|
+
"""
|
|
60
|
+
skill_path = Path(skill_dir)
|
|
61
|
+
if not skill_path.exists():
|
|
62
|
+
raise FileNotFoundError(f"Skill directory not found: {skill_dir}")
|
|
63
|
+
|
|
64
|
+
content_parts = []
|
|
65
|
+
|
|
66
|
+
# Load main SKILL.md if present
|
|
67
|
+
main_skill = skill_path / "SKILL.md"
|
|
68
|
+
if main_skill.exists():
|
|
69
|
+
content_parts.append(f"## Skill: {skill_path.name}\n\n{main_skill.read_text()}")
|
|
70
|
+
|
|
71
|
+
# For plugins, check for prompt.py
|
|
72
|
+
prompt_py = skill_path / "scripts" / "prompt.py"
|
|
73
|
+
if prompt_py.exists():
|
|
74
|
+
content_parts.append(f"## Prompt Template ({prompt_py.name})\n\n```python\n{prompt_py.read_text()}\n```")
|
|
75
|
+
|
|
76
|
+
# Check for nested skills (common in plugins)
|
|
77
|
+
skills_subdir = skill_path / "skills"
|
|
78
|
+
if skills_subdir.exists():
|
|
79
|
+
for nested_skill_dir in skills_subdir.iterdir():
|
|
80
|
+
if nested_skill_dir.is_dir():
|
|
81
|
+
nested_skill_md = nested_skill_dir / "SKILL.md"
|
|
82
|
+
if nested_skill_md.exists():
|
|
83
|
+
content_parts.append(
|
|
84
|
+
f"## Nested Skill: {nested_skill_dir.name}\n\n{nested_skill_md.read_text()}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not content_parts:
|
|
88
|
+
raise FileNotFoundError(f"No SKILL.md or prompt.py found in: {skill_dir}")
|
|
89
|
+
|
|
90
|
+
return "\n\n---\n\n".join(content_parts)
|
|
91
|
+
|
|
92
|
+
# JSON Schema for the analysis function call output
|
|
93
|
+
ANALYSIS_FUNCTION = {
|
|
94
|
+
"name": "report_analysis",
|
|
95
|
+
"description": "Report the analysis of signal events, focusing on issues and areas for improvement",
|
|
96
|
+
"parameters": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"issues": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"description": "List of issues, problems, and areas needing improvement (THIS IS THE PRIMARY FOCUS)",
|
|
102
|
+
"items": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"title": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "Short title for this issue"
|
|
108
|
+
},
|
|
109
|
+
"description": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Detailed description of the issue, why it's problematic, and its impact"
|
|
112
|
+
},
|
|
113
|
+
"severity": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"enum": ["critical", "high", "medium", "low"],
|
|
116
|
+
"description": "How severe/impactful this issue is"
|
|
117
|
+
},
|
|
118
|
+
"frequency": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "How often this issue occurs (e.g., '15% of traces', 'frequent', 'occasional')"
|
|
121
|
+
},
|
|
122
|
+
"trace_urls": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"description": "Up to 5 representative trace URLs demonstrating this issue",
|
|
125
|
+
"items": {"type": "string"},
|
|
126
|
+
"maxItems": 5
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"required": ["title", "description", "severity", "trace_urls"]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"recommendations": {
|
|
133
|
+
"type": "array",
|
|
134
|
+
"description": "Specific, actionable recommendations with CONCRETE implementation details (e.g., exact prompt changes, code snippets)",
|
|
135
|
+
"items": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"title": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "Short title for this recommendation"
|
|
141
|
+
},
|
|
142
|
+
"description": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"description": "Detailed description with SPECIFIC implementation. Include exact prompt text changes, code modifications, or configuration updates. Use 'Change X to Y' format where possible."
|
|
145
|
+
},
|
|
146
|
+
"prompt_changes": {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"description": "Specific prompt/instruction changes. Each item should have 'before' (current text) and 'after' (proposed text)",
|
|
149
|
+
"items": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"section": {"type": "string", "description": "Which section of the prompt to modify"},
|
|
153
|
+
"before": {"type": "string", "description": "Current text (or 'N/A' if adding new)"},
|
|
154
|
+
"after": {"type": "string", "description": "Proposed new text"}
|
|
155
|
+
},
|
|
156
|
+
"required": ["section", "after"]
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"addresses": {
|
|
160
|
+
"type": "array",
|
|
161
|
+
"description": "Which issues this recommendation fixes",
|
|
162
|
+
"items": {"type": "string"}
|
|
163
|
+
},
|
|
164
|
+
"priority": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"enum": ["high", "medium", "low"],
|
|
167
|
+
"description": "Priority for implementing this recommendation"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"required": ["title", "description", "addresses", "priority"]
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"strengths": {
|
|
174
|
+
"type": "array",
|
|
175
|
+
"description": "Brief list of things working well (keep this short, focus is on improvements)",
|
|
176
|
+
"items": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"title": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"description": "Short title"
|
|
182
|
+
},
|
|
183
|
+
"description": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Brief description of what's working well"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"required": ["title", "description"]
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"metrics": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"description": "Quantitative metrics from the analysis",
|
|
194
|
+
"properties": {
|
|
195
|
+
"total_signals": {
|
|
196
|
+
"type": "integer",
|
|
197
|
+
"description": "Total number of signals analyzed"
|
|
198
|
+
},
|
|
199
|
+
"issue_rate": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Percentage of signals showing issues"
|
|
202
|
+
},
|
|
203
|
+
"key_statistics": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"description": "Other relevant statistics",
|
|
206
|
+
"items": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"properties": {
|
|
209
|
+
"name": {"type": "string"},
|
|
210
|
+
"value": {"type": "string"}
|
|
211
|
+
},
|
|
212
|
+
"required": ["name", "value"]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"required": ["total_signals"]
|
|
217
|
+
},
|
|
218
|
+
"summary": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"description": "Executive summary focusing on the most critical improvements needed"
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"required": ["issues", "recommendations", "metrics", "summary"]
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_env_var(name: str, required: bool = True) -> str | None:
|
|
229
|
+
"""Get an environment variable.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
name: Environment variable name
|
|
233
|
+
required: If True, raise error if not set. If False, return None.
|
|
234
|
+
"""
|
|
235
|
+
value = os.getenv(name)
|
|
236
|
+
if not value and required:
|
|
237
|
+
raise ValueError(f"{name} environment variable is required")
|
|
238
|
+
return value
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_llm_config() -> tuple[str, str, str]:
|
|
242
|
+
"""Get LLM configuration from environment variables.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Tuple of (api_key, model, base_url)
|
|
246
|
+
"""
|
|
247
|
+
api_key = get_env_var("LLM_API_KEY")
|
|
248
|
+
model = os.getenv("LLM_MODEL", DEFAULT_LLM_MODEL)
|
|
249
|
+
base_url = os.getenv("LLM_BASE_URL", DEFAULT_LLM_BASE_URL)
|
|
250
|
+
return api_key, model, base_url
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def query_laminar_signals(api_key: str, signal_name: str, days: int) -> list[dict]:
|
|
254
|
+
"""Query Laminar SQL API to fetch signal events.
|
|
255
|
+
|
|
256
|
+
Note: days is validated as int by argparse, so no SQL injection risk there.
|
|
257
|
+
"""
|
|
258
|
+
# Escape single quotes to prevent SQL injection
|
|
259
|
+
escaped_signal = signal_name.replace("'", "''")
|
|
260
|
+
query = f"""
|
|
261
|
+
SELECT id, trace_id, name, payload, timestamp
|
|
262
|
+
FROM signal_events
|
|
263
|
+
WHERE name = '{escaped_signal}'
|
|
264
|
+
AND timestamp > now() - INTERVAL {days} DAY
|
|
265
|
+
ORDER BY timestamp DESC
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
request_data = json.dumps({"query": query}).encode("utf-8")
|
|
269
|
+
request = urllib.request.Request(
|
|
270
|
+
LAMINAR_API_URL,
|
|
271
|
+
data=request_data,
|
|
272
|
+
headers={
|
|
273
|
+
"Authorization": f"Bearer {api_key}",
|
|
274
|
+
"Content-Type": "application/json",
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
280
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
281
|
+
return result.get("data", [])
|
|
282
|
+
except urllib.error.HTTPError as e:
|
|
283
|
+
print(f"Error querying Laminar API: HTTP {e.code}", file=sys.stderr)
|
|
284
|
+
print(f"Response: {e.read().decode('utf-8')}", file=sys.stderr)
|
|
285
|
+
sys.exit(1)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def list_available_signals(api_key: str, days: int) -> list[dict]:
|
|
289
|
+
"""List all available signal names and their counts.
|
|
290
|
+
|
|
291
|
+
Note: days is validated as int by argparse, so no SQL injection risk.
|
|
292
|
+
"""
|
|
293
|
+
query = f"""
|
|
294
|
+
SELECT name, COUNT(*) as count
|
|
295
|
+
FROM signal_events
|
|
296
|
+
WHERE timestamp > now() - INTERVAL {days} DAY
|
|
297
|
+
GROUP BY name
|
|
298
|
+
ORDER BY count DESC
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
request_data = json.dumps({"query": query}).encode("utf-8")
|
|
302
|
+
request = urllib.request.Request(
|
|
303
|
+
LAMINAR_API_URL,
|
|
304
|
+
data=request_data,
|
|
305
|
+
headers={
|
|
306
|
+
"Authorization": f"Bearer {api_key}",
|
|
307
|
+
"Content-Type": "application/json",
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
313
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
314
|
+
return result.get("data", [])
|
|
315
|
+
except urllib.error.HTTPError as e:
|
|
316
|
+
print(f"Error querying Laminar API: HTTP {e.code}", file=sys.stderr)
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def parse_signal(signal: dict, project_id: str | None = None) -> dict:
|
|
321
|
+
"""Parse signal and extract payload as both dict and formatted JSON."""
|
|
322
|
+
payload_str = signal.get("payload", "{}")
|
|
323
|
+
try:
|
|
324
|
+
payload = json.loads(payload_str)
|
|
325
|
+
except json.JSONDecodeError:
|
|
326
|
+
payload = {}
|
|
327
|
+
|
|
328
|
+
signal_id = signal.get("id")
|
|
329
|
+
trace_id = signal.get("trace_id")
|
|
330
|
+
|
|
331
|
+
# Construct the Laminar trace URL
|
|
332
|
+
# Format: https://laminar.sh/project/{project_id}/traces?traceId={trace_id}
|
|
333
|
+
if project_id and trace_id:
|
|
334
|
+
trace_url = f"{LAMINAR_APP_URL}/project/{project_id}/traces?traceId={trace_id}"
|
|
335
|
+
else:
|
|
336
|
+
trace_url = None
|
|
337
|
+
|
|
338
|
+
# Return signal data with both parsed payload fields and formatted JSON
|
|
339
|
+
result = {
|
|
340
|
+
"id": signal_id,
|
|
341
|
+
"trace_id": trace_id,
|
|
342
|
+
"trace_url": trace_url,
|
|
343
|
+
"timestamp": signal.get("timestamp"),
|
|
344
|
+
"payload": payload,
|
|
345
|
+
"payload_json": json.dumps(payload, indent=2),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Also flatten payload fields to top level for easy template access
|
|
349
|
+
for key, value in payload.items():
|
|
350
|
+
if key not in result:
|
|
351
|
+
result[key] = value
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def load_prompt_template(
|
|
357
|
+
signal_name: str,
|
|
358
|
+
prompt_file: str | None = None,
|
|
359
|
+
) -> str:
|
|
360
|
+
"""Load the appropriate prompt template.
|
|
361
|
+
|
|
362
|
+
Priority:
|
|
363
|
+
1. Custom prompt file if provided
|
|
364
|
+
2. Built-in template for the signal type
|
|
365
|
+
3. Default template
|
|
366
|
+
"""
|
|
367
|
+
# If a custom prompt file is provided, use it
|
|
368
|
+
if prompt_file:
|
|
369
|
+
prompt_path = Path(prompt_file)
|
|
370
|
+
if not prompt_path.exists():
|
|
371
|
+
print(f"Error: Prompt file not found: {prompt_file}", file=sys.stderr)
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
return prompt_path.read_text()
|
|
374
|
+
|
|
375
|
+
# Check for built-in template for this signal type
|
|
376
|
+
if signal_name in BUILTIN_TEMPLATES:
|
|
377
|
+
template_file = TEMPLATES_DIR / BUILTIN_TEMPLATES[signal_name]
|
|
378
|
+
if template_file.exists():
|
|
379
|
+
return template_file.read_text()
|
|
380
|
+
|
|
381
|
+
# Fall back to default template
|
|
382
|
+
default_template = TEMPLATES_DIR / "default.j2"
|
|
383
|
+
if default_template.exists():
|
|
384
|
+
return default_template.read_text()
|
|
385
|
+
|
|
386
|
+
# Fallback if templates directory doesn't exist
|
|
387
|
+
raise FileNotFoundError(
|
|
388
|
+
f"No template found. Expected templates in: {TEMPLATES_DIR}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def build_analysis_prompt(
|
|
393
|
+
signals: list[dict],
|
|
394
|
+
signal_name: str,
|
|
395
|
+
template_str: str,
|
|
396
|
+
skill_content: str | None = None,
|
|
397
|
+
) -> str:
|
|
398
|
+
"""Build the analysis prompt using Jinja template."""
|
|
399
|
+
template = Template(template_str)
|
|
400
|
+
prompt = template.render(
|
|
401
|
+
signals=signals,
|
|
402
|
+
num_signals=len(signals),
|
|
403
|
+
signal_name=signal_name,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Append skill content if provided
|
|
407
|
+
if skill_content:
|
|
408
|
+
prompt += f"""
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Current Agent Skill/Prompt Configuration
|
|
413
|
+
|
|
414
|
+
The following is the current skill/prompt configuration that the agent uses.
|
|
415
|
+
Your recommendations MUST be grounded in this configuration with SPECIFIC, ACTIONABLE changes.
|
|
416
|
+
|
|
417
|
+
{skill_content}
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## CRITICAL REQUIREMENTS FOR RECOMMENDATIONS
|
|
422
|
+
|
|
423
|
+
Your recommendations MUST include SPECIFIC, ACTIONABLE prompt changes:
|
|
424
|
+
|
|
425
|
+
1. **Quote the exact text** from the skill/prompt that needs to change
|
|
426
|
+
2. **Provide the replacement text** - not vague suggestions, but actual wording
|
|
427
|
+
3. **Specify the section** where the change should be made
|
|
428
|
+
4. **Use the prompt_changes field** to provide before/after diffs
|
|
429
|
+
|
|
430
|
+
❌ BAD (vague): "Update the prompt to be more strict about blocking PRs"
|
|
431
|
+
✅ GOOD (specific):
|
|
432
|
+
- Section: VERDICT
|
|
433
|
+
- Before: "❌ Needs rework: Fundamental design issues must be addressed first"
|
|
434
|
+
- After: "❌ Needs rework: BLOCK the PR if any of these are found: 1. Security vulnerabilities 2. Race conditions 3. Missing tests for new behavior. Do NOT approve with suggestions for these categories."
|
|
435
|
+
|
|
436
|
+
❌ BAD: "Add instructions about deduplication"
|
|
437
|
+
✅ GOOD:
|
|
438
|
+
- Section: CRITICAL REVIEW OUTPUT FORMAT (new subsection)
|
|
439
|
+
- After: "**NOISE CONTROL**: If the same issue appears in multiple locations, post ONE comment on the first occurrence that summarizes the pattern (e.g., 'This None-check issue appears on lines X, Y, Z') rather than commenting on each line."
|
|
440
|
+
|
|
441
|
+
Be as specific as possible. The goal is for someone to copy-paste your suggested changes directly into the skill file.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
return prompt
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def query_llm(api_key: str, prompt: str, model: str, base_url: str) -> dict:
|
|
448
|
+
"""Query the LLM with the analysis prompt using function calling.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Parsed JSON object from the function call response.
|
|
452
|
+
"""
|
|
453
|
+
url = f"{base_url.rstrip('/')}/v1/chat/completions"
|
|
454
|
+
|
|
455
|
+
request_data = json.dumps({
|
|
456
|
+
"model": model,
|
|
457
|
+
"messages": [
|
|
458
|
+
{
|
|
459
|
+
"role": "user",
|
|
460
|
+
"content": prompt,
|
|
461
|
+
}
|
|
462
|
+
],
|
|
463
|
+
"tools": [
|
|
464
|
+
{
|
|
465
|
+
"type": "function",
|
|
466
|
+
"function": ANALYSIS_FUNCTION,
|
|
467
|
+
}
|
|
468
|
+
],
|
|
469
|
+
"tool_choice": {
|
|
470
|
+
"type": "function",
|
|
471
|
+
"function": {"name": "report_analysis"}
|
|
472
|
+
},
|
|
473
|
+
"max_tokens": 8192,
|
|
474
|
+
"temperature": 0.7,
|
|
475
|
+
}).encode("utf-8")
|
|
476
|
+
|
|
477
|
+
request = urllib.request.Request(
|
|
478
|
+
url,
|
|
479
|
+
data=request_data,
|
|
480
|
+
headers={
|
|
481
|
+
"Authorization": f"Bearer {api_key}",
|
|
482
|
+
"Content-Type": "application/json",
|
|
483
|
+
},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
with urllib.request.urlopen(request, timeout=300) as response:
|
|
488
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
489
|
+
|
|
490
|
+
# Extract the function call arguments
|
|
491
|
+
message = result["choices"][0]["message"]
|
|
492
|
+
if "tool_calls" in message and message["tool_calls"]:
|
|
493
|
+
tool_call = message["tool_calls"][0]
|
|
494
|
+
if tool_call["type"] == "function":
|
|
495
|
+
return json.loads(tool_call["function"]["arguments"])
|
|
496
|
+
|
|
497
|
+
# Fallback: try to parse content as JSON if no tool call
|
|
498
|
+
if message.get("content"):
|
|
499
|
+
content = message["content"]
|
|
500
|
+
# Try to extract JSON from markdown code blocks
|
|
501
|
+
if "```json" in content:
|
|
502
|
+
start = content.find("```json") + 7
|
|
503
|
+
end = content.find("```", start)
|
|
504
|
+
if end > start:
|
|
505
|
+
content = content[start:end].strip()
|
|
506
|
+
elif "```" in content:
|
|
507
|
+
start = content.find("```") + 3
|
|
508
|
+
end = content.find("```", start)
|
|
509
|
+
if end > start:
|
|
510
|
+
content = content[start:end].strip()
|
|
511
|
+
try:
|
|
512
|
+
return json.loads(content)
|
|
513
|
+
except json.JSONDecodeError:
|
|
514
|
+
print(f"Warning: Could not parse LLM response as JSON", file=sys.stderr)
|
|
515
|
+
print(f"Response content: {message['content'][:500]}...", file=sys.stderr)
|
|
516
|
+
|
|
517
|
+
raise ValueError("No function call response received from LLM")
|
|
518
|
+
|
|
519
|
+
except urllib.error.HTTPError as e:
|
|
520
|
+
print(f"Error querying LLM: HTTP {e.code}", file=sys.stderr)
|
|
521
|
+
print(f"Response: {e.read().decode('utf-8')}", file=sys.stderr)
|
|
522
|
+
sys.exit(1)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def format_analysis_as_markdown(analysis: dict, signal_name: str) -> str:
|
|
526
|
+
"""Convert the structured analysis output to markdown format."""
|
|
527
|
+
lines = [
|
|
528
|
+
"# Agent Improvement Report",
|
|
529
|
+
"",
|
|
530
|
+
f"**Signal:** `{signal_name}`",
|
|
531
|
+
"",
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
# Executive Summary
|
|
535
|
+
if analysis.get("summary"):
|
|
536
|
+
lines.append("## Executive Summary")
|
|
537
|
+
lines.append("")
|
|
538
|
+
lines.append(analysis["summary"])
|
|
539
|
+
lines.append("")
|
|
540
|
+
|
|
541
|
+
# Issues (Primary Focus)
|
|
542
|
+
lines.append("## Issues Requiring Attention")
|
|
543
|
+
lines.append("")
|
|
544
|
+
for i, issue in enumerate(analysis.get("issues", []), 1):
|
|
545
|
+
severity = issue.get("severity", "medium").upper()
|
|
546
|
+
lines.append(f"### {i}. [{severity}] {issue['title']}")
|
|
547
|
+
lines.append("")
|
|
548
|
+
lines.append(issue["description"])
|
|
549
|
+
lines.append("")
|
|
550
|
+
if issue.get("frequency"):
|
|
551
|
+
lines.append(f"**Frequency:** {issue['frequency']}")
|
|
552
|
+
lines.append("")
|
|
553
|
+
if issue.get("trace_urls"):
|
|
554
|
+
lines.append("**Example traces:**")
|
|
555
|
+
for url in issue["trace_urls"]:
|
|
556
|
+
lines.append(f"- {url}")
|
|
557
|
+
lines.append("")
|
|
558
|
+
|
|
559
|
+
# Recommendations
|
|
560
|
+
lines.append("## Recommended Fixes")
|
|
561
|
+
lines.append("")
|
|
562
|
+
for i, rec in enumerate(analysis.get("recommendations", []), 1):
|
|
563
|
+
priority = rec.get("priority", "medium").upper()
|
|
564
|
+
lines.append(f"### {i}. [{priority} PRIORITY] {rec['title']}")
|
|
565
|
+
lines.append("")
|
|
566
|
+
lines.append(rec["description"])
|
|
567
|
+
lines.append("")
|
|
568
|
+
|
|
569
|
+
# Display specific prompt changes if provided
|
|
570
|
+
if rec.get("prompt_changes"):
|
|
571
|
+
lines.append("**Suggested Prompt Changes:**")
|
|
572
|
+
lines.append("")
|
|
573
|
+
for change in rec["prompt_changes"]:
|
|
574
|
+
section = change.get("section", "Unknown section")
|
|
575
|
+
before = change.get("before", "N/A")
|
|
576
|
+
after = change.get("after", "")
|
|
577
|
+
lines.append(f"📍 **Section:** {section}")
|
|
578
|
+
lines.append("")
|
|
579
|
+
if before and before != "N/A":
|
|
580
|
+
lines.append("```diff")
|
|
581
|
+
lines.append(f"- {before}")
|
|
582
|
+
lines.append(f"+ {after}")
|
|
583
|
+
lines.append("```")
|
|
584
|
+
else:
|
|
585
|
+
lines.append("```")
|
|
586
|
+
lines.append(f"+ {after}")
|
|
587
|
+
lines.append("```")
|
|
588
|
+
lines.append("")
|
|
589
|
+
|
|
590
|
+
if rec.get("addresses"):
|
|
591
|
+
lines.append(f"*Fixes: {', '.join(rec['addresses'])}*")
|
|
592
|
+
lines.append("")
|
|
593
|
+
|
|
594
|
+
# Metrics
|
|
595
|
+
metrics = analysis.get("metrics", {})
|
|
596
|
+
lines.append("## Metrics")
|
|
597
|
+
lines.append("")
|
|
598
|
+
lines.append(f"**Total signals analyzed:** {metrics.get('total_signals', 'N/A')}")
|
|
599
|
+
if metrics.get("issue_rate"):
|
|
600
|
+
lines.append(f"**Issue rate:** {metrics['issue_rate']}")
|
|
601
|
+
lines.append("")
|
|
602
|
+
|
|
603
|
+
if metrics.get("key_statistics"):
|
|
604
|
+
lines.append("| Metric | Value |")
|
|
605
|
+
lines.append("|--------|-------|")
|
|
606
|
+
for stat in metrics["key_statistics"]:
|
|
607
|
+
lines.append(f"| {stat['name']} | {stat['value']} |")
|
|
608
|
+
lines.append("")
|
|
609
|
+
|
|
610
|
+
# Strengths (Brief)
|
|
611
|
+
if analysis.get("strengths"):
|
|
612
|
+
lines.append("## What's Working Well")
|
|
613
|
+
lines.append("")
|
|
614
|
+
for strength in analysis["strengths"]:
|
|
615
|
+
lines.append(f"- **{strength['title']}**: {strength['description']}")
|
|
616
|
+
lines.append("")
|
|
617
|
+
|
|
618
|
+
return "\n".join(lines)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def main():
|
|
622
|
+
"""Main entry point."""
|
|
623
|
+
parser = argparse.ArgumentParser(
|
|
624
|
+
description="Analyze Laminar signal events using an LLM",
|
|
625
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
626
|
+
epilog="""
|
|
627
|
+
Examples:
|
|
628
|
+
# Analyze PR review signals with built-in template
|
|
629
|
+
python analyze.py --signal "pr review suggestion and analysis"
|
|
630
|
+
|
|
631
|
+
# Analyze with skill context for grounded recommendations
|
|
632
|
+
python analyze.py --signal "pr review suggestion and analysis" \\
|
|
633
|
+
--skill-dir ../../../plugins/pr-review
|
|
634
|
+
|
|
635
|
+
# List available signals
|
|
636
|
+
python analyze.py --list-signals
|
|
637
|
+
|
|
638
|
+
# Use a custom prompt template
|
|
639
|
+
python analyze.py --signal "my-signal" --prompt-file my_prompt.j2
|
|
640
|
+
|
|
641
|
+
# Analyze last 30 days with JSON output
|
|
642
|
+
python analyze.py --signal "my-signal" --days 30 --format json
|
|
643
|
+
|
|
644
|
+
Environment Variables:
|
|
645
|
+
LMNR_PROJECT_API_KEY Laminar project API key (required)
|
|
646
|
+
LLM_API_KEY API key for the LLM (required)
|
|
647
|
+
LLM_MODEL Model to use (default: gemini-3-pro-preview)
|
|
648
|
+
LLM_BASE_URL Base URL for LLM API (default: https://llm-proxy.app.all-hands.dev)
|
|
649
|
+
""",
|
|
650
|
+
)
|
|
651
|
+
parser.add_argument(
|
|
652
|
+
"--signal",
|
|
653
|
+
help="Name of the signal to analyze",
|
|
654
|
+
)
|
|
655
|
+
parser.add_argument(
|
|
656
|
+
"--list-signals",
|
|
657
|
+
action="store_true",
|
|
658
|
+
help="List available signal names and exit",
|
|
659
|
+
)
|
|
660
|
+
parser.add_argument(
|
|
661
|
+
"--prompt-file",
|
|
662
|
+
help="Path to custom Jinja2 prompt template file",
|
|
663
|
+
)
|
|
664
|
+
parser.add_argument(
|
|
665
|
+
"--skill-dir",
|
|
666
|
+
help="Path to skill or plugin directory to ground recommendations in current prompts",
|
|
667
|
+
)
|
|
668
|
+
parser.add_argument(
|
|
669
|
+
"--days",
|
|
670
|
+
type=int,
|
|
671
|
+
default=DEFAULT_DAYS_LOOKBACK,
|
|
672
|
+
help=f"Number of days to look back (default: {DEFAULT_DAYS_LOOKBACK})",
|
|
673
|
+
)
|
|
674
|
+
parser.add_argument(
|
|
675
|
+
"--format",
|
|
676
|
+
choices=["md", "json"],
|
|
677
|
+
default="md",
|
|
678
|
+
help="Output format: 'md' for markdown (default), 'json' for raw JSON",
|
|
679
|
+
)
|
|
680
|
+
parser.add_argument(
|
|
681
|
+
"--output",
|
|
682
|
+
help="Output file path (default: stdout)",
|
|
683
|
+
)
|
|
684
|
+
args = parser.parse_args()
|
|
685
|
+
|
|
686
|
+
# Get API keys and config
|
|
687
|
+
laminar_key = get_env_var("LMNR_PROJECT_API_KEY")
|
|
688
|
+
laminar_project_id = get_env_var("LMNR_PROJECT_ID", required=False)
|
|
689
|
+
llm_key, llm_model, llm_base_url = get_llm_config()
|
|
690
|
+
|
|
691
|
+
# Handle --list-signals
|
|
692
|
+
if args.list_signals:
|
|
693
|
+
print(f"Available signals (last {args.days} days):")
|
|
694
|
+
print()
|
|
695
|
+
signals = list_available_signals(laminar_key, args.days)
|
|
696
|
+
if not signals:
|
|
697
|
+
print(" No signals found")
|
|
698
|
+
else:
|
|
699
|
+
for s in signals:
|
|
700
|
+
builtin = " [has built-in template]" if s["name"] in BUILTIN_TEMPLATES else ""
|
|
701
|
+
print(f" {s['name']}: {s['count']} events{builtin}")
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
# Require --signal if not listing
|
|
705
|
+
if not args.signal:
|
|
706
|
+
parser.error("--signal is required (or use --list-signals)")
|
|
707
|
+
|
|
708
|
+
print("=" * 60, file=sys.stderr)
|
|
709
|
+
print("Laminar Signal Analysis", file=sys.stderr)
|
|
710
|
+
print("=" * 60, file=sys.stderr)
|
|
711
|
+
print(file=sys.stderr)
|
|
712
|
+
|
|
713
|
+
# Fetch signals from Laminar
|
|
714
|
+
print(f"Signal: {args.signal}", file=sys.stderr)
|
|
715
|
+
print(f"Fetching signals from Laminar (last {args.days} days)...", file=sys.stderr)
|
|
716
|
+
raw_signals = query_laminar_signals(laminar_key, args.signal, args.days)
|
|
717
|
+
print(f"Found {len(raw_signals)} signal events", file=sys.stderr)
|
|
718
|
+
print(file=sys.stderr)
|
|
719
|
+
|
|
720
|
+
if not raw_signals:
|
|
721
|
+
print("No signals found. Exiting.", file=sys.stderr)
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
# Parse signals
|
|
725
|
+
signals = [parse_signal(s, laminar_project_id) for s in raw_signals]
|
|
726
|
+
|
|
727
|
+
# Warn if no project ID (trace URLs won't be generated)
|
|
728
|
+
if not laminar_project_id:
|
|
729
|
+
print("Warning: LMNR_PROJECT_ID not set, trace URLs will not be generated", file=sys.stderr)
|
|
730
|
+
print("Set LMNR_PROJECT_ID to enable clickable trace links", file=sys.stderr)
|
|
731
|
+
print(file=sys.stderr)
|
|
732
|
+
|
|
733
|
+
# Load prompt template
|
|
734
|
+
template_str = load_prompt_template(args.signal, args.prompt_file)
|
|
735
|
+
if args.prompt_file:
|
|
736
|
+
template_source = f"custom ({args.prompt_file})"
|
|
737
|
+
elif args.signal in BUILTIN_TEMPLATES:
|
|
738
|
+
template_source = f"built-in ({BUILTIN_TEMPLATES[args.signal]})"
|
|
739
|
+
else:
|
|
740
|
+
template_source = "default (default.j2)"
|
|
741
|
+
print(f"Using {template_source} prompt template", file=sys.stderr)
|
|
742
|
+
|
|
743
|
+
# Load skill content if provided
|
|
744
|
+
skill_content = None
|
|
745
|
+
if args.skill_dir:
|
|
746
|
+
print(f"Loading skill content from: {args.skill_dir}", file=sys.stderr)
|
|
747
|
+
try:
|
|
748
|
+
skill_content = load_skill_content(args.skill_dir)
|
|
749
|
+
print(f"Loaded {len(skill_content)} characters of skill content", file=sys.stderr)
|
|
750
|
+
except FileNotFoundError as e:
|
|
751
|
+
print(f"Warning: {e}", file=sys.stderr)
|
|
752
|
+
|
|
753
|
+
# Build prompt and query LLM
|
|
754
|
+
print("Building analysis prompt...", file=sys.stderr)
|
|
755
|
+
prompt = build_analysis_prompt(signals, args.signal, template_str, skill_content)
|
|
756
|
+
print(f"Prompt length: {len(prompt)} characters", file=sys.stderr)
|
|
757
|
+
print(file=sys.stderr)
|
|
758
|
+
|
|
759
|
+
print(f"Querying LLM ({llm_model}) for analysis...", file=sys.stderr)
|
|
760
|
+
print("This may take a minute...", file=sys.stderr)
|
|
761
|
+
print(file=sys.stderr)
|
|
762
|
+
|
|
763
|
+
analysis = query_llm(llm_key, prompt, llm_model, llm_base_url)
|
|
764
|
+
|
|
765
|
+
# Format output based on requested format
|
|
766
|
+
if args.format == "json":
|
|
767
|
+
output_text = json.dumps(analysis, indent=2)
|
|
768
|
+
else:
|
|
769
|
+
output_text = format_analysis_as_markdown(analysis, args.signal)
|
|
770
|
+
|
|
771
|
+
# Write output
|
|
772
|
+
if args.output:
|
|
773
|
+
Path(args.output).write_text(output_text)
|
|
774
|
+
print(f"Analysis written to: {args.output}", file=sys.stderr)
|
|
775
|
+
else:
|
|
776
|
+
print(output_text)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
if __name__ == "__main__":
|
|
780
|
+
main()
|