Skip to content

Commit 7eaceef

Browse files
Xuan Yangcopybara-github
authored andcommitted
chore: create an initial prototype agent to answer Github issue questions
This agent will post a comment to answer questions or provide more information according to the knowledge base. PiperOrigin-RevId: 778183040
1 parent 17d6042 commit 7eaceef

File tree

5 files changed

+340
-0
lines changed

5 files changed

+340
-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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 typing import Any
16+
17+
from adk_answering_agent.settings import GITHUB_BASE_URL
18+
from adk_answering_agent.settings import IS_INTERACTIVE
19+
from adk_answering_agent.settings import OWNER
20+
from adk_answering_agent.settings import REPO
21+
from adk_answering_agent.settings import VERTEXAI_DATASTORE_ID
22+
from adk_answering_agent.utils import error_response
23+
from adk_answering_agent.utils import get_request
24+
from adk_answering_agent.utils import post_request
25+
from google.adk.agents import Agent
26+
from google.adk.tools import VertexAiSearchTool
27+
import requests
28+
29+
APPROVAL_INSTRUCTION = (
30+
"**Do not** wait or ask for user approval or confirmation for adding the"
31+
" comment."
32+
)
33+
if IS_INTERACTIVE:
34+
APPROVAL_INSTRUCTION = (
35+
"Ask for user approval or confirmation for adding the comment."
36+
)
37+
38+
39+
def get_issue(issue_number: int) -> dict[str, Any]:
40+
"""Get the details of the specified issue number.
41+
42+
Args:
43+
issue_number: issue number of the Github issue.
44+
45+
Returns:
46+
The status of this request, with the issue details when successful.
47+
"""
48+
print(f"Attempting to get issue #{issue_number}")
49+
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}"
50+
try:
51+
response = get_request(url)
52+
except requests.exceptions.RequestException as e:
53+
return error_response(f"{e}")
54+
return {"status": "success", "issue": response}
55+
56+
57+
def add_comment_to_issue(issue_number: int, comment: str) -> dict[str, any]:
58+
"""Add the specified comment to the given issue number.
59+
60+
Args:
61+
issue_number: issue number of the Github issue
62+
comment: comment to add
63+
64+
Returns:
65+
The the status of this request, with the applied comment when successful.
66+
"""
67+
print(f"Attempting to add comment '{comment}' to issue #{issue_number}")
68+
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/comments"
69+
payload = {"body": comment}
70+
71+
try:
72+
response = post_request(url, payload)
73+
except requests.exceptions.RequestException as e:
74+
return error_response(f"{e}")
75+
return {
76+
"status": "success",
77+
"added_comment": response,
78+
}
79+
80+
81+
def list_comments_on_issue(issue_number: int) -> dict[str, any]:
82+
"""List all comments on the given issue number.
83+
84+
Args:
85+
issue_number: issue number of the Github issue
86+
87+
Returns:
88+
The the status of this request, with the list of comments when successful.
89+
"""
90+
print(f"Attempting to list comments on issue #{issue_number}")
91+
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/comments"
92+
93+
try:
94+
response = get_request(url)
95+
except requests.exceptions.RequestException as e:
96+
return error_response(f"{e}")
97+
return {"status": "success", "comments": response}
98+
99+
100+
root_agent = Agent(
101+
model="gemini-2.5-pro",
102+
name="adk_repo_answering_agent",
103+
description="Answer questions about ADK repo.",
104+
instruction=f"""
105+
You are a helpful assistant that responds to questions from the GitHub repository `{OWNER}/{REPO}`
106+
based on information about Google ADK found in the document store: {VERTEXAI_DATASTORE_ID}.
107+
108+
When user specifies a issue number, here are the steps:
109+
1. Use the `get_issue` tool to get the details of the issue.
110+
* If the issue is closed, do not respond.
111+
2. Use the `list_comments_on_issue` tool to list all comments on the issue.
112+
3. Focus on the latest comment but referece all comments if needed to understand the context.
113+
* If there is no comment at all, just focus on the issue title and body.
114+
4. If all the following conditions are met, try toadd a comment to the issue, otherwise, do not respond:
115+
* The latest comment is from the issue reporter.
116+
* The latest comment is not from you or other agents (marked as "Response from XXX Agent").
117+
* The latest comment is asking a question or requesting information.
118+
* The issue is not about a feature request.
119+
5. Use the `VertexAiSearchTool` to find relevant information before answering.
120+
121+
IMPORTANT:
122+
* {APPROVAL_INSTRUCTION}
123+
* If you can't find the answer or information in the document store, **do not** respond.
124+
* Include a bolded note (e.g. "Response from ADK Oncall Agent") in your comment
125+
to indicate this comment was added by an ADK Oncall agent.
126+
* Do not respond to any other issue except the one specified by the user.
127+
* Please include your justification for your decision in your output
128+
to the user who is telling with you.
129+
* If you uses citation from the document store, please provide a footnote
130+
referencing the source document format it as: "[1] URL of the document".
131+
* Replace the "gs://prefix/" part, e.g. "gs://adk-qa-bucket/", to be "https://github.com/google/"
132+
* Add "blob/main/" after the repo name, e.g. "adk-python", "adk-docs", for example:
133+
* If the original URL is "gs://adk-qa-bucket/adk-python/src/google/adk/version.py",
134+
then the citation URL is "https://github.com/google/adk-python/blob/main/src/google/adk/version.py"
135+
* If the original URL is "gs://adk-qa-bucket/adk-docs/docs/index.md",
136+
then the citation URL is "https://github.com/google/adk-docs/blob/main/docs/index.md"
137+
* If the file is a html file, replace the ".html" to be ".md"
138+
""",
139+
tools=[
140+
VertexAiSearchTool(data_store_id=VERTEXAI_DATASTORE_ID),
141+
get_issue,
142+
add_comment_to_issue,
143+
list_comments_on_issue,
144+
],
145+
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 asyncio
16+
import time
17+
18+
from adk_answering_agent import agent
19+
from adk_answering_agent.settings import ISSUE_NUMBER
20+
from adk_answering_agent.settings import OWNER
21+
from adk_answering_agent.settings import REPO
22+
from adk_answering_agent.utils import parse_number_string
23+
from google.adk.agents.run_config import RunConfig
24+
from google.adk.runners import InMemoryRunner
25+
from google.adk.runners import Runner
26+
from google.genai import types
27+
28+
APP_NAME = "adk_answering_app"
29+
USER_ID = "adk_answering_user"
30+
31+
32+
async def call_agent_async(
33+
runner: Runner, user_id: str, session_id: str, prompt: str
34+
) -> str:
35+
"""Call the agent asynchronously with the user's prompt."""
36+
content = types.Content(
37+
role="user", parts=[types.Part.from_text(text=prompt)]
38+
)
39+
40+
final_response_text = ""
41+
async for event in runner.run_async(
42+
user_id=user_id,
43+
session_id=session_id,
44+
new_message=content,
45+
run_config=RunConfig(save_input_blobs_as_artifacts=False),
46+
):
47+
if event.content and event.content.parts:
48+
if text := "".join(part.text or "" for part in event.content.parts):
49+
print(f"** {event.author} (ADK): {text}")
50+
if event.author == agent.root_agent.name:
51+
final_response_text += text
52+
53+
return final_response_text
54+
55+
56+
async def main():
57+
runner = InMemoryRunner(
58+
agent=agent.root_agent,
59+
app_name=APP_NAME,
60+
)
61+
session = await runner.session_service.create_session(
62+
app_name=APP_NAME, user_id=USER_ID
63+
)
64+
65+
issue_number = parse_number_string(ISSUE_NUMBER)
66+
if not issue_number:
67+
print(f"Error: Invalid issue number received: {ISSUE_NUMBER}.")
68+
return
69+
70+
prompt = (
71+
f"Please check issue #{issue_number} see if you can help answer the"
72+
" question or provide some information!"
73+
)
74+
response = await call_agent_async(runner, USER_ID, session.id, prompt)
75+
print(f"<<<< Agent Final Output: {response}\n")
76+
77+
78+
if __name__ == "__main__":
79+
start_time = time.time()
80+
print(
81+
f"Start Q&A checking on {OWNER}/{REPO} issue #{ISSUE_NUMBER} at"
82+
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start_time))}"
83+
)
84+
print("-" * 80)
85+
asyncio.run(main())
86+
print("-" * 80)
87+
end_time = time.time()
88+
print(
89+
"Q&A checking finished at"
90+
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(end_time))}",
91+
)
92+
print("Total script execution time:", f"{end_time - start_time:.2f} seconds")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
23+
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
24+
if not GITHUB_TOKEN:
25+
raise ValueError("GITHUB_TOKEN environment variable not set")
26+
27+
VERTEXAI_DATASTORE_ID = os.environ.get("VERTEXAI_DATASTORE_ID")
28+
29+
OWNER = os.getenv("OWNER", "google")
30+
REPO = os.getenv("REPO", "adk-python")
31+
ISSUE_NUMBER = os.getenv("ISSUE_NUMBER")
32+
33+
IS_INTERACTIVE = os.environ.get("INTERACTIVE", "1").lower() in ["true", "1"]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 typing import Any
16+
17+
from adk_answering_agent.settings import GITHUB_TOKEN
18+
import requests
19+
20+
headers = {
21+
"Authorization": f"token {GITHUB_TOKEN}",
22+
"Accept": "application/vnd.github.v3+json",
23+
}
24+
25+
26+
def get_request(
27+
url: str, params: dict[str, Any] | None = None
28+
) -> dict[str, Any]:
29+
if params is None:
30+
params = {}
31+
response = requests.get(url, headers=headers, params=params, timeout=60)
32+
response.raise_for_status()
33+
return response.json()
34+
35+
36+
def post_request(url: str, payload: Any) -> dict[str, Any]:
37+
response = requests.post(url, headers=headers, json=payload, timeout=60)
38+
response.raise_for_status()
39+
return response.json()
40+
41+
42+
def error_response(error_message: str) -> dict[str, Any]:
43+
return {"status": "error", "error_message": error_message}
44+
45+
46+
def parse_number_string(number_str: str, default_value: int = 0) -> int:
47+
"""Parse a number from the given string."""
48+
try:
49+
return int(number_str)
50+
except ValueError:
51+
print(
52+
f"Warning: Invalid number string: {number_str}. Defaulting to"
53+
f" {default_value}."
54+
)
55+
return default_value

0 commit comments

Comments
 (0)