@openhands/extensions 0.28.0 → 0.30.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 +44 -1
- package/.claude-plugin/marketplace.json +7 -5
- package/.codex-plugin/marketplace.json +7 -5
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/check-extensions.yml +1 -1
- package/.github/workflows/tests.yml +1 -1
- package/.plugin/marketplace.json +7 -5
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +7 -2
- package/README.md +3 -3
- package/apps/vscode/README.md +8 -6
- package/apps/vscode/canvas-extension.json +11 -3
- package/apps/vscode/extension.js +60 -12
- package/apps/vscode/scripts/package.mjs +20 -74
- package/automations/bundle-index.js +9 -7
- package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
- package/automations/catalog/github-issue-triage/manifest.json +4 -3
- package/automations/catalog/github-pr-reviewer/manifest.json +5 -4
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
- package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
- package/integrations/catalog/atlassian-rovo.json +4 -3
- package/integrations/catalog/bitbucket.json +16 -1
- package/integrations/catalog/github.json +1 -0
- package/integrations/catalog/gitlab.json +1 -1
- package/marketplaces/openhands-extensions.json +7 -5
- package/package.json +1 -1
- package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +76 -8
- package/plugins/pr-review/README.md +1 -0
- package/plugins/pr-review/scripts/prompt.py +44 -11
- package/plugins/runtime-api-configs/SKILL.md +1 -1
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/canvas-extension-api/SKILL.md +4 -1
- package/skills/canvas-extension-api/references/backend-safety.md +3 -3
- package/skills/canvas-extension-api/references/native-design.md +250 -0
- package/skills/canvas-extension-api/references/v1-contract.md +7 -4
- package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
- package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
- package/skills/github/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/README.md +73 -14
- package/skills/github-issue-triage/SKILL.md +26 -46
- package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
- package/skills/github-issue-triage/scripts/worker.py +153 -103
- package/skills/github-pr-reviewer/README.md +103 -1
- package/skills/github-pr-reviewer/SKILL.md +35 -8
- package/skills/github-pr-reviewer/references/state-schema.md +8 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
- package/skills/github-pr-reviewer/scripts/main.py +163 -39
- package/skills/github-pr-reviewer/scripts/publication.py +193 -0
- package/skills/github-pr-reviewer/scripts/worker.py +297 -8
- package/skills/github-pr-reviewer/tests/test_main.py +73 -5
- package/skills/gitlab-issue-to-mr/README.md +10 -3
- package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
- package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
- package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
- package/skills/index.js +10 -10
- package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/README.md +18 -11
- package/skills/jira-issue-to-pr/SKILL.md +72 -23
- package/skills/jira-issue-to-pr/references/setup.md +43 -12
- package/skills/jira-issue-to-pr/scripts/main.py +136 -26
- package/skills/openhands-automation/references/custom-automation.md +76 -8
- package/skills/slack-channel-monitor/README.md +6 -1
- package/skills/slack-channel-monitor/SKILL.md +6 -3
- package/skills/slack-channel-monitor/scripts/main.py +36 -4
- package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/SKILL.md +5 -3
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +173 -6
- package/tests/test_automation_llm_provenance.py +1209 -0
- package/tests/test_github_review_publication.py +422 -0
- package/tests/test_github_reviewer_delivery.py +174 -21
- package/tests/test_github_triage_delivery.py +411 -238
- package/tests/test_gitlab_issue_to_mr.py +284 -0
- package/tests/test_jira_issue_to_pr.py +200 -6
- package/tests/test_pr_review_prompt_injection.py +353 -0
- package/tests/test_slack_channel_monitor.py +32 -1
- package/tests/test_vscode_app.py +8 -2
- package/tests/vscode-app.test.mjs +8 -2
|
@@ -2,19 +2,26 @@
|
|
|
2
2
|
Jira issue-to-PR poller — reads all configuration from config.json in the same directory.
|
|
3
3
|
|
|
4
4
|
config.json fields:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
jira_mcp_server Settings name of the connected Atlassian Rovo MCP server. When set,
|
|
6
|
+
Jira is reached through it (OpenHands Cloud and Enterprise only)
|
|
7
|
+
and no Jira credential is configured here
|
|
8
|
+
jira_cloud_id Atlassian cloud id of the Jira site; required with jira_mcp_server
|
|
9
|
+
jira_base_url e.g. "https://yourcompany.atlassian.net"; without jira_mcp_server
|
|
10
|
+
jira_email Atlassian account email used for Basic auth; without jira_mcp_server
|
|
7
11
|
jira_token_secret Name of the OpenHands secret holding the Jira API token
|
|
8
12
|
jira_label Label to watch for (default: "create-pr")
|
|
9
13
|
max_new_per_run Max conversations dispatched per run (default: 5)
|
|
14
|
+
git_provider Where the ticket's repository lives: "github" (default), "gitlab"
|
|
15
|
+
or "bitbucket". It only changes what the conversation is asked
|
|
10
16
|
|
|
11
|
-
The target
|
|
12
|
-
|
|
17
|
+
The target repository is NOT configured here. Each Jira ticket body must name it
|
|
18
|
+
(owner/repo on GitHub, group/project or a URL on GitLab, workspace/repo on Bitbucket);
|
|
19
|
+
the spawned agent extracts it from the ticket text.
|
|
13
20
|
|
|
14
21
|
Runs against a local agent server (AGENT_SERVER_URL is injected) or on OpenHands Cloud
|
|
15
22
|
(OPENHANDS_CLOUD_API_URL / OPENHANDS_API_KEY / SANDBOX_ID are injected instead).
|
|
16
23
|
"""
|
|
17
|
-
import base64, json, os, re, sys, tempfile, urllib.error, urllib.request, uuid
|
|
24
|
+
import base64, json, os, re, sys, tempfile, urllib.error, urllib.parse, urllib.request, uuid
|
|
18
25
|
from datetime import datetime, timezone
|
|
19
26
|
from pathlib import Path
|
|
20
27
|
|
|
@@ -23,11 +30,67 @@ _HERE = Path(__file__).parent
|
|
|
23
30
|
with open(_HERE / "config.json") as _f:
|
|
24
31
|
_cfg = json.load(_f)
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
# Jira is reached through this connected MCP server when it is named, and through
|
|
34
|
+
# Jira's REST API with an API token otherwise.
|
|
35
|
+
JIRA_MCP_SERVER = _cfg.get("jira_mcp_server")
|
|
36
|
+
JIRA_CLOUD_ID = _cfg["jira_cloud_id"] if JIRA_MCP_SERVER else None
|
|
37
|
+
JIRA_BASE_URL = None if JIRA_MCP_SERVER else _cfg["jira_base_url"].rstrip("/")
|
|
38
|
+
JIRA_EMAIL = None if JIRA_MCP_SERVER else _cfg["jira_email"]
|
|
28
39
|
JIRA_TOKEN_SECRET = _cfg.get("jira_token_secret", "JIRA_CLOUD_KEY")
|
|
29
40
|
JIRA_LABEL = _cfg.get("jira_label", "create-pr")
|
|
41
|
+
# Atlassian renamed the comment tool in v2 of its MCP server and moves v1 connections
|
|
42
|
+
# to the v2 tools on its own schedule, so a deployment can meet either name.
|
|
43
|
+
JIRA_COMMENT_TOOLS = ("addOrEditJiraIssueComment", "addCommentToJiraIssue")
|
|
30
44
|
MAX_NEW_PER_RUN = int(_cfg.get("max_new_per_run", 5))
|
|
45
|
+
GIT_PROVIDER = _cfg.get("git_provider", "github")
|
|
46
|
+
# What the conversation is told about the provider the ticket names its repository on.
|
|
47
|
+
# Only the prompt differs: the conversation pushes with the user's connected integration,
|
|
48
|
+
# and without one it delivers through the tools of the connected MCP server instead.
|
|
49
|
+
GIT_PROVIDERS = {
|
|
50
|
+
"github": {
|
|
51
|
+
"name": "GitHub",
|
|
52
|
+
"request": "Pull Request",
|
|
53
|
+
"short": "PR",
|
|
54
|
+
"reference": '"owner/repo" format (e.g. "acme-org/backend") or a full GitHub URL\n'
|
|
55
|
+
' (e.g. "https://github.com/acme-org/backend")',
|
|
56
|
+
"clone": "https://github.com/<owner>/<repo>",
|
|
57
|
+
"delivery": "",
|
|
58
|
+
},
|
|
59
|
+
"gitlab": {
|
|
60
|
+
"name": "GitLab",
|
|
61
|
+
"request": "Merge Request",
|
|
62
|
+
"short": "MR",
|
|
63
|
+
"reference": '"group/project" format, subgroups included (e.g. "acme-org/platform/backend"),\n'
|
|
64
|
+
' or a full GitLab URL, which may be on a self-managed host\n'
|
|
65
|
+
' (e.g. "https://gitlab.example.com/acme-org/platform/backend")',
|
|
66
|
+
"clone": "https://gitlab.com/<group>/<project>, or the host the ticket's URL names",
|
|
67
|
+
"delivery": """
|
|
68
|
+
Delivering through GitLab:
|
|
69
|
+
- If GitLab tools from a connected MCP server are available to you, prefer them for reading
|
|
70
|
+
the issue and for opening the merge request.
|
|
71
|
+
- Without a `GITLAB_TOKEN` you cannot push with git: create the branch with `add_branch`,
|
|
72
|
+
commit your changes with `add_commit` (one call, a file action per changed file), and open
|
|
73
|
+
the merge request with `save_merge_request`. A private project cannot be cloned without a
|
|
74
|
+
credential; read it with `list_repository_tree` and `get_repository_file` instead.
|
|
75
|
+
""",
|
|
76
|
+
},
|
|
77
|
+
"bitbucket": {
|
|
78
|
+
"name": "Bitbucket",
|
|
79
|
+
"request": "Pull Request",
|
|
80
|
+
"short": "PR",
|
|
81
|
+
"reference": '"workspace/repo" format (e.g. "acme-org/backend") or a full Bitbucket URL\n'
|
|
82
|
+
' (e.g. "https://bitbucket.org/acme-org/backend")',
|
|
83
|
+
"clone": "https://bitbucket.org/<workspace>/<repo>",
|
|
84
|
+
"delivery": """
|
|
85
|
+
Delivering through Bitbucket:
|
|
86
|
+
- Without a `BITBUCKET_TOKEN`, use the Bitbucket tools of the connected Atlassian Rovo MCP
|
|
87
|
+
server: read the repository with `getBitbucketRepoFileContent`, create the branch with
|
|
88
|
+
`createBitbucketRepoBranch`, commit your changes with `createBitbucketRepoCommit`, and open
|
|
89
|
+
the pull request with `createBitbucketRepoPullRequest`. Tools the server does not list
|
|
90
|
+
directly are reached through `discover` and `executeRead` / `executeWrite`.
|
|
91
|
+
""",
|
|
92
|
+
},
|
|
93
|
+
}
|
|
31
94
|
# How often a conversation that OpenHands Cloud failed to start is started again.
|
|
32
95
|
MAX_START_ATTEMPTS = 3
|
|
33
96
|
|
|
@@ -163,14 +226,39 @@ def extract_adf_text(adf):
|
|
|
163
226
|
return " ".join(p for p in parts if p).strip()
|
|
164
227
|
|
|
165
228
|
|
|
229
|
+
def call_mcp_tool(name, arguments):
|
|
230
|
+
"""Call a tool on the connected MCP server and return its text output.
|
|
231
|
+
|
|
232
|
+
OpenHands makes the call with the credentials the user stored when connecting
|
|
233
|
+
the server, so this run holds no Jira credential of its own.
|
|
234
|
+
"""
|
|
235
|
+
server = urllib.parse.quote(JIRA_MCP_SERVER, safe="")
|
|
236
|
+
result = _cloud_request("POST", f"/api/v1/mcp/servers/{server}/call-tool", {
|
|
237
|
+
"name": name, "arguments": arguments,
|
|
238
|
+
})
|
|
239
|
+
if result.get("is_error"):
|
|
240
|
+
raise RuntimeError(f"{name} failed: {str(result.get('text'))[:500]}")
|
|
241
|
+
return result.get("text") or ""
|
|
242
|
+
|
|
243
|
+
|
|
166
244
|
def fetch_labeled_issues(auth_header):
|
|
167
|
-
"""Return open Jira issues with JIRA_LABEL
|
|
168
|
-
|
|
169
|
-
|
|
245
|
+
"""Return open Jira issues with JIRA_LABEL.
|
|
246
|
+
|
|
247
|
+
Uses the connected MCP server's JQL search, or the current v3 search endpoint.
|
|
248
|
+
"""
|
|
249
|
+
search = {
|
|
170
250
|
"jql": f'labels = "{JIRA_LABEL}" AND statusCategory != Done',
|
|
171
251
|
"fields": ["key", "summary", "description", "status", "updated"],
|
|
172
252
|
"maxResults": 50,
|
|
173
|
-
}
|
|
253
|
+
}
|
|
254
|
+
if JIRA_MCP_SERVER:
|
|
255
|
+
text = call_mcp_tool("searchJiraIssuesUsingJql", {"cloudId": JIRA_CLOUD_ID, **search})
|
|
256
|
+
try:
|
|
257
|
+
return json.loads(text).get("issues", [])
|
|
258
|
+
except (ValueError, AttributeError) as exc:
|
|
259
|
+
raise RuntimeError(f"Unexpected Jira search result: {text[:500]}") from exc
|
|
260
|
+
url = f"{JIRA_BASE_URL}/rest/api/3/search/jql"
|
|
261
|
+
body = json.dumps(search).encode()
|
|
174
262
|
req = urllib.request.Request(
|
|
175
263
|
url, data=body,
|
|
176
264
|
headers={
|
|
@@ -188,7 +276,19 @@ def fetch_labeled_issues(auth_header):
|
|
|
188
276
|
|
|
189
277
|
|
|
190
278
|
def post_jira_comment(issue_key, auth_header, text):
|
|
191
|
-
"""Post a plain-text comment on a Jira issue
|
|
279
|
+
"""Post a plain-text comment on a Jira issue."""
|
|
280
|
+
if JIRA_MCP_SERVER:
|
|
281
|
+
errors = []
|
|
282
|
+
for tool in JIRA_COMMENT_TOOLS:
|
|
283
|
+
try:
|
|
284
|
+
call_mcp_tool(tool, {
|
|
285
|
+
"cloudId": JIRA_CLOUD_ID, "issueIdOrKey": issue_key, "commentBody": text,
|
|
286
|
+
})
|
|
287
|
+
return
|
|
288
|
+
except RuntimeError as exc:
|
|
289
|
+
errors.append(str(exc))
|
|
290
|
+
print(f"Warning: failed to post Jira comment on {issue_key}: {'; '.join(errors)}")
|
|
291
|
+
return
|
|
192
292
|
url = f"{JIRA_BASE_URL}/rest/api/3/issue/{issue_key}/comment"
|
|
193
293
|
body = json.dumps({
|
|
194
294
|
"body": {
|
|
@@ -321,10 +421,21 @@ try:
|
|
|
321
421
|
raise RuntimeError("The automation KV store is required on OpenHands Cloud, "
|
|
322
422
|
"but AUTOMATION_KV_TOKEN is not set for this run.")
|
|
323
423
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
424
|
+
if GIT_PROVIDER not in GIT_PROVIDERS:
|
|
425
|
+
raise RuntimeError(f"Unknown git_provider {GIT_PROVIDER!r}; "
|
|
426
|
+
f"expected one of: {', '.join(GIT_PROVIDERS)}")
|
|
427
|
+
git = GIT_PROVIDERS[GIT_PROVIDER]
|
|
428
|
+
|
|
429
|
+
auth_header = None
|
|
430
|
+
if not JIRA_MCP_SERVER:
|
|
431
|
+
jira_token = get_secret(JIRA_TOKEN_SECRET)
|
|
432
|
+
auth_header = "Basic " + base64.b64encode(
|
|
433
|
+
f"{JIRA_EMAIL}:{jira_token}".encode()
|
|
434
|
+
).decode()
|
|
435
|
+
elif IS_LOCAL:
|
|
436
|
+
# Only OpenHands Cloud and Enterprise call a connected MCP server for a run.
|
|
437
|
+
raise RuntimeError("jira_mcp_server needs OpenHands Cloud or Enterprise; "
|
|
438
|
+
"a local run reaches Jira with jira_token_secret.")
|
|
328
439
|
|
|
329
440
|
state = load_state()
|
|
330
441
|
processed_keys = set(state.get("processed_keys", []))
|
|
@@ -343,7 +454,7 @@ try:
|
|
|
343
454
|
if not IS_LOCAL:
|
|
344
455
|
settle_cloud_starts(state, processed_keys, auth_header)
|
|
345
456
|
|
|
346
|
-
print(f"Polling {JIRA_BASE_URL} for issues labeled '{JIRA_LABEL}'…")
|
|
457
|
+
print(f"Polling {JIRA_BASE_URL or JIRA_MCP_SERVER} for issues labeled '{JIRA_LABEL}'…")
|
|
347
458
|
issues = fetch_labeled_issues(auth_header)
|
|
348
459
|
print(f"Total matching issues : {len(issues)}")
|
|
349
460
|
|
|
@@ -393,28 +504,27 @@ try:
|
|
|
393
504
|
description = extract_adf_text(issue["fields"].get("description"))
|
|
394
505
|
branch = f"jira/{key.lower()}"
|
|
395
506
|
|
|
396
|
-
prompt = f"""Create a
|
|
507
|
+
prompt = f"""Create a {git['name']} {git['request']} for the following Jira issue.
|
|
397
508
|
|
|
398
509
|
Jira Issue : {key}
|
|
399
510
|
Summary : {summary}
|
|
400
511
|
Description: {description or "No description provided."}
|
|
401
512
|
|
|
402
513
|
Steps:
|
|
403
|
-
1. Find the target
|
|
404
|
-
|
|
405
|
-
(e.g. "https://github.com/acme-org/backend"). Use that repository.
|
|
514
|
+
1. Find the target {git['name']} repository in the Description above. Look for a reference in
|
|
515
|
+
{git['reference']}. Use that repository.
|
|
406
516
|
If no repository is mentioned, create a file `jira/{key}/notes.md` with the issue
|
|
407
|
-
details and print a message explaining that no
|
|
408
|
-
2. Clone the repository (e.g.
|
|
517
|
+
details and print a message explaining that no {git['name']} repo was found in the ticket.
|
|
518
|
+
2. Clone the repository (e.g. {git['clone']}).
|
|
409
519
|
3. Create branch `{branch}` from the default branch.
|
|
410
520
|
4. Implement the changes described in the issue.
|
|
411
521
|
If the description is vague or missing, create `jira/{key}/notes.md`
|
|
412
522
|
with the issue key, summary, and description as a placeholder.
|
|
413
|
-
5. Commit, push the branch, and open a
|
|
523
|
+
5. Commit, push the branch, and open a {git['request']}:
|
|
414
524
|
- Title : [{key}] {summary}
|
|
415
525
|
- Body : Reference the Jira issue key and describe the changes made.
|
|
416
|
-
6. Print the
|
|
417
|
-
"""
|
|
526
|
+
6. Print the {git['short']} URL when done.
|
|
527
|
+
{git['delivery']}"""
|
|
418
528
|
if IS_LOCAL:
|
|
419
529
|
workdir = tempfile.mkdtemp(prefix=f"jira-{key.lower()}-")
|
|
420
530
|
payload = {
|
|
@@ -249,6 +249,7 @@ uv pip install -q openhands-sdk openhands-workspace openhands-tools
|
|
|
249
249
|
import os
|
|
250
250
|
|
|
251
251
|
from openhands.sdk import Conversation
|
|
252
|
+
from openhands.sdk.automation import automation_conversation_kwargs
|
|
252
253
|
from openhands.tools.preset.default import get_default_agent
|
|
253
254
|
from openhands.workspace import OpenHandsCloudWorkspace
|
|
254
255
|
|
|
@@ -266,13 +267,48 @@ with OpenHandsCloudWorkspace(
|
|
|
266
267
|
) as workspace:
|
|
267
268
|
llm = workspace.get_llm()
|
|
268
269
|
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
269
|
-
conversation = Conversation(
|
|
270
|
+
conversation = Conversation(
|
|
271
|
+
agent=agent,
|
|
272
|
+
workspace=workspace,
|
|
273
|
+
**automation_conversation_kwargs(),
|
|
274
|
+
)
|
|
270
275
|
conversation.send_message("Your automation prompt here")
|
|
271
276
|
conversation.run()
|
|
272
277
|
conversation.close()
|
|
273
278
|
# OpenHandsCloudWorkspace.__exit__ fires the completion callback here.
|
|
274
279
|
```
|
|
275
280
|
|
|
281
|
+
### Link automation conversations to run telemetry
|
|
282
|
+
|
|
283
|
+
When an automation script starts a conversation, preserve the trace context that the automation service injected into the run. This links the conversation span to the triggering webhook, scheduler dispatch, and run record.
|
|
284
|
+
|
|
285
|
+
- SDK conversations: pass `**automation_conversation_kwargs()` into every `Conversation(...)` call.
|
|
286
|
+
- Direct agent-server API calls: merge `automation_observability_headers()` into the headers for each `POST /api/conversations` request.
|
|
287
|
+
- If you add your own conversation tags, pass them through `conversation_tags={...}` instead of replacing the helper output.
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from openhands.sdk import Conversation
|
|
291
|
+
from openhands.sdk.automation import (
|
|
292
|
+
automation_conversation_kwargs,
|
|
293
|
+
automation_observability_headers,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
conversation = Conversation(
|
|
297
|
+
agent=agent,
|
|
298
|
+
workspace=workspace,
|
|
299
|
+
**automation_conversation_kwargs(),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
headers = {
|
|
303
|
+
"X-Session-API-Key": api_key,
|
|
304
|
+
**automation_observability_headers(span_name="automation.conversation"),
|
|
305
|
+
}
|
|
306
|
+
# Use these headers when creating a backend conversation directly.
|
|
307
|
+
# httpx.post(f"{agent_url}/api/conversations", headers=headers, json=payload)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The built-in prompt and plugin presets use this helper. Custom scripts should do the same for every new conversation so traces, observability metadata, and automation conversation tags stay attached.
|
|
311
|
+
|
|
276
312
|
### Selecting an LLM profile
|
|
277
313
|
|
|
278
314
|
The automation service injects an `AUTOMATION_MODEL` environment variable holding the **LLM profile name** the automation should run with. This is the profile a user selects when editing the automation, or — when none is selected — a snapshot of the user's active profile taken when the automation was created. To honor it on every run, read `AUTOMATION_MODEL` and pass it to `workspace.get_llm(profile_name=...)`:
|
|
@@ -299,13 +335,27 @@ Conversations started during a run remain accessible in the OpenHands UI after t
|
|
|
299
335
|
|
|
300
336
|
```python
|
|
301
337
|
# Default: conversation persists after close (users can view/continue it)
|
|
302
|
-
conversation = Conversation(
|
|
338
|
+
conversation = Conversation(
|
|
339
|
+
agent=agent,
|
|
340
|
+
workspace=workspace,
|
|
341
|
+
**automation_conversation_kwargs(),
|
|
342
|
+
)
|
|
303
343
|
|
|
304
344
|
# Explicitly persist (same as default)
|
|
305
|
-
conversation = Conversation(
|
|
345
|
+
conversation = Conversation(
|
|
346
|
+
agent=agent,
|
|
347
|
+
workspace=workspace,
|
|
348
|
+
delete_on_close=False,
|
|
349
|
+
**automation_conversation_kwargs(),
|
|
350
|
+
)
|
|
306
351
|
|
|
307
352
|
# Delete conversation resources on close
|
|
308
|
-
conversation = Conversation(
|
|
353
|
+
conversation = Conversation(
|
|
354
|
+
agent=agent,
|
|
355
|
+
workspace=workspace,
|
|
356
|
+
delete_on_close=True,
|
|
357
|
+
**automation_conversation_kwargs(),
|
|
358
|
+
)
|
|
309
359
|
```
|
|
310
360
|
|
|
311
361
|
The agent server itself persists until it times out or is manually deleted; this is managed by the automation service, not by the workspace.
|
|
@@ -339,7 +389,11 @@ else:
|
|
|
339
389
|
with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
|
|
340
390
|
llm = workspace.get_llm()
|
|
341
391
|
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
342
|
-
conversation = Conversation(
|
|
392
|
+
conversation = Conversation(
|
|
393
|
+
agent=agent,
|
|
394
|
+
workspace=workspace,
|
|
395
|
+
**automation_conversation_kwargs(),
|
|
396
|
+
)
|
|
343
397
|
conversation.send_message(f"Investigate these alerts and open GitHub issues: {alerts}")
|
|
344
398
|
conversation.run()
|
|
345
399
|
conversation.close()
|
|
@@ -358,7 +412,11 @@ from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
|
358
412
|
with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
|
|
359
413
|
llm = workspace.get_llm()
|
|
360
414
|
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
361
|
-
conversation = Conversation(
|
|
415
|
+
conversation = Conversation(
|
|
416
|
+
agent=agent,
|
|
417
|
+
workspace=workspace,
|
|
418
|
+
**automation_conversation_kwargs(),
|
|
419
|
+
)
|
|
362
420
|
conversation.send_message("Run a long analysis task")
|
|
363
421
|
# Conversation is now running asynchronously in the agent server.
|
|
364
422
|
|
|
@@ -404,7 +462,12 @@ stop_hook = HookConfig(
|
|
|
404
462
|
with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
|
|
405
463
|
llm = workspace.get_llm()
|
|
406
464
|
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
407
|
-
conversation = Conversation(
|
|
465
|
+
conversation = Conversation(
|
|
466
|
+
agent=agent,
|
|
467
|
+
workspace=workspace,
|
|
468
|
+
hook_config=stop_hook,
|
|
469
|
+
**automation_conversation_kwargs(),
|
|
470
|
+
)
|
|
408
471
|
conversation.send_message("Do some long-running work")
|
|
409
472
|
# Don't call run() — the conversation runs asynchronously.
|
|
410
473
|
# When the agent stops, the stop hook will fire the callback.
|
|
@@ -765,6 +828,7 @@ import os
|
|
|
765
828
|
import json
|
|
766
829
|
|
|
767
830
|
from openhands.sdk import Conversation
|
|
831
|
+
from openhands.sdk.automation import automation_conversation_kwargs
|
|
768
832
|
from openhands.tools.preset.default import get_default_agent
|
|
769
833
|
from openhands.workspace import OpenHandsCloudWorkspace
|
|
770
834
|
|
|
@@ -781,7 +845,11 @@ with OpenHandsCloudWorkspace(
|
|
|
781
845
|
) as workspace:
|
|
782
846
|
llm = workspace.get_llm()
|
|
783
847
|
agent = get_default_agent(llm=llm, cli_mode=True)
|
|
784
|
-
conversation = Conversation(
|
|
848
|
+
conversation = Conversation(
|
|
849
|
+
agent=agent,
|
|
850
|
+
workspace=workspace,
|
|
851
|
+
**automation_conversation_kwargs(),
|
|
852
|
+
)
|
|
785
853
|
conversation.send_message("Generate a weekly status report")
|
|
786
854
|
conversation.run()
|
|
787
855
|
conversation.close()
|
|
@@ -37,12 +37,17 @@ This skill is activated by keywords:
|
|
|
37
37
|
thread immediately on trigger detection
|
|
38
38
|
- **Automatic summaries**: when the conversation reaches a terminal state the
|
|
39
39
|
agent's final response is posted back to the thread; error/stuck states
|
|
40
|
-
receive a clear error notice
|
|
40
|
+
receive a clear error notice, and every result shows the LLM settings source
|
|
41
|
+
and concrete model
|
|
41
42
|
- **Persistent state**: conversation tracking and poll timestamps are stored
|
|
42
43
|
in `automation-state/slack_poller_{automation_id}.json` across runs
|
|
43
44
|
|
|
44
45
|
## Prerequisites
|
|
45
46
|
|
|
47
|
+
The footer reports `default` and the concrete model from the server settings
|
|
48
|
+
already used to start the conversation. Follow-up replies keep those recorded
|
|
49
|
+
values because they reuse the same conversation.
|
|
50
|
+
|
|
46
51
|
Set at least one of the following in **OpenHands Settings - Secrets**:
|
|
47
52
|
|
|
48
53
|
| Secret | Token type | Minimum scopes |
|
|
@@ -251,14 +251,17 @@ Each cron run executes `main.py`, which runs **10 polling iterations** (every
|
|
|
251
251
|
ignored.
|
|
252
252
|
- Contains trigger phrase outside a tracked conversation → 👀 reaction, create
|
|
253
253
|
a new conversation, post link.
|
|
254
|
+
- Records `default` and the concrete model from the server settings already
|
|
255
|
+
used to start the conversation, for the response footer.
|
|
254
256
|
- Thread replies: agent receives full thread history for context.
|
|
255
257
|
- Root messages: agent receives the trigger text only.
|
|
256
258
|
6. **Checks conversation statuses** - for each active conversation where
|
|
257
259
|
`time.time() - last_activity > 15 s`:
|
|
258
260
|
- If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's
|
|
259
|
-
final response via `/api/conversations/{id}/agent_final_response
|
|
260
|
-
it to the Slack thread using Slack's
|
|
261
|
-
formatting renders correctly. Mark the
|
|
261
|
+
final response via `/api/conversations/{id}/agent_final_response`, append
|
|
262
|
+
the LLM profile and model, and post it to the Slack thread using Slack's
|
|
263
|
+
`markdown_text` field so Markdown formatting renders correctly. Mark the
|
|
264
|
+
record `watching` for five minutes
|
|
262
265
|
so triggered follow-up replies can continue the same conversation.
|
|
263
266
|
7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary
|
|
264
267
|
races). If a conversation creation failed, pins `last_poll` further back to
|
|
@@ -33,6 +33,7 @@ Optional secret:
|
|
|
33
33
|
|
|
34
34
|
import json
|
|
35
35
|
import os
|
|
36
|
+
import re
|
|
36
37
|
import sys
|
|
37
38
|
import time
|
|
38
39
|
import urllib.error
|
|
@@ -577,7 +578,12 @@ def _build_secrets_payload(agent_url: str, api_key: str) -> dict:
|
|
|
577
578
|
return secrets
|
|
578
579
|
|
|
579
580
|
|
|
580
|
-
def create_conversation(
|
|
581
|
+
def create_conversation(
|
|
582
|
+
agent_url: str,
|
|
583
|
+
api_key: str,
|
|
584
|
+
initial_message: str,
|
|
585
|
+
agent: dict | None = None,
|
|
586
|
+
) -> str:
|
|
581
587
|
"""Create a conversation and return its ID.
|
|
582
588
|
|
|
583
589
|
The server auto-starts the agent when initial_message is provided
|
|
@@ -597,10 +603,9 @@ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> s
|
|
|
597
603
|
workspace_dir = os.path.join(root, "slack-monitor-conversations")
|
|
598
604
|
os.makedirs(workspace_dir, exist_ok=True)
|
|
599
605
|
|
|
600
|
-
agent = _get_agent_dict(agent_url, api_key)
|
|
601
606
|
payload: dict = {
|
|
602
607
|
"workspace": {"working_dir": workspace_dir},
|
|
603
|
-
"agent": agent,
|
|
608
|
+
"agent": agent or _get_agent_dict(agent_url, api_key),
|
|
604
609
|
"initial_message": {"content": [{"text": initial_message}]},
|
|
605
610
|
}
|
|
606
611
|
|
|
@@ -639,6 +644,20 @@ def conversation_final_response(agent_url: str, api_key: str, conv_id: str) -> s
|
|
|
639
644
|
return result.get("response", "")
|
|
640
645
|
|
|
641
646
|
|
|
647
|
+
def _llm_provenance(profile: str, model: str) -> str:
|
|
648
|
+
return f"LLM profile: `{profile}` · Model: `{model}`"
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _with_llm_provenance(body: str, profile: str, model: str) -> str:
|
|
652
|
+
provenance = _llm_provenance(profile, model)
|
|
653
|
+
body = "\n".join(
|
|
654
|
+
line
|
|
655
|
+
for line in (body or "").splitlines()
|
|
656
|
+
if not re.fullmatch(r"LLM profile: .* · Model: .*", line.strip())
|
|
657
|
+
).strip()
|
|
658
|
+
return f"{body}\n\n{provenance}" if body else provenance
|
|
659
|
+
|
|
660
|
+
|
|
642
661
|
# ── Message filtering ──────────────────────────────────────────────────────────
|
|
643
662
|
|
|
644
663
|
def _is_human_message(msg: dict, bot_user_id: str, bot_message_ts: list[str]) -> bool:
|
|
@@ -937,7 +956,10 @@ def _process_trigger_message(
|
|
|
937
956
|
)
|
|
938
957
|
|
|
939
958
|
try:
|
|
940
|
-
|
|
959
|
+
agent = _get_agent_dict(agent_url, api_key)
|
|
960
|
+
conv_id = create_conversation(
|
|
961
|
+
agent_url, api_key, initial_prompt, agent=agent
|
|
962
|
+
)
|
|
941
963
|
conv_url = f"{openhands_url}/conversations/{conv_id}"
|
|
942
964
|
|
|
943
965
|
now = time.time()
|
|
@@ -946,6 +968,8 @@ def _process_trigger_message(
|
|
|
946
968
|
"channel_id": channel_id,
|
|
947
969
|
"thread_ts": thread_root,
|
|
948
970
|
"status": "active",
|
|
971
|
+
"llm_profile": "default",
|
|
972
|
+
"llm_model": agent["llm"].get("model") or "unknown",
|
|
949
973
|
"last_activity": now,
|
|
950
974
|
"last_seen_reply_ts": msg_ts,
|
|
951
975
|
"reply_poll_backoff_seconds": THREAD_REPLY_INITIAL_BACKOFF_SECONDS,
|
|
@@ -1003,6 +1027,14 @@ def _check_conversation_completion(
|
|
|
1003
1027
|
)
|
|
1004
1028
|
else:
|
|
1005
1029
|
summary = final if final else "Success (no message available)."
|
|
1030
|
+
# The values recorded when this script started the conversation; a
|
|
1031
|
+
# thread follow-up reuses it, so they still describe its LLM. A record
|
|
1032
|
+
# written before provenance was tracked cannot name either value.
|
|
1033
|
+
summary = _with_llm_provenance(
|
|
1034
|
+
summary,
|
|
1035
|
+
rec.get("llm_profile") or "unknown",
|
|
1036
|
+
rec.get("llm_model") or "unknown",
|
|
1037
|
+
)
|
|
1006
1038
|
|
|
1007
1039
|
ts_back = post_message(slack_token, channel_id, summary, thread_ts=thread_ts)
|
|
1008
1040
|
if ts_back:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-code-change",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Create pull or merge requests from
|
|
4
|
+
"description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "OpenHands",
|
|
7
7
|
"email": "contact@all-hands.dev"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"repository": "https://github.com/OpenHands/extensions",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
-
"jira",
|
|
14
13
|
"linear",
|
|
15
14
|
"github",
|
|
16
15
|
"gitlab",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-code-change",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Create pull or merge requests from
|
|
4
|
+
"description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "OpenHands",
|
|
7
7
|
"email": "contact@all-hands.dev"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"repository": "https://github.com/OpenHands/extensions",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
-
"jira",
|
|
14
13
|
"linear",
|
|
15
14
|
"github",
|
|
16
15
|
"gitlab",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-code-change",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Create pull or merge requests from
|
|
4
|
+
"description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "OpenHands",
|
|
7
7
|
"email": "contact@all-hands.dev"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"repository": "https://github.com/OpenHands/extensions",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
-
"jira",
|
|
14
13
|
"linear",
|
|
15
14
|
"github",
|
|
16
15
|
"gitlab",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ticket-to-code-change
|
|
3
3
|
description: >
|
|
4
|
-
Set up a ticket-to-code-change automation using
|
|
5
|
-
|
|
4
|
+
Set up a ticket-to-code-change automation using Linear as the issue tracker
|
|
5
|
+
and GitHub, GitLab, or Bitbucket as the source-control provider. The Jira
|
|
6
|
+
templates are served by the jira-issue-to-pr skill instead.
|
|
6
7
|
Watches for implementation-ready tickets, starts an OpenHands conversation
|
|
7
8
|
to implement and test the request, opens a pull or merge request, and links
|
|
8
9
|
the result back to the ticket.
|
|
@@ -19,7 +20,8 @@ requests or merge requests.
|
|
|
19
20
|
|
|
20
21
|
Ask the user for:
|
|
21
22
|
|
|
22
|
-
1. The issue tracker: Jira
|
|
23
|
+
1. The issue tracker: Linear. Jira automations are set up with the
|
|
24
|
+
`jira-issue-to-pr` skill instead, which ships a poller.
|
|
23
25
|
2. The source-control provider: GitHub, GitLab, or Bitbucket Cloud.
|
|
24
26
|
3. The project or team to watch and the label or workflow state that means
|
|
25
27
|
"implementation ready".
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# auto-generated by sync_extensions.py
|
|
3
|
-
description: Set up a ticket-to-code-change automation using
|
|
3
|
+
description: Set up a ticket-to-code-change automation using Linear as the issue tracker and GitHub, GitLab, or Bitbucket as the source-control provider. The Jira templates are served by the jira-issue-to-pr skill instead. Watches for implementation-ready tickets, starts an OpenHands conversation to implement and test the request, opens a pull or merge request, and links the result back to the ticket.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
|