Skip to content

Commit 5eff66a

Browse files
xuanyang15copybara-github
authored andcommitted
chore: create an initial prototype agent to triage pull requests
This agent will post a comment if the PR is not following our contribution guides or add a label and reviewer for the PR if it passes the guide check. PiperOrigin-RevId: 788511767
1 parent 282d67f commit 5eff66a

File tree

4 files changed

+441
-0
lines changed

4 files changed

+441
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 . import agent
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
import os
16+
17+
from dotenv import load_dotenv
18+
19+
load_dotenv(override=True)
20+
21+
GITHUB_BASE_URL = "https://api.github.com"
22+
GITHUB_GRAPHQL_URL = GITHUB_BASE_URL + "/graphql"
23+
24+
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
25+
if not GITHUB_TOKEN:
26+
raise ValueError("GITHUB_TOKEN environment variable not set")
27+
28+
OWNER = os.getenv("OWNER", "google")
29+
REPO = os.getenv("REPO", "adk-python")
30+
BOT_LABEL = os.getenv("BOT_LABEL", "bot triaged")
31+
32+
IS_INTERACTIVE = os.environ.get("INTERACTIVE", "1").lower() in ["true", "1"]

0 commit comments

Comments
 (0)