ultralytics-actions 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
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.
Potentially problematic release.
This version of ultralytics-actions might be problematic. Click here for more details.
- actions/__init__.py +1 -1
- actions/dispatch_actions.py +3 -3
- actions/first_interaction.py +60 -221
- actions/review_pr.py +140 -64
- actions/summarize_pr.py +26 -128
- actions/summarize_release.py +13 -2
- actions/update_markdown_code_blocks.py +0 -1
- actions/utils/__init__.py +11 -1
- actions/utils/github_utils.py +249 -1
- actions/utils/openai_utils.py +139 -13
- {ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/METADATA +3 -1
- ultralytics_actions-0.1.1.dist-info/RECORD +19 -0
- ultralytics_actions-0.1.0.dist-info/RECORD +0 -19
- {ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/WHEEL +0 -0
- {ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/entry_points.txt +0 -0
- {ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/top_level.txt +0 -0
actions/utils/github_utils.py
CHANGED
|
@@ -13,6 +13,91 @@ from actions import __version__
|
|
|
13
13
|
GITHUB_API_URL = "https://api.github.com"
|
|
14
14
|
GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
|
|
15
15
|
|
|
16
|
+
# GraphQL Queries
|
|
17
|
+
GRAPHQL_REPO_LABELS = """
|
|
18
|
+
query($owner: String!, $name: String!) {
|
|
19
|
+
repository(owner: $owner, name: $name) {
|
|
20
|
+
labels(first: 100, query: "") {
|
|
21
|
+
nodes {
|
|
22
|
+
id
|
|
23
|
+
name
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
GRAPHQL_PR_CONTRIBUTORS = """
|
|
31
|
+
query($owner: String!, $repo: String!, $pr_number: Int!) {
|
|
32
|
+
repository(owner: $owner, name: $repo) {
|
|
33
|
+
pullRequest(number: $pr_number) {
|
|
34
|
+
closingIssuesReferences(first: 50) { nodes { number } }
|
|
35
|
+
url
|
|
36
|
+
title
|
|
37
|
+
body
|
|
38
|
+
author { login, __typename }
|
|
39
|
+
reviews(first: 50) { nodes { author { login, __typename } } }
|
|
40
|
+
comments(first: 50) { nodes { author { login, __typename } } }
|
|
41
|
+
commits(first: 100) { nodes { commit { author { user { login } }, committer { user { login } } } } }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
GRAPHQL_UPDATE_DISCUSSION = """
|
|
48
|
+
mutation($discussionId: ID!, $title: String!, $body: String!) {
|
|
49
|
+
updateDiscussion(input: {discussionId: $discussionId, title: $title, body: $body}) {
|
|
50
|
+
discussion {
|
|
51
|
+
id
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
GRAPHQL_CLOSE_DISCUSSION = """
|
|
58
|
+
mutation($discussionId: ID!) {
|
|
59
|
+
closeDiscussion(input: {discussionId: $discussionId}) {
|
|
60
|
+
discussion {
|
|
61
|
+
id
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
GRAPHQL_LOCK_DISCUSSION = """
|
|
68
|
+
mutation($lockableId: ID!, $lockReason: LockReason) {
|
|
69
|
+
lockLockable(input: {lockableId: $lockableId, lockReason: $lockReason}) {
|
|
70
|
+
lockedRecord {
|
|
71
|
+
... on Discussion {
|
|
72
|
+
id
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
GRAPHQL_ADD_DISCUSSION_COMMENT = """
|
|
80
|
+
mutation($discussionId: ID!, $body: String!) {
|
|
81
|
+
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
|
|
82
|
+
comment {
|
|
83
|
+
id
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
GRAPHQL_ADD_LABELS_TO_DISCUSSION = """
|
|
90
|
+
mutation($labelableId: ID!, $labelIds: [ID!]!) {
|
|
91
|
+
addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) {
|
|
92
|
+
labelable {
|
|
93
|
+
... on Discussion {
|
|
94
|
+
id
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
|
|
16
101
|
|
|
17
102
|
class Action:
|
|
18
103
|
"""Handles GitHub Actions API interactions and event processing."""
|
|
@@ -110,7 +195,7 @@ class Action:
|
|
|
110
195
|
url = f"{GITHUB_API_URL}/repos/{self.repository}/pulls/{self.pr.get('number')}"
|
|
111
196
|
response = self.get(url, headers=self.headers_diff)
|
|
112
197
|
if response.status_code == 200:
|
|
113
|
-
self._pr_diff_cache = response.text
|
|
198
|
+
self._pr_diff_cache = response.text or "ERROR: EMPTY DIFF, NO CODE CHANGES IN THIS PR."
|
|
114
199
|
elif response.status_code == 406:
|
|
115
200
|
self._pr_diff_cache = "ERROR: PR diff exceeds GitHub's 20,000 line limit, unable to retrieve diff."
|
|
116
201
|
else:
|
|
@@ -148,6 +233,169 @@ class Action:
|
|
|
148
233
|
print(result.get("errors"))
|
|
149
234
|
return result
|
|
150
235
|
|
|
236
|
+
def update_pr_description(self, number: int, new_summary: str, max_retries: int = 2):
|
|
237
|
+
"""Updates PR description with summary, retrying if description is None."""
|
|
238
|
+
import time
|
|
239
|
+
|
|
240
|
+
url = f"{GITHUB_API_URL}/repos/{self.repository}/pulls/{number}"
|
|
241
|
+
description = ""
|
|
242
|
+
for i in range(max_retries + 1):
|
|
243
|
+
description = self.get(url).json().get("body") or ""
|
|
244
|
+
if description:
|
|
245
|
+
break
|
|
246
|
+
if i < max_retries:
|
|
247
|
+
print("No current PR description found, retrying...")
|
|
248
|
+
time.sleep(1)
|
|
249
|
+
|
|
250
|
+
start = "## 🛠️ PR Summary"
|
|
251
|
+
if start in description:
|
|
252
|
+
print("Existing PR Summary found, replacing.")
|
|
253
|
+
updated_description = description.split(start)[0] + new_summary
|
|
254
|
+
else:
|
|
255
|
+
print("PR Summary not found, appending.")
|
|
256
|
+
updated_description = description + "\n\n" + new_summary
|
|
257
|
+
|
|
258
|
+
self.patch(url, json={"body": updated_description})
|
|
259
|
+
|
|
260
|
+
def get_label_ids(self, labels: list[str]) -> list[str]:
|
|
261
|
+
"""Retrieves GitHub label IDs for a list of label names using the GraphQL API."""
|
|
262
|
+
owner, repo = self.repository.split("/")
|
|
263
|
+
result = self.graphql_request(GRAPHQL_REPO_LABELS, variables={"owner": owner, "name": repo})
|
|
264
|
+
if "data" in result and "repository" in result["data"]:
|
|
265
|
+
all_labels = result["data"]["repository"]["labels"]["nodes"]
|
|
266
|
+
label_map = {label["name"].lower(): label["id"] for label in all_labels}
|
|
267
|
+
return [label_map.get(label.lower()) for label in labels if label.lower() in label_map]
|
|
268
|
+
return []
|
|
269
|
+
|
|
270
|
+
def apply_labels(self, number: int, node_id: str, labels: list[str], issue_type: str):
|
|
271
|
+
"""Applies specified labels to a GitHub issue, pull request, or discussion."""
|
|
272
|
+
if "Alert" in labels:
|
|
273
|
+
self.create_alert_label()
|
|
274
|
+
|
|
275
|
+
if issue_type == "discussion":
|
|
276
|
+
label_ids = self.get_label_ids(labels)
|
|
277
|
+
if not label_ids:
|
|
278
|
+
print("No valid labels to apply.")
|
|
279
|
+
return
|
|
280
|
+
self.graphql_request(GRAPHQL_ADD_LABELS_TO_DISCUSSION, {"labelableId": node_id, "labelIds": label_ids})
|
|
281
|
+
else:
|
|
282
|
+
url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{number}/labels"
|
|
283
|
+
self.post(url, json={"labels": labels})
|
|
284
|
+
|
|
285
|
+
def create_alert_label(self):
|
|
286
|
+
"""Creates the 'Alert' label in the repository if it doesn't exist."""
|
|
287
|
+
alert_label = {"name": "Alert", "color": "FF0000", "description": "Potential spam, abuse, or off-topic."}
|
|
288
|
+
self.post(f"{GITHUB_API_URL}/repos/{self.repository}/labels", json=alert_label)
|
|
289
|
+
|
|
290
|
+
def remove_labels(self, number: int, labels: tuple[str, ...]):
|
|
291
|
+
"""Removes specified labels from an issue or PR."""
|
|
292
|
+
for label in labels:
|
|
293
|
+
self.delete(f"{GITHUB_API_URL}/repos/{self.repository}/issues/{number}/labels/{label}")
|
|
294
|
+
|
|
295
|
+
def add_comment(self, number: int, node_id: str, comment: str, issue_type: str):
|
|
296
|
+
"""Adds a comment to an issue, pull request, or discussion."""
|
|
297
|
+
if issue_type == "discussion":
|
|
298
|
+
self.graphql_request(GRAPHQL_ADD_DISCUSSION_COMMENT, variables={"discussionId": node_id, "body": comment})
|
|
299
|
+
else:
|
|
300
|
+
self.post(f"{GITHUB_API_URL}/repos/{self.repository}/issues/{number}/comments", json={"body": comment})
|
|
301
|
+
|
|
302
|
+
def update_content(self, number: int, node_id: str, issue_type: str, title: str = None, body: str = None):
|
|
303
|
+
"""Updates the title and/or body of an issue, pull request, or discussion."""
|
|
304
|
+
if issue_type == "discussion":
|
|
305
|
+
variables = {"discussionId": node_id}
|
|
306
|
+
if title:
|
|
307
|
+
variables["title"] = title
|
|
308
|
+
if body:
|
|
309
|
+
variables["body"] = body
|
|
310
|
+
self.graphql_request(GRAPHQL_UPDATE_DISCUSSION, variables=variables)
|
|
311
|
+
else:
|
|
312
|
+
url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{number}"
|
|
313
|
+
data = {}
|
|
314
|
+
if title:
|
|
315
|
+
data["title"] = title
|
|
316
|
+
if body:
|
|
317
|
+
data["body"] = body
|
|
318
|
+
self.patch(url, json=data)
|
|
319
|
+
|
|
320
|
+
def close_item(self, number: int, node_id: str, issue_type: str):
|
|
321
|
+
"""Closes an issue, pull request, or discussion."""
|
|
322
|
+
if issue_type == "discussion":
|
|
323
|
+
self.graphql_request(GRAPHQL_CLOSE_DISCUSSION, variables={"discussionId": node_id})
|
|
324
|
+
else:
|
|
325
|
+
url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{number}"
|
|
326
|
+
self.patch(url, json={"state": "closed"})
|
|
327
|
+
|
|
328
|
+
def lock_item(self, number: int, node_id: str, issue_type: str):
|
|
329
|
+
"""Locks an issue, pull request, or discussion to prevent further interactions."""
|
|
330
|
+
if issue_type == "discussion":
|
|
331
|
+
self.graphql_request(GRAPHQL_LOCK_DISCUSSION, variables={"lockableId": node_id, "lockReason": "OFF_TOPIC"})
|
|
332
|
+
else:
|
|
333
|
+
url = f"{GITHUB_API_URL}/repos/{self.repository}/issues/{number}/lock"
|
|
334
|
+
self.put(url, json={"lock_reason": "off-topic"})
|
|
335
|
+
|
|
336
|
+
def block_user(self, username: str):
|
|
337
|
+
"""Blocks a user from the organization."""
|
|
338
|
+
url = f"{GITHUB_API_URL}/orgs/{self.repository.split('/')[0]}/blocks/{username}"
|
|
339
|
+
self.put(url)
|
|
340
|
+
|
|
341
|
+
def handle_alert(self, number: int, node_id: str, issue_type: str, username: str, block: bool = False):
|
|
342
|
+
"""Handles content flagged as alert: updates content, locks, optionally closes and blocks user."""
|
|
343
|
+
new_title = "Content Under Review"
|
|
344
|
+
new_body = """This post has been flagged for review by [Ultralytics Actions](https://ultralytics.com/actions) due to possible spam, abuse, or off-topic content. For more information please see our:
|
|
345
|
+
|
|
346
|
+
- [Code of Conduct](https://docs.ultralytics.com/help/code-of-conduct/)
|
|
347
|
+
- [Security Policy](https://docs.ultralytics.com/help/security/)
|
|
348
|
+
|
|
349
|
+
For questions or bug reports related to this action please visit https://github.com/ultralytics/actions.
|
|
350
|
+
|
|
351
|
+
Thank you 🙏
|
|
352
|
+
"""
|
|
353
|
+
self.update_content(number, node_id, issue_type, title=new_title, body=new_body)
|
|
354
|
+
if issue_type != "pull request":
|
|
355
|
+
self.close_item(number, node_id, issue_type)
|
|
356
|
+
self.lock_item(number, node_id, issue_type)
|
|
357
|
+
if block:
|
|
358
|
+
self.block_user(username)
|
|
359
|
+
|
|
360
|
+
def get_pr_contributors(self) -> tuple[str | None, dict]:
|
|
361
|
+
"""Gets PR contributors and closing issues, returns (pr_credit_string, pr_data)."""
|
|
362
|
+
owner, repo = self.repository.split("/")
|
|
363
|
+
variables = {"owner": owner, "repo": repo, "pr_number": self.pr["number"]}
|
|
364
|
+
response = self.post(GITHUB_GRAPHQL_URL, json={"query": GRAPHQL_PR_CONTRIBUTORS, "variables": variables})
|
|
365
|
+
if response.status_code != 200:
|
|
366
|
+
return None, {}
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
data = response.json()["data"]["repository"]["pullRequest"]
|
|
370
|
+
comments = data["reviews"]["nodes"] + data["comments"]["nodes"]
|
|
371
|
+
token_username = self.get_username()
|
|
372
|
+
author = data["author"]["login"] if data["author"]["__typename"] != "Bot" else None
|
|
373
|
+
|
|
374
|
+
contributors = {x["author"]["login"] for x in comments if x["author"]["__typename"] != "Bot"}
|
|
375
|
+
|
|
376
|
+
for commit in data["commits"]["nodes"]:
|
|
377
|
+
commit_data = commit["commit"]
|
|
378
|
+
for user_type in ["author", "committer"]:
|
|
379
|
+
if user := commit_data[user_type].get("user"):
|
|
380
|
+
if login := user.get("login"):
|
|
381
|
+
contributors.add(login)
|
|
382
|
+
|
|
383
|
+
contributors.discard(author)
|
|
384
|
+
contributors.discard(token_username)
|
|
385
|
+
|
|
386
|
+
pr_credit = ""
|
|
387
|
+
if author and author != token_username:
|
|
388
|
+
pr_credit += f"@{author}"
|
|
389
|
+
if contributors:
|
|
390
|
+
pr_credit += (" with contributions from " if pr_credit else "") + ", ".join(
|
|
391
|
+
f"@{c}" for c in contributors
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
return pr_credit, data
|
|
395
|
+
except KeyError as e:
|
|
396
|
+
print(f"Error parsing GraphQL response: {e}")
|
|
397
|
+
return None, {}
|
|
398
|
+
|
|
151
399
|
def print_info(self):
|
|
152
400
|
"""Print GitHub Actions information including event details and repository information."""
|
|
153
401
|
info = {
|
actions/utils/openai_utils.py
CHANGED
|
@@ -28,18 +28,91 @@ def remove_outer_codeblocks(string):
|
|
|
28
28
|
"""Removes outer code block markers and language identifiers from a string while preserving inner content."""
|
|
29
29
|
string = string.strip()
|
|
30
30
|
if string.startswith("```") and string.endswith("```"):
|
|
31
|
-
# Get everything after first ``` and newline, up to the last ```
|
|
32
31
|
string = string[string.find("\n") + 1 : string.rfind("```")].strip()
|
|
33
32
|
return string
|
|
34
33
|
|
|
35
34
|
|
|
35
|
+
def filter_labels(available_labels: dict, current_labels: list = None, is_pr: bool = False) -> dict:
|
|
36
|
+
"""Filters labels by removing manually-assigned and mutually exclusive labels."""
|
|
37
|
+
current_labels = current_labels or []
|
|
38
|
+
filtered = available_labels.copy()
|
|
39
|
+
|
|
40
|
+
for label in {
|
|
41
|
+
"help wanted",
|
|
42
|
+
"TODO",
|
|
43
|
+
"research",
|
|
44
|
+
"non-reproducible",
|
|
45
|
+
"popular",
|
|
46
|
+
"invalid",
|
|
47
|
+
"Stale",
|
|
48
|
+
"wontfix",
|
|
49
|
+
"duplicate",
|
|
50
|
+
}:
|
|
51
|
+
filtered.pop(label, None)
|
|
52
|
+
|
|
53
|
+
if "bug" in current_labels:
|
|
54
|
+
filtered.pop("question", None)
|
|
55
|
+
elif "question" in current_labels:
|
|
56
|
+
filtered.pop("bug", None)
|
|
57
|
+
|
|
58
|
+
if "Alert" not in filtered:
|
|
59
|
+
filtered["Alert"] = (
|
|
60
|
+
"Potential spam, abuse, or illegal activity including advertising, unsolicited promotions, malware, "
|
|
61
|
+
"phishing, crypto offers, pirated software or media, free movie downloads, cracks, keygens or any other "
|
|
62
|
+
"content that violates terms of service or legal standards."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return filtered
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_pr_summary_guidelines() -> str:
|
|
69
|
+
"""Returns PR summary formatting guidelines (used by both unified PR open and PR update/merge)."""
|
|
70
|
+
return """Summarize this PR, focusing on major changes, their purpose, and potential impact. Keep the summary clear and concise, suitable for a broad audience. Add emojis to enliven the summary. Your response must include all 3 sections below with their markdown headers:
|
|
71
|
+
|
|
72
|
+
### 🌟 Summary
|
|
73
|
+
(single-line synopsis)
|
|
74
|
+
|
|
75
|
+
### 📊 Key Changes
|
|
76
|
+
- (bullet points highlighting major changes)
|
|
77
|
+
|
|
78
|
+
### 🎯 Purpose & Impact
|
|
79
|
+
- (bullet points explaining benefits and potential impact to users)"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_pr_summary_prompt(repository: str, diff_text: str) -> tuple[str, bool]:
|
|
83
|
+
"""Returns the complete PR summary generation prompt with diff (used by PR update/merge)."""
|
|
84
|
+
ratio = 3.3 # about 3.3 characters per token
|
|
85
|
+
limit = round(128000 * ratio * 0.5) # use up to 50% of the 128k context window for prompt
|
|
86
|
+
|
|
87
|
+
prompt = (
|
|
88
|
+
f"{get_pr_summary_guidelines()}\n\nRepository: '{repository}'\n\nHere's the PR diff:\n\n{diff_text[:limit]}"
|
|
89
|
+
)
|
|
90
|
+
return prompt, len(diff_text) > limit
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_pr_first_comment_template(repository: str) -> str:
|
|
94
|
+
"""Returns the PR first comment template with checklist (used only by unified PR open)."""
|
|
95
|
+
return f"""👋 Hello @username, thank you for submitting an `{repository}` 🚀 PR! To ensure a seamless integration of your work, please review the following checklist:
|
|
96
|
+
|
|
97
|
+
- ✅ **Define a Purpose**: Clearly explain the purpose of your fix or feature in your PR description, and link to any [relevant issues](https://github.com/{repository}/issues). Ensure your commit messages are clear, concise, and adhere to the project's conventions.
|
|
98
|
+
- ✅ **Synchronize with Source**: Confirm your PR is synchronized with the `{repository}` `main` branch. If it's behind, update it by clicking the 'Update branch' button or by running `git pull` and `git merge main` locally.
|
|
99
|
+
- ✅ **Ensure CI Checks Pass**: Verify all Ultralytics [Continuous Integration (CI)](https://docs.ultralytics.com/help/CI/) checks are passing. If any checks fail, please address the issues.
|
|
100
|
+
- ✅ **Update Documentation**: Update the relevant [documentation](https://docs.ultralytics.com/) for any new or modified features.
|
|
101
|
+
- ✅ **Add Tests**: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
|
|
102
|
+
- ✅ **Sign the CLA**: Please ensure you have signed our [Contributor License Agreement](https://docs.ultralytics.com/help/CLA/) if this is your first Ultralytics PR by writing "I have read the CLA Document and I sign the CLA" in a new message.
|
|
103
|
+
- ✅ **Minimize Changes**: Limit your changes to the **minimum** necessary for your bug fix or feature addition. _"It is not daily increase but daily decrease, hack away the unessential. The closer to the source, the less wastage there is."_ — Bruce Lee
|
|
104
|
+
|
|
105
|
+
For more guidance, please refer to our [Contributing Guide](https://docs.ultralytics.com/help/contributing/). Don't hesitate to leave a comment if you have any questions. Thank you for contributing to Ultralytics! 🚀"""
|
|
106
|
+
|
|
107
|
+
|
|
36
108
|
def get_completion(
|
|
37
109
|
messages: list[dict[str, str]],
|
|
38
110
|
check_links: bool = True,
|
|
39
|
-
remove: list[str] = (" @giscus[bot]",),
|
|
40
|
-
temperature: float = 1.0,
|
|
41
|
-
reasoning_effort: str = None,
|
|
42
|
-
|
|
111
|
+
remove: list[str] = (" @giscus[bot]",),
|
|
112
|
+
temperature: float = 1.0,
|
|
113
|
+
reasoning_effort: str = None,
|
|
114
|
+
response_format: dict = None,
|
|
115
|
+
) -> str | dict:
|
|
43
116
|
"""Generates a completion using OpenAI's Responses API based on input messages."""
|
|
44
117
|
assert OPENAI_API_KEY, "OpenAI API key is required."
|
|
45
118
|
url = "https://api.openai.com/v1/responses"
|
|
@@ -47,14 +120,12 @@ def get_completion(
|
|
|
47
120
|
if messages and messages[0].get("role") == "system":
|
|
48
121
|
messages[0]["content"] += "\n\n" + SYSTEM_PROMPT_ADDITION
|
|
49
122
|
|
|
50
|
-
content = ""
|
|
51
123
|
max_retries = 2
|
|
52
|
-
for attempt in range(max_retries + 2):
|
|
124
|
+
for attempt in range(max_retries + 2):
|
|
53
125
|
data = {"model": OPENAI_MODEL, "input": messages, "store": False, "temperature": temperature}
|
|
54
|
-
|
|
55
|
-
# Add reasoning for GPT-5 models
|
|
56
126
|
if "gpt-5" in OPENAI_MODEL:
|
|
57
|
-
data["reasoning"] = {"effort": reasoning_effort or "low"}
|
|
127
|
+
data["reasoning"] = {"effort": reasoning_effort or "low"}
|
|
128
|
+
# GPT-5 Responses API handles JSON via prompting, not format parameter
|
|
58
129
|
|
|
59
130
|
r = requests.post(url, json=data, headers=headers)
|
|
60
131
|
if r.status_code != 200:
|
|
@@ -62,7 +133,6 @@ def get_completion(
|
|
|
62
133
|
r.raise_for_status()
|
|
63
134
|
response_data = r.json()
|
|
64
135
|
|
|
65
|
-
# Extract text from output array
|
|
66
136
|
content = ""
|
|
67
137
|
for item in response_data.get("output", []):
|
|
68
138
|
if item.get("type") == "message":
|
|
@@ -71,10 +141,16 @@ def get_completion(
|
|
|
71
141
|
content += content_item.get("text", "")
|
|
72
142
|
|
|
73
143
|
content = content.strip()
|
|
144
|
+
if response_format and response_format.get("type") == "json_object":
|
|
145
|
+
import json
|
|
146
|
+
|
|
147
|
+
return json.loads(content)
|
|
148
|
+
|
|
74
149
|
content = remove_outer_codeblocks(content)
|
|
75
150
|
for x in remove:
|
|
76
151
|
content = content.replace(x, "")
|
|
77
|
-
|
|
152
|
+
|
|
153
|
+
if not check_links or check_links_in_string(content):
|
|
78
154
|
return content
|
|
79
155
|
|
|
80
156
|
if attempt < max_retries:
|
|
@@ -82,11 +158,61 @@ def get_completion(
|
|
|
82
158
|
else:
|
|
83
159
|
print("Max retries reached. Updating prompt to exclude links.")
|
|
84
160
|
messages.append({"role": "user", "content": "Please provide a response without any URLs or links in it."})
|
|
85
|
-
check_links = False
|
|
161
|
+
check_links = False
|
|
86
162
|
|
|
87
163
|
return content
|
|
88
164
|
|
|
89
165
|
|
|
166
|
+
def get_pr_open_response(repository: str, diff_text: str, title: str, body: str, available_labels: dict) -> dict:
|
|
167
|
+
"""Generates unified PR response with summary, labels, and first comment in a single API call."""
|
|
168
|
+
ratio = 3.3 # about 3.3 characters per token
|
|
169
|
+
limit = round(128000 * ratio * 0.5) # use up to 50% of the 128k context window for prompt
|
|
170
|
+
is_large = len(diff_text) > limit
|
|
171
|
+
|
|
172
|
+
filtered_labels = filter_labels(available_labels, is_pr=True)
|
|
173
|
+
labels_str = "\n".join(f"- {name}: {description}" for name, description in filtered_labels.items())
|
|
174
|
+
|
|
175
|
+
prompt = f"""You are processing a new GitHub pull request for the {repository} repository.
|
|
176
|
+
|
|
177
|
+
Generate 3 outputs in a single JSON response for the PR titled {title} with the following diff:
|
|
178
|
+
{diff_text[:limit]}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
--- FIRST JSON OUTPUT (PR SUMMARY) ---
|
|
182
|
+
{get_pr_summary_guidelines()}
|
|
183
|
+
|
|
184
|
+
--- SECOND JSON OUTPUT (PR LABELS) ---
|
|
185
|
+
Array of 1-3 most relevant label names. Only use "Alert" with high confidence for inappropriate PRs. Return empty array if no labels relevant. Available labels:
|
|
186
|
+
{labels_str}
|
|
187
|
+
|
|
188
|
+
--- THIRD OUTPUT (PR FIRST COMMENT) ---
|
|
189
|
+
Customized welcome message adapting the template below:
|
|
190
|
+
- INCLUDE ALL LINKS AND INSTRUCTIONS from the template below, customized as appropriate
|
|
191
|
+
- Keep all checklist items and links from template
|
|
192
|
+
- Only link to files or URLs in the template below, do not add external links
|
|
193
|
+
- Mention this is automated and an engineer will assist
|
|
194
|
+
- Use a few emojis
|
|
195
|
+
- No sign-off or "best regards"
|
|
196
|
+
- No spaces between bullet points
|
|
197
|
+
|
|
198
|
+
Example comment template (adapt as needed, keep all links):
|
|
199
|
+
{get_pr_first_comment_template(repository)}
|
|
200
|
+
|
|
201
|
+
Return ONLY valid JSON in this exact format:
|
|
202
|
+
{{"summary": "...", "labels": [...], "first_comment": "..."}}"""
|
|
203
|
+
|
|
204
|
+
messages = [
|
|
205
|
+
{"role": "system", "content": "You are an Ultralytics AI assistant processing GitHub PRs."},
|
|
206
|
+
{"role": "user", "content": prompt},
|
|
207
|
+
]
|
|
208
|
+
result = get_completion(messages, temperature=1.0, response_format={"type": "json_object"})
|
|
209
|
+
if is_large and "summary" in result:
|
|
210
|
+
result["summary"] = (
|
|
211
|
+
"**WARNING ⚠️** this PR is very large, summary may not cover all changes.\n\n" + result["summary"]
|
|
212
|
+
)
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
|
|
90
216
|
if __name__ == "__main__":
|
|
91
217
|
messages = [
|
|
92
218
|
{"role": "system", "content": "You are a helpful AI assistant."},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ultralytics-actions
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Ultralytics Actions for GitHub automation and PR management.
|
|
5
5
|
Author-email: Glenn Jocher <glenn.jocher@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -64,6 +64,7 @@ Ultralytics Actions automatically applies formats, updates, and enhancements usi
|
|
|
64
64
|
- **Spell Check:** Common misspellings are caught using [codespell](https://github.com/codespell-project/codespell).
|
|
65
65
|
- **Broken Links Check:** Broken links in documentation and Markdown files are identified using [Lychee](https://github.com/lycheeverse/lychee).
|
|
66
66
|
- **PR Summary:** Concise Pull Request summaries are generated using [OpenAI](https://openai.com/) GPT-5, improving clarity and review efficiency.
|
|
67
|
+
- **PR Review:** AI-powered inline code reviews identify critical bugs, security issues, and code quality concerns with suggested fixes.
|
|
67
68
|
- **Auto-labeling:** Relevant labels are applied to issues and pull requests via [OpenAI](https://openai.com/) GPT-5 for intelligent categorization.
|
|
68
69
|
|
|
69
70
|
## 🛠️ How It Works
|
|
@@ -74,6 +75,7 @@ Ultralytics Actions triggers on various GitHub events to streamline workflows:
|
|
|
74
75
|
- **Pull Requests:**
|
|
75
76
|
- Ensures contributions meet formatting standards before merging.
|
|
76
77
|
- Generates a concise summary of changes using GPT-5.
|
|
78
|
+
- Provides AI-powered inline code reviews with suggested fixes for critical issues.
|
|
77
79
|
- Applies relevant labels using GPT-5 for intelligent categorization.
|
|
78
80
|
- **Issues:** Automatically applies relevant labels using GPT-5 when new issues are created.
|
|
79
81
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
actions/__init__.py,sha256=Sxxh83BO8x10c7LkJcNoSTK6sE8e4KtnqLV9qZsp_pA,772
|
|
2
|
+
actions/dispatch_actions.py,sha256=8jaaVkA_LSlpUQ4tuzmQtf2kw3G09uVRD_LmJyXYKNE,4215
|
|
3
|
+
actions/first_interaction.py,sha256=BkiMKppSeZ3VOP9jK0Q7FwqUYivFM4LIjJia9KgqNco,9193
|
|
4
|
+
actions/review_pr.py,sha256=O9EfC8qNbfwq1FgFkVVFse1YTUKYyLIbo9d3Hf9luu4,15434
|
|
5
|
+
actions/summarize_pr.py,sha256=K7o6RQQxUjsWpP4FgH7Nrgp4Qbvnprk8C01PasX_2vE,5724
|
|
6
|
+
actions/summarize_release.py,sha256=bIAqBbjq4eCDBAV3MWb8yq1hSK93MEyazoN29kT1-js,9068
|
|
7
|
+
actions/update_file_headers.py,sha256=E5fKYLdeW16-BHCcuqxohGpGZqgEh-WX4ZmCQJw2R90,6684
|
|
8
|
+
actions/update_markdown_code_blocks.py,sha256=w3DTRltg2Rmr4-qrNawv_S2vJbheKE0tne1iz79FzXg,8692
|
|
9
|
+
actions/utils/__init__.py,sha256=sKNx6o5jcAraEdGFph0o-YC7dMMY-dg_FprIBa6Jydw,1027
|
|
10
|
+
actions/utils/common_utils.py,sha256=8ZmgaXZU3J2sg-HSaldp3hHYq7bI3akcJHdIXPmcNAo,11908
|
|
11
|
+
actions/utils/github_utils.py,sha256=WpvVQIWScIB_mZxWSZ_zGun-oW90bI15eKqVCDLusJs,18264
|
|
12
|
+
actions/utils/openai_utils.py,sha256=91ohkSlldVZt-icYcK7ZLfgQfnMbf4mTWT3nRnQ1QG4,10486
|
|
13
|
+
actions/utils/version_utils.py,sha256=EIbm3iZVNyNl3dh8aNz_9ITeTC93ZxfyUzIRkO3tSXw,3242
|
|
14
|
+
ultralytics_actions-0.1.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
15
|
+
ultralytics_actions-0.1.1.dist-info/METADATA,sha256=iDfUTlwy7koo7KAtTHwkrvYMSRRqJHzr67RJEBI4nus,12389
|
|
16
|
+
ultralytics_actions-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
ultralytics_actions-0.1.1.dist-info/entry_points.txt,sha256=n_VbDs3Xj33daaeN_2D72UTEuyeH8hVc6-CPH55ymkY,496
|
|
18
|
+
ultralytics_actions-0.1.1.dist-info/top_level.txt,sha256=5apM5x80QlJcGbACn1v3fkmIuL1-XQCKcItJre7w7Tw,8
|
|
19
|
+
ultralytics_actions-0.1.1.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
actions/__init__.py,sha256=0aWgNpfTBBLWZ0b0Ij8EQ1K5fKag5uAN4U1gNL7NdJY,772
|
|
2
|
-
actions/dispatch_actions.py,sha256=RPqjsnH_8932oLaPp9wWRCWKTaMp8M9dn6CIAMtNjN0,4230
|
|
3
|
-
actions/first_interaction.py,sha256=bPG_BMp_k-iJbeQ-fI1bTRHxnfo86ZdIhb0wytBGXVo,16511
|
|
4
|
-
actions/review_pr.py,sha256=OR8qF9Eo1JDdjHyau5pnVq2-9c4buru0IoyNgOrsE8E,11979
|
|
5
|
-
actions/summarize_pr.py,sha256=2qDkSlonQGytJXcthsggPE-AKu2xhtcsWwIaBgkSmAk,10499
|
|
6
|
-
actions/summarize_release.py,sha256=1MQ7Cefv4GTRxr10LwX4b6CFyYZNLNrbCzUKlokUoKE,8671
|
|
7
|
-
actions/update_file_headers.py,sha256=E5fKYLdeW16-BHCcuqxohGpGZqgEh-WX4ZmCQJw2R90,6684
|
|
8
|
-
actions/update_markdown_code_blocks.py,sha256=XOW0k5f84yYzoXE-zneX0QJeM4dseusPJha8d6kV9hE,8747
|
|
9
|
-
actions/utils/__init__.py,sha256=7k4cmFX0Td99Uzgsd8Mm-E0xq5kQ5ZJoPM_oGCVD4CU,804
|
|
10
|
-
actions/utils/common_utils.py,sha256=8ZmgaXZU3J2sg-HSaldp3hHYq7bI3akcJHdIXPmcNAo,11908
|
|
11
|
-
actions/utils/github_utils.py,sha256=GvU_GSwEr_nA59CgyrurjHSCyKpDSL3LDm69OnuAdJI,8117
|
|
12
|
-
actions/utils/openai_utils.py,sha256=wo1VBsDwcnkl9uyKt3D8ZY0OjcO_voc6tVbbcfcRl88,4315
|
|
13
|
-
actions/utils/version_utils.py,sha256=EIbm3iZVNyNl3dh8aNz_9ITeTC93ZxfyUzIRkO3tSXw,3242
|
|
14
|
-
ultralytics_actions-0.1.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
15
|
-
ultralytics_actions-0.1.0.dist-info/METADATA,sha256=uPDfpMtIiZvuCxbeIXzQ1gFua0W0m5JjvSY3qmhL41Q,12166
|
|
16
|
-
ultralytics_actions-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
-
ultralytics_actions-0.1.0.dist-info/entry_points.txt,sha256=n_VbDs3Xj33daaeN_2D72UTEuyeH8hVc6-CPH55ymkY,496
|
|
18
|
-
ultralytics_actions-0.1.0.dist-info/top_level.txt,sha256=5apM5x80QlJcGbACn1v3fkmIuL1-XQCKcItJre7w7Tw,8
|
|
19
|
-
ultralytics_actions-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
{ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{ultralytics_actions-0.1.0.dist-info → ultralytics_actions-0.1.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|