From 408d3dfeb1475da343a15ae13e9b128985460a5d Mon Sep 17 00:00:00 2001 From: "Xiang (Sean) Zhou" Date: Tue, 2 Sep 2025 14:24:55 -0700 Subject: [PATCH] chore: refactor discussion answering agent to merge answer_discussions.py into main.py mainly to dedup duplicated functionality in both file. PiperOrigin-RevId: 802291102 --- .github/workflows/discussion_answering.yml | 3 +- .../samples/adk_answering_agent/README.md | 22 ++- .../adk_answering_agent/answer_discussions.py | 172 ------------------ .../samples/adk_answering_agent/main.py | 143 +++++++++++++-- 4 files changed, 141 insertions(+), 199 deletions(-) delete mode 100644 contributing/samples/adk_answering_agent/answer_discussions.py diff --git a/.github/workflows/discussion_answering.yml b/.github/workflows/discussion_answering.yml index 5326dc9c..fbcda9ef 100644 --- a/.github/workflows/discussion_answering.yml +++ b/.github/workflows/discussion_answering.yml @@ -41,6 +41,5 @@ jobs: OWNER: 'google' REPO: 'adk-python' INTERACTIVE: 0 - DISCUSSION_NUMBER: ${{ github.event.discussion.number }} PYTHONPATH: contributing/samples - run: python -m adk_answering_agent.main + run: python -m adk_answering_agent.main --discussion_number ${{ github.event.discussion.number }} diff --git a/contributing/samples/adk_answering_agent/README.md b/contributing/samples/adk_answering_agent/README.md index 158c825a..d55f7f78 100644 --- a/contributing/samples/adk_answering_agent/README.md +++ b/contributing/samples/adk_answering_agent/README.md @@ -6,7 +6,7 @@ This agent can be operated in three distinct modes: - An interactive mode for local use. - A batch script mode for oncall use. -- A fully automated GitHub Actions workflow (TBD). +- A fully automated GitHub Actions workflow. --- @@ -31,26 +31,30 @@ This will start a local server and provide a URL to access the agent's web inter ## Batch Script Mode -The `answer_discussions.py` is created for ADK oncall team to batch process discussions. +The `main.py` script supports batch processing for ADK oncall team to process discussions. ### Features -* **Batch Process**: Taken either a number as the count of the recent discussions or a list of discussion numbers, the script will invoke the agent to answer all the specified discussions in one single run. +* **Single Discussion**: Process a specific discussion by providing its number. +* **Batch Process**: Process the N most recently updated discussions. -### Running in Interactive Mode -To run the agent in batch script mode, first set the required environment variables. Then, execute the following command in your terminal: +### Running in Batch Script Mode +To run the agent in batch script mode, first set the required environment variables. Then, execute one of the following commands: ```bash export PYTHONPATH=contributing/samples -python -m adk_answering_agent.answer_discussions --numbers 27 36 # Answer specified discussions -``` -Or `python -m adk_answering_agent.answer_discussions --recent 10` to answer the 10 most recent updated discussions. +# Answer a specific discussion +python -m adk_answering_agent.main --discussion_number 27 + +# Answer the 10 most recent updated discussions +python -m adk_answering_agent.main --recent 10 +``` --- ## GitHub Workflow Mode -The `main.py` is reserved for the Github Workflow. The detailed setup for the automatic workflow is TBD. +The `main.py` script is automatically triggered by GitHub Actions when new discussions are created in the Q&A category. The workflow is configured in `.github/workflows/discussion_answering.yml` and automatically processes discussions using the `--discussion_number` flag. --- diff --git a/contributing/samples/adk_answering_agent/answer_discussions.py b/contributing/samples/adk_answering_agent/answer_discussions.py deleted file mode 100644 index 1aa73758..00000000 --- a/contributing/samples/adk_answering_agent/answer_discussions.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import asyncio -import sys -import time - -from adk_answering_agent import agent -from adk_answering_agent.settings import OWNER -from adk_answering_agent.settings import REPO -from adk_answering_agent.utils import call_agent_async -from adk_answering_agent.utils import run_graphql_query -from google.adk.runners import InMemoryRunner -import requests - -APP_NAME = "adk_discussion_answering_app" -USER_ID = "adk_discussion_answering_assistant" - - -async def list_most_recent_discussions(count: int = 1) -> list[int] | None: - """Fetches a specified number of the most recently updated discussions. - - Args: - count: The number of discussions to retrieve. Defaults to 1. - - Returns: - A list of discussion numbers. - """ - print( - f"Attempting to fetch the {count} most recently updated discussions from" - f" {OWNER}/{REPO}..." - ) - - query = """ - query($owner: String!, $repo: String!, $count: Int!) { - repository(owner: $owner, name: $repo) { - discussions( - first: $count - orderBy: {field: UPDATED_AT, direction: DESC} - ) { - nodes { - title - number - updatedAt - author { - login - } - } - } - } - } - """ - variables = {"owner": OWNER, "repo": REPO, "count": count} - - try: - response = run_graphql_query(query, variables) - - if "errors" in response: - print(f"Error from GitHub API: {response['errors']}", file=sys.stderr) - return None - - discussions = ( - response.get("data", {}) - .get("repository", {}) - .get("discussions", {}) - .get("nodes", []) - ) - return [d["number"] for d in discussions] - - except requests.exceptions.RequestException as e: - print(f"Request failed: {e}", file=sys.stderr) - return None - - -def process_arguments(): - """Parses command-line arguments.""" - parser = argparse.ArgumentParser( - description="A script that answer questions for Github discussions.", - epilog=( - "Example usage: \n" - "\tpython -m adk_answering_agent.answer_discussions --recent 10\n" - "\tpython -m adk_answering_agent.answer_discussions --numbers 21 31\n" - ), - formatter_class=argparse.RawTextHelpFormatter, - ) - - group = parser.add_mutually_exclusive_group(required=True) - - group.add_argument( - "--recent", - type=int, - metavar="COUNT", - help="Answer the N most recently updated discussion numbers.", - ) - - group.add_argument( - "--numbers", - type=int, - nargs="+", - metavar="NUM", - help="Answer a specific list of discussion numbers.", - ) - - if len(sys.argv) == 1: - parser.print_help(sys.stderr) - sys.exit(1) - - return parser.parse_args() - - -async def main(): - args = process_arguments() - discussion_numbers = [] - - if args.recent: - discussion_numbers = await list_most_recent_discussions(count=args.recent) - elif args.numbers: - discussion_numbers = args.numbers - - if not discussion_numbers: - print("No discussions specified. Exiting...", file=sys.stderr) - sys.exit(1) - - print(f"Will try to answer discussions: {discussion_numbers}...") - - runner = InMemoryRunner( - agent=agent.root_agent, - app_name=APP_NAME, - ) - - for discussion_number in discussion_numbers: - print("#" * 80) - print(f"Starting to process discussion #{discussion_number}...") - # Create a new session for each discussion to avoid interference. - session = await runner.session_service.create_session( - app_name=APP_NAME, user_id=USER_ID - ) - prompt = ( - f"Please check discussion #{discussion_number} see if you can help" - " answer the question or provide some information!" - ) - response = await call_agent_async(runner, USER_ID, session.id, prompt) - print(f"<<<< Agent Final Output: {response}\n") - - -if __name__ == "__main__": - start_time = time.time() - print( - f"Start answering discussions for {OWNER}/{REPO} at" - f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start_time))}" - ) - print("-" * 80) - asyncio.run(main()) - print("-" * 80) - end_time = time.time() - print( - "Discussion answering finished at" - f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(end_time))}", - ) - print("Total script execution time:", f"{end_time - start_time:.2f} seconds") diff --git a/contributing/samples/adk_answering_agent/main.py b/contributing/samples/adk_answering_agent/main.py index bb1d7032..de44a71b 100644 --- a/contributing/samples/adk_answering_agent/main.py +++ b/contributing/samples/adk_answering_agent/main.py @@ -12,18 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse import asyncio import logging +import os +import sys import time from adk_answering_agent import agent -from adk_answering_agent.settings import DISCUSSION_NUMBER from adk_answering_agent.settings import OWNER from adk_answering_agent.settings import REPO from adk_answering_agent.utils import call_agent_async from adk_answering_agent.utils import parse_number_string +from adk_answering_agent.utils import run_graphql_query from google.adk.cli.utils import logs from google.adk.runners import InMemoryRunner +import requests APP_NAME = "adk_answering_app" USER_ID = "adk_answering_user" @@ -31,32 +35,139 @@ USER_ID = "adk_answering_user" logs.setup_adk_logger(level=logging.DEBUG) +async def list_most_recent_discussions(count: int = 1) -> list[int] | None: + """Fetches a specified number of the most recently updated discussions. + + Args: + count: The number of discussions to retrieve. Defaults to 1. + + Returns: + A list of discussion numbers. + """ + print( + f"Attempting to fetch the {count} most recently updated discussions from" + f" {OWNER}/{REPO}..." + ) + + query = """ + query($owner: String!, $repo: String!, $count: Int!) { + repository(owner: $owner, name: $repo) { + discussions( + first: $count + orderBy: {field: UPDATED_AT, direction: DESC} + ) { + nodes { + title + number + updatedAt + author { + login + } + } + } + } + } + """ + variables = {"owner": OWNER, "repo": REPO, "count": count} + + try: + response = run_graphql_query(query, variables) + + if "errors" in response: + print(f"Error from GitHub API: {response['errors']}", file=sys.stderr) + return None + + discussions = ( + response.get("data", {}) + .get("repository", {}) + .get("discussions", {}) + .get("nodes", []) + ) + return [d["number"] for d in discussions] + + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}", file=sys.stderr) + return None + + +def process_arguments(): + """Parses command-line arguments.""" + parser = argparse.ArgumentParser( + description="A script that answers questions for GitHub discussions.", + epilog=( + "Example usage: \n" + "\tpython -m adk_answering_agent.main --recent 10\n" + "\tpython -m adk_answering_agent.main --discussion_number 21\n" + ), + formatter_class=argparse.RawTextHelpFormatter, + ) + + group = parser.add_mutually_exclusive_group(required=True) + + group.add_argument( + "--recent", + type=int, + metavar="COUNT", + help="Answer the N most recently updated discussion numbers.", + ) + + group.add_argument( + "--discussion_number", + type=str, + metavar="NUM", + help="Answer a specific discussion number.", + ) + + return parser.parse_args() + + async def main(): + args = process_arguments() + discussion_numbers = [] + + if args.recent: + fetched_numbers = await list_most_recent_discussions(count=args.recent) + if not fetched_numbers: + print("No discussions found. Exiting...", file=sys.stderr) + return + discussion_numbers = fetched_numbers + elif args.discussion_number: + discussion_number = parse_number_string(args.discussion_number) + if not discussion_number: + print( + "Error: Invalid discussion number received:" + f" {args.discussion_number}." + ) + return + discussion_numbers = [discussion_number] + + print(f"Will try to answer discussions: {discussion_numbers}...") + runner = InMemoryRunner( agent=agent.root_agent, app_name=APP_NAME, ) - session = await runner.session_service.create_session( - app_name=APP_NAME, user_id=USER_ID - ) - discussion_number = parse_number_string(DISCUSSION_NUMBER) - if not discussion_number: - print(f"Error: Invalid discussion number received: {DISCUSSION_NUMBER}.") - return - - prompt = ( - f"Please check discussion #{discussion_number} see if you can help answer" - " the question or provide some information!" - ) - response = await call_agent_async(runner, USER_ID, session.id, prompt) - print(f"<<<< Agent Final Output: {response}\n") + for discussion_number in discussion_numbers: + if len(discussion_numbers) > 1: + print("#" * 80) + print(f"Starting to process discussion #{discussion_number}...") + # Create a new session for each discussion to avoid interference. + session = await runner.session_service.create_session( + app_name=APP_NAME, user_id=USER_ID + ) + prompt = ( + f"Please check discussion #{discussion_number} see if you can help" + " answer the question or provide some information!" + ) + response = await call_agent_async(runner, USER_ID, session.id, prompt) + print(f"<<<< Agent Final Output: {response}\n") if __name__ == "__main__": start_time = time.time() print( - f"Start Q&A checking on {OWNER}/{REPO} discussion #{DISCUSSION_NUMBER} at" + f"Start Q&A checking on {OWNER}/{REPO} at" f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start_time))}" ) print("-" * 80)