|
| 1 | +# Copyright 2025 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +from pathlib import Path |
| 16 | +from typing import Any |
| 17 | + |
| 18 | +from adk_pr_triaging_agent.settings import BOT_LABEL |
| 19 | +from adk_pr_triaging_agent.settings import GITHUB_BASE_URL |
| 20 | +from adk_pr_triaging_agent.settings import IS_INTERACTIVE |
| 21 | +from adk_pr_triaging_agent.settings import OWNER |
| 22 | +from adk_pr_triaging_agent.settings import REPO |
| 23 | +from adk_pr_triaging_agent.utils import error_response |
| 24 | +from adk_pr_triaging_agent.utils import get_diff |
| 25 | +from adk_pr_triaging_agent.utils import post_request |
| 26 | +from adk_pr_triaging_agent.utils import read_file |
| 27 | +from adk_pr_triaging_agent.utils import run_graphql_query |
| 28 | +from google.adk import Agent |
| 29 | +import requests |
| 30 | + |
| 31 | +LABEL_TO_OWNER = { |
| 32 | + "documentation": "polong-lin", |
| 33 | + "services": "DeanChensj", |
| 34 | + "tools": "seanzhou1023", |
| 35 | + "eval": "ankursharmas", |
| 36 | + "live": "hangfei", |
| 37 | + "models": "selcukgun", |
| 38 | + "tracing": "Jacksunwei", |
| 39 | + "core": "Jacksunwei", |
| 40 | + "web": "wyf7107", |
| 41 | +} |
| 42 | + |
| 43 | +CONTRIBUTING_MD = read_file( |
| 44 | + Path(__file__).resolve().parents[3] / "CONTRIBUTING.md" |
| 45 | +) |
| 46 | + |
| 47 | +APPROVAL_INSTRUCTION = ( |
| 48 | + "Do not ask for user approval for labeling or commenting! If you can't find" |
| 49 | + " appropriate labels for the PR, do not label it." |
| 50 | +) |
| 51 | +if IS_INTERACTIVE: |
| 52 | + APPROVAL_INSTRUCTION = ( |
| 53 | + "Only label or comment when the user approves the labeling or commenting!" |
| 54 | + ) |
| 55 | + |
| 56 | + |
| 57 | +def get_pull_request_details(pr_number: int) -> str: |
| 58 | + """Get the details of the specified pull request. |
| 59 | +
|
| 60 | + Args: |
| 61 | + pr_number: number of the Github pull request. |
| 62 | +
|
| 63 | + Returns: |
| 64 | + The status of this request, with the details when successful. |
| 65 | + """ |
| 66 | + print(f"Fetching details for PR #{pr_number} from {OWNER}/{REPO}") |
| 67 | + query = """ |
| 68 | + query($owner: String!, $repo: String!, $prNumber: Int!) { |
| 69 | + repository(owner: $owner, name: $repo) { |
| 70 | + pullRequest(number: $prNumber) { |
| 71 | + id |
| 72 | + title |
| 73 | + body |
| 74 | + author { |
| 75 | + login |
| 76 | + } |
| 77 | + labels(last: 10) { |
| 78 | + nodes { |
| 79 | + name |
| 80 | + } |
| 81 | + } |
| 82 | + files(last: 50) { |
| 83 | + nodes { |
| 84 | + path |
| 85 | + } |
| 86 | + } |
| 87 | + comments(last: 50) { |
| 88 | + nodes { |
| 89 | + id |
| 90 | + body |
| 91 | + createdAt |
| 92 | + author { |
| 93 | + login |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + commits(last: 50) { |
| 98 | + nodes { |
| 99 | + commit { |
| 100 | + url |
| 101 | + message |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + statusCheckRollup { |
| 106 | + state |
| 107 | + contexts(last: 20) { |
| 108 | + nodes { |
| 109 | + ... on StatusContext { |
| 110 | + context |
| 111 | + state |
| 112 | + targetUrl |
| 113 | + } |
| 114 | + ... on CheckRun { |
| 115 | + name |
| 116 | + status |
| 117 | + conclusion |
| 118 | + detailsUrl |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + """ |
| 127 | + variables = {"owner": OWNER, "repo": REPO, "prNumber": pr_number} |
| 128 | + url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/pulls/{pr_number}" |
| 129 | + |
| 130 | + try: |
| 131 | + response = run_graphql_query(query, variables) |
| 132 | + if "errors" in response: |
| 133 | + return error_response(str(response["errors"])) |
| 134 | + |
| 135 | + pr = response.get("data", {}).get("repository", {}).get("pullRequest") |
| 136 | + if not pr: |
| 137 | + return error_response(f"Pull Request #{pr_number} not found.") |
| 138 | + |
| 139 | + # Filter out main merge commits. |
| 140 | + original_commits = pr.get("commits", {}).get("nodes", {}) |
| 141 | + if original_commits: |
| 142 | + filtered_commits = [ |
| 143 | + commit_node |
| 144 | + for commit_node in original_commits |
| 145 | + if not commit_node["commit"]["message"].startswith( |
| 146 | + "Merge branch 'main' into" |
| 147 | + ) |
| 148 | + ] |
| 149 | + pr["commits"]["nodes"] = filtered_commits |
| 150 | + |
| 151 | + # Get diff of the PR and truncate it to avoid exceeding the maximum tokens. |
| 152 | + pr["diff"] = get_diff(url)[:10000] |
| 153 | + |
| 154 | + return {"status": "success", "pull_request": pr} |
| 155 | + except requests.exceptions.RequestException as e: |
| 156 | + return error_response(str(e)) |
| 157 | + |
| 158 | + |
| 159 | +def add_label_and_reviewer_to_pr(pr_number: int, label: str) -> dict[str, Any]: |
| 160 | + """Adds a specified label and requests a review from a mapped reviewer on a PR. |
| 161 | +
|
| 162 | + Args: |
| 163 | + pr_number: the number of the Github pull request |
| 164 | + label: the label to add |
| 165 | +
|
| 166 | + Returns: |
| 167 | + The the status of this request, with the applied label and assigned |
| 168 | + reviewer when successful. |
| 169 | + """ |
| 170 | + print(f"Attempting to add label '{label}' and a reviewer to PR #{pr_number}") |
| 171 | + if label not in LABEL_TO_OWNER: |
| 172 | + return error_response( |
| 173 | + f"Error: Label '{label}' is not an allowed label. Will not apply." |
| 174 | + ) |
| 175 | + |
| 176 | + # Pull Request is a special issue in Github, so we can use issue url for PR. |
| 177 | + label_url = ( |
| 178 | + f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{pr_number}/labels" |
| 179 | + ) |
| 180 | + label_payload = [label, BOT_LABEL] |
| 181 | + |
| 182 | + try: |
| 183 | + response = post_request(label_url, label_payload) |
| 184 | + except requests.exceptions.RequestException as e: |
| 185 | + return error_response(f"Error: {e}") |
| 186 | + |
| 187 | + owner = LABEL_TO_OWNER.get(label, None) |
| 188 | + if not owner: |
| 189 | + return { |
| 190 | + "status": "warning", |
| 191 | + "message": ( |
| 192 | + f"{response}\n\nLabel '{label}' does not have an owner. Will not" |
| 193 | + " assign." |
| 194 | + ), |
| 195 | + "applied_label": label, |
| 196 | + } |
| 197 | + reviewer_url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/pulls/{pr_number}/requested_reviewers" |
| 198 | + reviewer_payload = {"reviewers": [owner]} |
| 199 | + try: |
| 200 | + post_request(reviewer_url, reviewer_payload) |
| 201 | + except requests.exceptions.RequestException as e: |
| 202 | + return { |
| 203 | + "status": "warning", |
| 204 | + "message": f"Reviewer not assigned: {e}", |
| 205 | + "applied_label": label, |
| 206 | + } |
| 207 | + |
| 208 | + return { |
| 209 | + "status": "success", |
| 210 | + "applied_label": label, |
| 211 | + "assigned_reviewer": owner, |
| 212 | + } |
| 213 | + |
| 214 | + |
| 215 | +def add_comment_to_pr(pr_number: int, comment: str) -> dict[str, Any]: |
| 216 | + """Add the specified comment to the given PR number. |
| 217 | +
|
| 218 | + Args: |
| 219 | + pr_number: the number of the Github pull request |
| 220 | + comment: the comment to add |
| 221 | +
|
| 222 | + Returns: |
| 223 | + The the status of this request, with the applied comment when successful. |
| 224 | + """ |
| 225 | + print(f"Attempting to add comment '{comment}' to issue #{pr_number}") |
| 226 | + |
| 227 | + # Pull Request is a special issue in Github, so we can use issue url for PR. |
| 228 | + url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{pr_number}/comments" |
| 229 | + payload = {"body": comment} |
| 230 | + |
| 231 | + try: |
| 232 | + post_request(url, payload) |
| 233 | + except requests.exceptions.RequestException as e: |
| 234 | + return error_response(f"Error: {e}") |
| 235 | + return { |
| 236 | + "status": "success", |
| 237 | + "added_comment": comment, |
| 238 | + } |
| 239 | + |
| 240 | + |
| 241 | +root_agent = Agent( |
| 242 | + model="gemini-2.5-pro", |
| 243 | + name="adk_pr_triaging_assistant", |
| 244 | + description="Triage ADK pull requests.", |
| 245 | + instruction=f""" |
| 246 | + # 1. Identity |
| 247 | + You are a Pull Request (PR) triaging bot for the Github {REPO} repo with the owner {OWNER}. |
| 248 | +
|
| 249 | + # 2. Responsibilities |
| 250 | + Your core responsibility includes: |
| 251 | + - Get the pull request details. |
| 252 | + - Add a label to the pull request. |
| 253 | + - Assign a reviewer to the pull request. |
| 254 | + - Check if the pull request is following the contribution guidelines. |
| 255 | + - Add a comment to the pull request if it's not following the guidelines. |
| 256 | +
|
| 257 | + **IMPORTANT: {APPROVAL_INSTRUCTION}** |
| 258 | +
|
| 259 | + # 3. Guidelines & Rules |
| 260 | + Here are the rules for labeling: |
| 261 | + - If the PR is about documentations, label it with "documentation". |
| 262 | + - If it's about session, memory, artifacts services, label it with "services" |
| 263 | + - If it's about UI/web, label it with "web" |
| 264 | + - If it's related to tools, label it with "tools" |
| 265 | + - If it's about agent evalaution, then label it with "eval". |
| 266 | + - If it's about streaming/live, label it with "live". |
| 267 | + - If it's about model support(non-Gemini, like Litellm, Ollama, OpenAI models), label it with "models". |
| 268 | + - If it's about tracing, label it with "tracing". |
| 269 | + - If it's agent orchestration, agent definition, label it with "core". |
| 270 | + - If you can't find a appropriate labels for the PR, follow the previous instruction that starts with "IMPORTANT:". |
| 271 | +
|
| 272 | + Here is the contribution guidelines: |
| 273 | + `{CONTRIBUTING_MD}` |
| 274 | +
|
| 275 | + Here are the guidelines for checking if the PR is following the guidelines: |
| 276 | + - The "statusCheckRollup" in the pull request details may help you to identify if the PR is following some of the guidelines (e.g. CLA compliance). |
| 277 | +
|
| 278 | + Here are the guidelines for the comment: |
| 279 | + - **Be Polite and Helpful:** Start with a friendly tone. |
| 280 | + - **Be Specific:** Clearly list only the sections from the contribution guidelines that are still missing. |
| 281 | + - **Address the Author:** Mention the PR author by their username (e.g., `@username`). |
| 282 | + - **Provide Context:** Explain *why* the information or action is needed. |
| 283 | + - **Do not be repetitive:** If you have already commented on an PR asking for information, do not comment again unless new information has been added and it's still incomplete. |
| 284 | + - **Identify yourself:** Include a bolded note (e.g. "Response from ADK Triaging Agent") in your comment to indicate this comment was added by an ADK Answering Agent. |
| 285 | +
|
| 286 | + **Example Comment for a PR:** |
| 287 | + > **Response from ADK Triaging Agent** |
| 288 | + > |
| 289 | + > Hello @[pr-author-username], thank you for creating this PR! |
| 290 | + > |
| 291 | + > This PR is a bug fix, could you please associate the github issue with this PR? If there is no existing issue, could you please create one? |
| 292 | + > |
| 293 | + > In addition, could you please provide logs or screenshot after the fix is applied? |
| 294 | + > |
| 295 | + > This information will help reviewers to review your PR more efficiently. Thanks! |
| 296 | +
|
| 297 | + # 4. Steps |
| 298 | + When you are given a PR, here are the steps you should take: |
| 299 | + - Call the `get_pull_request_details` tool to get the details of the PR. |
| 300 | + - Skip the PR (i.e. do not label or comment) if the PR is closed or is labeled with "{BOT_LABEL}" or "google-contributior". |
| 301 | + - Check if the PR is following the contribution guidelines. |
| 302 | + - If it's not following the guidelines, recommend or add a comment to the PR that points to the contribution guidelines (https://github.com/google/adk-python/blob/main/CONTRIBUTING.md). |
| 303 | + - If it's following the guidelines, recommend or add a label to the PR. |
| 304 | +
|
| 305 | + # 5. Output |
| 306 | + Present the followings in an easy to read format highlighting PR number and your label. |
| 307 | + - The PR summary in a few sentence |
| 308 | + - The label you recommended or added with the justification |
| 309 | + - The owner of the label if you assigned a reviewer to the PR |
| 310 | + - The comment you recommended or added to the PR with the justification |
| 311 | + """, |
| 312 | + tools=[ |
| 313 | + get_pull_request_details, |
| 314 | + add_label_and_reviewer_to_pr, |
| 315 | + add_comment_to_pr, |
| 316 | + ], |
| 317 | +) |
0 commit comments