diff --git a/.github/workflows/pr-triage.yml b/.github/workflows/pr-triage.yml new file mode 100644 index 00000000..256af86d --- /dev/null +++ b/.github/workflows/pr-triage.yml @@ -0,0 +1,38 @@ +name: ADK Pull Request Triaging Agent + +on: + pull_request: + types: [opened, reopened, edited] + +jobs: + agent-triage-pull-request: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests google-adk + + - name: Run Triaging Script + env: + GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + GOOGLE_GENAI_USE_VERTEXAI: 0 + OWNER: 'google' + REPO: 'adk-python' + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + INTERACTIVE: ${{ secrets.PR_TRIAGE_INTERACTIVE }} + PYTHONPATH: contributing/samples + run: python -m adk_pr_triaging_agent.main diff --git a/contributing/samples/adk_pr_triaging_agent/README.md b/contributing/samples/adk_pr_triaging_agent/README.md new file mode 100644 index 00000000..f702f866 --- /dev/null +++ b/contributing/samples/adk_pr_triaging_agent/README.md @@ -0,0 +1,68 @@ +# ADK Pull Request Triaging Assistant + +The ADK Pull Request (PR) Triaging Assistant is a Python-based agent designed to help manage and triage GitHub pull requests for the `google/adk-python` repository. It uses a large language model to analyze new and unlabelled pull requests, recommend appropriate labels, assign a reviewer, and check contribution guides based on a predefined set of rules. + +This agent can be operated in two distinct modes: + +* an interactive mode for local use +* a fully automated GitHub Actions workflow. + +--- + +## Interactive Mode + +This mode allows you to run the agent locally to review its recommendations in real-time before any changes are made to your repository's pull requests. + +### Features +* **Web Interface**: The agent's interactive mode can be rendered in a web browser using the ADK's `adk web` command. +* **User Approval**: In interactive mode, the agent is instructed to ask for your confirmation before applying a label or posting a comment to a GitHub pull request. + +### Running in Interactive Mode +To run the agent in interactive mode, first set the required environment variables. Then, execute the following command in your terminal: + +```bash +adk web +``` +This will start a local server and provide a URL to access the agent's web interface in your browser. + +--- + +## GitHub Workflow Mode + +For automated, hands-off PR triaging, the agent can be integrated directly into your repository's CI/CD pipeline using a GitHub Actions workflow. + +### Workflow Triggers +The GitHub workflow is configured to run on specific triggers: + +* **Pull Request Events**: The workflow executes automatically whenever a new PR is `opened` or an existing one is `reopened` or `edited`. + +### Automated Labeling +When running as part of the GitHub workflow, the agent operates non-interactively. It identifies and applies the best label or posts a comment directly without requiring user approval. This behavior is configured by setting the `INTERACTIVE` environment variable to `0` in the workflow file. + +### Workflow Configuration +The workflow is defined in a YAML file (`.github/workflows/pr-triage.yml`). This file contains the steps to check out the code, set up the Python environment, install dependencies, and run the triaging script with the necessary environment variables and secrets. + +--- + +## Setup and Configuration + +Whether running in interactive or workflow mode, the agent requires the following setup. + +### Dependencies +The agent requires the following Python libraries. + +```bash +pip install --upgrade pip +pip install google-adk +``` + +### Environment Variables +The following environment variables are required for the agent to connect to the necessary services. + +* `GITHUB_TOKEN`: **(Required)** A GitHub Personal Access Token with `pull_requests:write` permissions. Needed for both interactive and workflow modes. +* `GOOGLE_API_KEY`: **(Required)** Your API key for the Gemini API. Needed for both interactive and workflow modes. +* `OWNER`: The GitHub organization or username that owns the repository (e.g., `google`). Needed for both modes. +* `REPO`: The name of the GitHub repository (e.g., `adk-python`). Needed for both modes. +* `INTERACTIVE`: Controls the agent's interaction mode. For the automated workflow, this is set to `0`. For interactive mode, it should be set to `1` or left unset. + +For local execution in interactive mode, you can place these variables in a `.env` file in the project's root directory. For the GitHub workflow, they should be configured as repository secrets. \ No newline at end of file diff --git a/contributing/samples/adk_pr_triaging_agent/main.py b/contributing/samples/adk_pr_triaging_agent/main.py new file mode 100644 index 00000000..da67fa16 --- /dev/null +++ b/contributing/samples/adk_pr_triaging_agent/main.py @@ -0,0 +1,65 @@ +# 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 asyncio +import time + +from adk_pr_triaging_agent import agent +from adk_pr_triaging_agent.settings import OWNER +from adk_pr_triaging_agent.settings import PULL_REQUEST_NUMBER +from adk_pr_triaging_agent.settings import REPO +from adk_pr_triaging_agent.utils import call_agent_async +from adk_pr_triaging_agent.utils import parse_number_string +from google.adk.runners import InMemoryRunner + +APP_NAME = "adk_pr_triaging_app" +USER_ID = "adk_pr_triaging_user" + + +async def main(): + 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 + ) + + pr_number = parse_number_string(PULL_REQUEST_NUMBER) + if not pr_number: + print( + f"Error: Invalid pull request number received: {PULL_REQUEST_NUMBER}." + ) + return + + prompt = f"Please triage pull request #{pr_number}!" + 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 triaging {OWNER}/{REPO} pull request #{PULL_REQUEST_NUMBER} 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( + "Triaging 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_pr_triaging_agent/settings.py b/contributing/samples/adk_pr_triaging_agent/settings.py index 9aff4cf5..1b2bb518 100644 --- a/contributing/samples/adk_pr_triaging_agent/settings.py +++ b/contributing/samples/adk_pr_triaging_agent/settings.py @@ -28,5 +28,6 @@ if not GITHUB_TOKEN: OWNER = os.getenv("OWNER", "google") REPO = os.getenv("REPO", "adk-python") BOT_LABEL = os.getenv("BOT_LABEL", "bot triaged") +PULL_REQUEST_NUMBER = os.getenv("PULL_REQUEST_NUMBER") IS_INTERACTIVE = os.environ.get("INTERACTIVE", "1").lower() in ["true", "1"] diff --git a/contributing/samples/adk_pr_triaging_agent/utils.py b/contributing/samples/adk_pr_triaging_agent/utils.py index e675c362..ebcfda9f 100644 --- a/contributing/samples/adk_pr_triaging_agent/utils.py +++ b/contributing/samples/adk_pr_triaging_agent/utils.py @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys from typing import Any from adk_pr_triaging_agent.settings import GITHUB_GRAPHQL_URL from adk_pr_triaging_agent.settings import GITHUB_TOKEN +from google.adk.agents.run_config import RunConfig +from google.adk.runners import Runner +from google.genai import types import requests headers = { @@ -75,3 +79,42 @@ def read_file(file_path: str) -> str: except FileNotFoundError: print(f"Error: File not found: {file_path}.") return "" + + +def parse_number_string(number_str: str | None, default_value: int = 0) -> int: + """Parse a number from the given string.""" + if not number_str: + return default_value + + try: + return int(number_str) + except ValueError: + print( + f"Warning: Invalid number string: {number_str}. Defaulting to" + f" {default_value}.", + file=sys.stderr, + ) + return default_value + + +async def call_agent_async( + runner: Runner, user_id: str, session_id: str, prompt: str +) -> str: + """Call the agent asynchronously with the user's prompt.""" + content = types.Content( + role="user", parts=[types.Part.from_text(text=prompt)] + ) + + final_response_text = "" + async for event in runner.run_async( + user_id=user_id, + session_id=session_id, + new_message=content, + run_config=RunConfig(save_input_blobs_as_artifacts=False), + ): + if event.content and event.content.parts: + if text := "".join(part.text or "" for part in event.content.parts): + if event.author != "user": + final_response_text += text + + return final_response_text