ultralytics-actions 0.0.25__tar.gz → 0.0.30__tar.gz

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.
Files changed (21) hide show
  1. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/PKG-INFO +1 -1
  2. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/__init__.py +1 -1
  3. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/first_interaction.py +2 -2
  4. ultralytics_actions-0.0.30/actions/summarize_pr.py +242 -0
  5. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/utils/__init__.py +2 -0
  6. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/utils/github_utils.py +21 -0
  7. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/ultralytics_actions.egg-info/PKG-INFO +1 -1
  8. ultralytics_actions-0.0.25/actions/summarize_pr.py +0 -140
  9. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/LICENSE +0 -0
  10. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/README.md +0 -0
  11. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/summarize_release.py +0 -0
  12. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/update_markdown_code_blocks.py +0 -0
  13. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/utils/common_utils.py +0 -0
  14. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/actions/utils/openai_utils.py +0 -0
  15. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/pyproject.toml +0 -0
  16. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/setup.cfg +0 -0
  17. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/ultralytics_actions.egg-info/SOURCES.txt +0 -0
  18. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/ultralytics_actions.egg-info/dependency_links.txt +0 -0
  19. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/ultralytics_actions.egg-info/entry_points.txt +0 -0
  20. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/ultralytics_actions.egg-info/requires.txt +0 -0
  21. {ultralytics_actions-0.0.25 → ultralytics_actions-0.0.30}/ultralytics_actions.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ultralytics-actions
3
- Version: 0.0.25
3
+ Version: 0.0.30
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>
@@ -22,4 +22,4 @@
22
22
  # ├── test_summarize_pr.py
23
23
  # └── ...
24
24
 
25
- __version__ = "0.0.25"
25
+ __version__ = "0.0.30"
@@ -335,7 +335,7 @@ INSTRUCTIONS:
335
335
  - Adapt the example {issue_type} response below as appropriate, keeping all badges, links and references provided
336
336
  - For bug reports, specifically request a minimum reproducible example (MRE) if not provided
337
337
  - INCLUDE ALL LINKS AND INSTRUCTIONS IN THE EXAMPLE BELOW, customized as appropriate
338
- - In your response, mention to the user that this is an automated response and that an Ultralytics engineer will also assist soon
338
+ - Mention to the user that this is an automated response and that an Ultralytics engineer will also assist soon
339
339
  - Do not add a sign-off or valediction like "best regards" at the end of your response
340
340
  - Do not add spaces between bullet points or numbered lists
341
341
  - Only link to files or URLs in the example below, do not add external links
@@ -359,7 +359,7 @@ YOUR {issue_type.upper()} RESPONSE:
359
359
  messages = [
360
360
  {
361
361
  "role": "system",
362
- "content": f"You are a helpful assistant responding to GitHub {issue_type}s for the {org_name} organization.",
362
+ "content": f"You are a helpful assistant responding to GitHub {issue_type}s for {org_name}.",
363
363
  },
364
364
  {"role": "user", "content": prompt},
365
365
  ]
@@ -0,0 +1,242 @@
1
+ # Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
2
+
3
+ import time
4
+
5
+ import requests
6
+
7
+ from .utils import (
8
+ GITHUB_API_URL,
9
+ GITHUB_HEADERS,
10
+ GITHUB_REPOSITORY,
11
+ PR,
12
+ get_completion,
13
+ get_github_username,
14
+ get_pr_diff,
15
+ )
16
+
17
+ # Constants
18
+ SUMMARY_START = (
19
+ "## 🛠️ PR Summary\n\n<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)<sub>\n\n"
20
+ )
21
+
22
+
23
+ def generate_merge_message(pr_author, contributors, pr_summary=None):
24
+ """Generates a thank-you message for merged PR contributors."""
25
+ contributors_str = ", ".join(f"@{c}" for c in contributors if c != pr_author)
26
+ mention_str = f"@{pr_author}"
27
+ if contributors_str:
28
+ mention_str += f" and {contributors_str}"
29
+
30
+ messages = [
31
+ {
32
+ "role": "system",
33
+ "content": "You are an Ultralytics AI assistant. Generate meaningful, inspiring messages to GitHub users.",
34
+ },
35
+ {
36
+ "role": "user",
37
+ "content": f"Write a friendly thank you for a merged PR by these GitHub contributors: {mention_str}. "
38
+ f"Context from PR:\n{pr_summary}\n\n"
39
+ f"Start with the exciting message that this PR is now merged, and weave in an inspiring quote "
40
+ f"from a famous figure in science, philosophy or stoicism. "
41
+ f"Keep the message concise yet relevant to the specific contributions in this PR. "
42
+ f"We want the contributors to feel their effort is appreciated and will make a difference in the world.",
43
+ },
44
+ ]
45
+ return get_completion(messages)
46
+
47
+
48
+ def post_merge_message(pr_number, pr_author, contributors, summary):
49
+ """Posts thank you message on PR after merge."""
50
+ message = generate_merge_message(pr_author, contributors, summary)
51
+ comment_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{pr_number}/comments"
52
+ response = requests.post(comment_url, json={"body": message}, headers=GITHUB_HEADERS)
53
+ return response.status_code == 201
54
+
55
+
56
+ def generate_issue_comment(pr_url, pr_summary):
57
+ """Generates a personalized issue comment using based on the PR context."""
58
+ messages = [
59
+ {
60
+ "role": "system",
61
+ "content": "You are an Ultralytics AI assistant. Generate friendly GitHub issue comments. No @ mentions or direct addressing.",
62
+ },
63
+ {
64
+ "role": "user",
65
+ "content": f"Write a GitHub issue comment announcing a potential fix has been merged in linked PR {pr_url}\n\n"
66
+ f"Context from PR:\n{pr_summary}\n\n"
67
+ f"Include:\n"
68
+ f"1. An explanation of key changes from the PR that may resolve this issue\n"
69
+ f"2. Options for testing if PR changes have resolved this issue:\n"
70
+ f" - pip install git+https://github.com/ultralytics/ultralytics.git@main # test latest changes\n"
71
+ f" - or await next official PyPI release\n"
72
+ f"3. Request feedback on whether the PR changes resolve the issue\n"
73
+ f"4. Thank 🙏 for reporting the issue and welcome any further feedback if the issue persists\n\n",
74
+ },
75
+ ]
76
+ return get_completion(messages)
77
+
78
+
79
+ def generate_pr_summary(repo_name, diff_text):
80
+ """Generates a concise, professional summary of a PR using OpenAI's API for Ultralytics repositories."""
81
+ if not diff_text:
82
+ diff_text = "**ERROR: DIFF IS EMPTY, THERE ARE ZERO CODE CHANGES IN THIS PR."
83
+ ratio = 3.3 # about 3.3 characters per token
84
+ limit = round(128000 * ratio * 0.5) # use up to 50% of the 128k context window for prompt
85
+ messages = [
86
+ {
87
+ "role": "system",
88
+ "content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub PRs from Ultralytics in a way that is accurate, concise, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple, concise terms.",
89
+ },
90
+ {
91
+ "role": "user",
92
+ "content": f"Summarize this '{repo_name}' 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. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
93
+ f"### 🌟 Summary (single-line synopsis)\n"
94
+ f"### 📊 Key Changes (bullet points highlighting any major changes)\n"
95
+ f"### 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n"
96
+ f"\n\nHere's the PR diff:\n\n{diff_text[:limit]}",
97
+ },
98
+ ]
99
+ reply = get_completion(messages)
100
+ if len(diff_text) > limit:
101
+ reply = "**WARNING ⚠️** this PR is very large, summary may not cover all changes.\n\n" + reply
102
+ return SUMMARY_START + reply
103
+
104
+
105
+ def update_pr_description(repo_name, pr_number, new_summary, max_retries=2):
106
+ """Updates PR description with new summary, retrying if description is None."""
107
+ pr_url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
108
+ description = ""
109
+ for i in range(max_retries + 1):
110
+ description = requests.get(pr_url, headers=GITHUB_HEADERS).json().get("body") or ""
111
+ if description:
112
+ break
113
+ if i < max_retries:
114
+ print("No current PR description found, retrying...")
115
+ time.sleep(1)
116
+
117
+ # Check if existing summary is present and update accordingly
118
+ start = "## 🛠️ PR Summary"
119
+ if start in description:
120
+ print("Existing PR Summary found, replacing.")
121
+ updated_description = description.split(start)[0] + new_summary
122
+ else:
123
+ print("PR Summary not found, appending.")
124
+ updated_description = description + "\n\n" + new_summary
125
+
126
+ # Update the PR description
127
+ update_response = requests.patch(pr_url, json={"body": updated_description}, headers=GITHUB_HEADERS)
128
+ return update_response.status_code
129
+
130
+
131
+ def label_fixed_issues(pr_number, pr_summary):
132
+ """Labels issues closed by this PR when merged, notifies users, and returns PR contributors."""
133
+ query = """
134
+ query($owner: String!, $repo: String!, $pr_number: Int!) {
135
+ repository(owner: $owner, name: $repo) {
136
+ pullRequest(number: $pr_number) {
137
+ closingIssuesReferences(first: 50) {
138
+ nodes {
139
+ number
140
+ }
141
+ }
142
+ url
143
+ body
144
+ author { login, __typename }
145
+ reviews(first: 50) {
146
+ nodes { author { login, __typename } }
147
+ }
148
+ comments(first: 50) {
149
+ nodes { author { login, __typename } }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ """
155
+
156
+ owner, repo = GITHUB_REPOSITORY.split("/")
157
+ variables = {"owner": owner, "repo": repo, "pr_number": pr_number}
158
+ graphql_url = "https://api.github.com/graphql"
159
+ response = requests.post(graphql_url, json={"query": query, "variables": variables}, headers=GITHUB_HEADERS)
160
+ if response.status_code != 200:
161
+ print(f"Failed to fetch linked issues. Status code: {response.status_code}")
162
+ return [], None
163
+
164
+ try:
165
+ data = response.json()["data"]["repository"]["pullRequest"]
166
+ comments = data["reviews"]["nodes"] + data["comments"]["nodes"] # merge lists
167
+ author = data["author"]["login"]
168
+
169
+ # Get unique contributors from reviews and comments
170
+ contributors = {x["author"]["login"] for x in comments if x["author"]["__typename"] != "Bot"}
171
+ contributors.discard(author) # Remove author from contributors list
172
+
173
+ # Generate personalized comment
174
+ comment = generate_issue_comment(pr_url=data["url"], pr_summary=pr_summary)
175
+
176
+ # Update linked issues
177
+ for issue in data["closingIssuesReferences"]["nodes"]:
178
+ issue_number = issue["number"]
179
+ # Add fixed label
180
+ label_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{issue_number}/labels"
181
+ label_response = requests.post(label_url, json={"labels": ["fixed"]}, headers=GITHUB_HEADERS)
182
+
183
+ # Add comment
184
+ comment_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{issue_number}/comments"
185
+ comment_response = requests.post(comment_url, json={"body": comment}, headers=GITHUB_HEADERS)
186
+
187
+ if label_response.status_code == 200 and comment_response.status_code == 201:
188
+ print(f"Added 'fixed' label and comment to issue #{issue_number}")
189
+ else:
190
+ print(
191
+ f"Failed to update issue #{issue_number}. Label status: {label_response.status_code}, "
192
+ f"Comment status: {comment_response.status_code}"
193
+ )
194
+
195
+ return contributors, author
196
+ except KeyError as e:
197
+ print(f"Error parsing GraphQL response: {e}")
198
+ return [], None
199
+
200
+
201
+ def remove_todos_on_merge(pr_number):
202
+ """Removes specified labels from PR."""
203
+ for label in ["TODO"]: # Can be extended with more labels in the future
204
+ requests.delete(
205
+ f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{pr_number}/labels/{label}", headers=GITHUB_HEADERS
206
+ )
207
+
208
+
209
+ def main():
210
+ """Summarize a pull request and update its description with a summary."""
211
+ pr_number = PR["number"]
212
+
213
+ print(f"Retrieving diff for PR {pr_number}")
214
+ diff = get_pr_diff(pr_number)
215
+
216
+ # Generate PR summary
217
+ print("Generating PR summary...")
218
+ summary = generate_pr_summary(GITHUB_REPOSITORY, diff)
219
+
220
+ # Update PR description
221
+ print("Updating PR description...")
222
+ status_code = update_pr_description(GITHUB_REPOSITORY, pr_number, summary)
223
+ if status_code == 200:
224
+ print("PR description updated successfully.")
225
+ else:
226
+ print(f"Failed to update PR description. Status code: {status_code}")
227
+
228
+ # Update linked issues and post thank you message if merged
229
+ if PR.get("merged"):
230
+ print("PR is merged, labeling fixed issues...")
231
+ contributors, author = label_fixed_issues(pr_number, summary)
232
+ print("Removing TODO label from PR...")
233
+ remove_todos_on_merge(pr_number)
234
+ username = get_github_username() # get GITHUB_TOKEN username
235
+ if author and author != username:
236
+ print("Posting PR author thank you message...")
237
+ contributors.discard(username)
238
+ post_merge_message(pr_number, author, contributors, summary)
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
@@ -14,6 +14,7 @@ from .github_utils import (
14
14
  PR,
15
15
  check_pypi_version,
16
16
  get_github_data,
17
+ get_github_username,
17
18
  get_pr_diff,
18
19
  graphql_request,
19
20
  ultralytics_actions_info,
@@ -38,6 +39,7 @@ __all__ = (
38
39
  "OPENAI_API_KEY",
39
40
  "OPENAI_MODEL",
40
41
  "get_completion",
42
+ "get_github_username",
41
43
  "check_pypi_version",
42
44
  "ultralytics_actions_info",
43
45
  )
@@ -24,6 +24,27 @@ PR = EVENT_DATA.get("pull_request", {})
24
24
  DISCUSSION = EVENT_DATA.get("discussion", {})
25
25
 
26
26
 
27
+ def get_github_username():
28
+ """Gets username associated with the GitHub token in GITHUB_HEADERS."""
29
+ query = """
30
+ query {
31
+ viewer {
32
+ login
33
+ }
34
+ }
35
+ """
36
+ response = requests.post("https://api.github.com/graphql", json={"query": query}, headers=GITHUB_HEADERS)
37
+ if response.status_code != 200:
38
+ print(f"Failed to fetch authenticated user. Status code: {response.status_code}")
39
+ return None
40
+
41
+ try:
42
+ return response.json()["data"]["viewer"]["login"]
43
+ except KeyError as e:
44
+ print(f"Error parsing authenticated user response: {e}")
45
+ return None
46
+
47
+
27
48
  def get_pr_diff(pr_number: int) -> str:
28
49
  """Retrieves the diff content for a specified pull request in a GitHub repository."""
29
50
  url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/pulls/{pr_number}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ultralytics-actions
3
- Version: 0.0.25
3
+ Version: 0.0.30
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>
@@ -1,140 +0,0 @@
1
- # Ultralytics Actions 🚀, AGPL-3.0 license https://ultralytics.com/license
2
-
3
- import time
4
-
5
- import requests
6
-
7
- from .utils import (
8
- GITHUB_API_URL,
9
- GITHUB_HEADERS,
10
- GITHUB_REPOSITORY,
11
- PR,
12
- get_completion,
13
- get_pr_diff,
14
- )
15
-
16
- # Action settings
17
- SUMMARY_START = (
18
- "## 🛠️ PR Summary\n\n<sub>Made with ❤️ by [Ultralytics Actions](https://github.com/ultralytics/actions)<sub>\n\n"
19
- )
20
-
21
-
22
- def generate_pr_summary(repo_name, diff_text):
23
- """Generates a concise, professional summary of a PR using OpenAI's API for Ultralytics repositories."""
24
- if not diff_text:
25
- diff_text = "**ERROR: DIFF IS EMPTY, THERE ARE ZERO CODE CHANGES IN THIS PR."
26
- ratio = 3.3 # about 3.3 characters per token
27
- limit = round(128000 * ratio * 0.5) # use up to 50% of the 128k context window for prompt
28
- messages = [
29
- {
30
- "role": "system",
31
- "content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub PRs from Ultralytics in a way that is accurate, concise, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple, concise terms.",
32
- },
33
- {
34
- "role": "user",
35
- "content": f"Summarize this '{repo_name}' 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. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
36
- f"### 🌟 Summary (single-line synopsis)\n"
37
- f"### 📊 Key Changes (bullet points highlighting any major changes)\n"
38
- f"### 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n"
39
- f"\n\nHere's the PR diff:\n\n{diff_text[:limit]}",
40
- },
41
- ]
42
- reply = get_completion(messages)
43
- if len(diff_text) > limit:
44
- reply = "**WARNING ⚠️** this PR is very large, summary may not cover all changes.\n\n" + reply
45
- return SUMMARY_START + reply
46
-
47
-
48
- def update_pr_description(repo_name, pr_number, new_summary, max_retries=2):
49
- """Updates PR description with new summary, retrying if description is None."""
50
- pr_url = f"{GITHUB_API_URL}/repos/{repo_name}/pulls/{pr_number}"
51
- description = ""
52
- for i in range(max_retries + 1):
53
- description = requests.get(pr_url, headers=GITHUB_HEADERS).json().get("body") or ""
54
- if description:
55
- break
56
- if i < max_retries:
57
- print("No current PR description found, retrying...")
58
- time.sleep(1)
59
-
60
- # Check if existing summary is present and update accordingly
61
- start = "## 🛠️ PR Summary"
62
- if start in description:
63
- print("Existing PR Summary found, replacing.")
64
- updated_description = description.split(start)[0] + new_summary
65
- else:
66
- print("PR Summary not found, appending.")
67
- updated_description = description + "\n\n" + new_summary
68
-
69
- # Update the PR description
70
- update_response = requests.patch(pr_url, json={"body": updated_description}, headers=GITHUB_HEADERS)
71
- return update_response.status_code
72
-
73
-
74
- def label_fixed_issues(pr_number):
75
- """Labels issues that are closed by this PR when it's merged."""
76
- # GraphQL query to get closing issues
77
- query = """
78
- query($owner: String!, $repo: String!, $pr_number: Int!) {
79
- repository(owner: $owner, name: $repo) {
80
- pullRequest(number: $pr_number) {
81
- closingIssuesReferences(first: 50) {
82
- nodes {
83
- number
84
- }
85
- }
86
- }
87
- }
88
- }
89
- """
90
-
91
- owner, repo = GITHUB_REPOSITORY.split("/")
92
- variables = {"owner": owner, "repo": repo, "pr_number": pr_number}
93
- graphql_url = "https://api.github.com/graphql"
94
- response = requests.post(graphql_url, json={"query": query, "variables": variables}, headers=GITHUB_HEADERS)
95
- if response.status_code != 200:
96
- print(f"Failed to fetch linked issues. Status code: {response.status_code}")
97
- return
98
-
99
- try:
100
- issues = response.json()["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["nodes"]
101
- for issue in issues:
102
- issue_number = issue["number"]
103
- label_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/issues/{issue_number}/labels"
104
- label_response = requests.post(label_url, json={"labels": ["fixed"]}, headers=GITHUB_HEADERS)
105
- if label_response.status_code == 200:
106
- print(f"Added 'fixed' label to issue #{issue_number}")
107
- else:
108
- print(f"Failed to add label to issue #{issue_number}. Status: {label_response.status_code}")
109
- except KeyError as e:
110
- print(f"Error parsing GraphQL response: {e}")
111
- return
112
-
113
-
114
- def main():
115
- """Summarize a pull request and update its description with an AI-generated summary."""
116
- pr_number = PR["number"]
117
-
118
- print(f"Retrieving diff for PR {pr_number}")
119
- diff = get_pr_diff(PR["number"])
120
-
121
- # Generate PR summary
122
- print("Generating PR summary...")
123
- summary = generate_pr_summary(GITHUB_REPOSITORY, diff)
124
-
125
- # Update PR description
126
- print("Updating PR description...")
127
- status_code = update_pr_description(GITHUB_REPOSITORY, pr_number, summary)
128
- if status_code == 200:
129
- print("PR description updated successfully.")
130
- else:
131
- print(f"Failed to update PR description. Status code: {status_code}")
132
-
133
- # Update linked issues
134
- if PR.get("merged"):
135
- print("PR is merged, labeling fixed issues...")
136
- label_fixed_issues(PR["number"])
137
-
138
-
139
- if __name__ == "__main__":
140
- main()