docs: Update ADK issue triaging agent to add component label before planned

Co-authored-by: Xuan Yang <xygoogle@google.com>
PiperOrigin-RevId: 839391092
This commit is contained in:
Xuan Yang
2025-12-02 12:54:44 -08:00
committed by Copybara-Service
parent 77401132d1
commit 3aef9a18b1
4 changed files with 174 additions and 63 deletions
+12 -6
View File
@@ -2,16 +2,22 @@ name: ADK Issue Triaging Agent
on:
issues:
types: [labeled]
types: [opened, labeled]
schedule:
# Run every 6 hours to triage planned but not triaged issues
# Run every 6 hours to triage untriaged issues
- cron: '0 */6 * * *'
jobs:
agent-triage-issues:
runs-on: ubuntu-latest
# Only run if labeled with "planned" or if it's a scheduled run
if: github.event_name == 'schedule' || github.event.label.name == 'planned'
# Run for:
# - Scheduled runs (batch processing)
# - New issues (need component labeling)
# - Issues labeled with "planned" (need owner assignment)
if: >-
github.event_name == 'schedule' ||
github.event.action == 'opened' ||
github.event.label.name == 'planned'
permissions:
issues: write
contents: read
@@ -35,8 +41,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GOOGLE_GENAI_USE_VERTEXAI: 0
OWNER: 'google'
REPO: 'adk-python'
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
INTERACTIVE: 0
EVENT_NAME: ${{ github.event_name }} # 'issues', 'schedule', etc.
ISSUE_NUMBER: ${{ github.event.issue.number }}
@@ -1,18 +1,39 @@
# ADK Issue Triaging Assistant
The ADK Issue Triaging Assistant is a Python-based agent designed to help manage and triage GitHub issues for the `google/adk-python` repository. It uses a large language model to analyze new and unlabelled issues, recommend appropriate labels based on a predefined set of rules, and apply them.
The ADK Issue Triaging Assistant is a Python-based agent designed to help manage and triage GitHub issues for the `google/adk-python` repository. It uses a large language model to analyze issues, recommend appropriate component labels, set issue types, and assign owners based on predefined rules.
This agent can be operated in two distinct modes: an interactive mode for local use or as a fully automated GitHub Actions workflow.
---
## Triaging Workflow
The agent performs different actions based on the issue state:
| Condition | Actions |
|-----------|---------|
| Issue without component label | Add component label + Set issue type (Bug/Feature) |
| Issue with "planned" label but no assignee | Assign owner based on component label |
| Issue with "planned" label AND no component label | Add component label + Set type + Assign owner |
### Component Labels
The agent can assign the following component labels, each mapped to an owner:
- `core`, `tools`, `mcp`, `eval`, `live`, `models`, `tracing`, `web`, `services`, `documentation`, `question`, `agent engine`, `a2a`, `bq`
### Issue Types
Based on the issue content, the agent will set the issue type to:
- **Bug**: For bug reports
- **Feature**: For feature requests
---
## 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 issues.
### 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 to a GitHub issue.
* **User Approval**: In interactive mode, the agent is instructed to ask for your confirmation before applying labels or assigning owners.
### 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:
@@ -31,12 +52,19 @@ For automated, hands-off issue triaging, the agent can be integrated directly in
### Workflow Triggers
The GitHub workflow is configured to run on specific triggers:
1. **Issue Events**: The workflow executes automatically whenever a new issue is `opened` or an existing one is `reopened`.
1. **New Issues (`opened`)**: When a new issue is created, the agent adds an appropriate component label and sets the issue type.
2. **Scheduled Runs**: The workflow also runs on a recurring schedule (every 6 hours) to process any unlabelled issues that may have been missed.
2. **Planned Label Added (`labeled` with "planned")**: When an issue is labeled as "planned", the agent assigns an owner based on the component label. If the issue doesn't have a component label yet, the agent will also add one.
### Automated Labeling
When running as part of the GitHub workflow, the agent operates non-interactively. It identifies the best label and applies it directly without requiring user approval. This behavior is configured by setting the `INTERACTIVE` environment variable to `0` in the workflow file.
3. **Scheduled Runs**: The workflow runs every 6 hours to process any issues that need triaging (either missing component labels or missing assignees for "planned" issues).
### Automated Actions
When running as part of the GitHub workflow, the agent operates non-interactively:
- **Component Labeling**: Automatically applies the most appropriate component label
- **Issue Type Setting**: Sets the issue type to Bug or Feature based on content
- **Owner Assignment**: Only assigns owners for issues marked as "planned"
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/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.
@@ -60,8 +88,8 @@ The following environment variables are required for the agent to connect to the
* `GITHUB_TOKEN`: **(Required)** A GitHub Personal Access Token with `issues: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.
* `OWNER`: The GitHub organization or username that owns the repository (e.g., `google`). In the workflow, this is automatically set from the repository context.
* `REPO`: The name of the GitHub repository (e.g., `adk-python`). In the workflow, this is automatically set from the repository context.
* `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.
@@ -78,22 +78,27 @@ if IS_INTERACTIVE:
APPROVAL_INSTRUCTION = "Only label them when the user approves the labeling!"
def list_planned_untriaged_issues(issue_count: int) -> dict[str, Any]:
"""List planned issues without component labels (e.g., core, tools, etc.).
def list_untriaged_issues(issue_count: int) -> dict[str, Any]:
"""List open issues that need triaging.
Returns issues that need any of the following actions:
1. Issues without component labels (need labeling + type setting)
2. Issues with 'planned' label but no assignee (need owner assignment)
Args:
issue_count: number of issues to return
Returns:
The status of this request, with a list of issues when successful.
Each issue includes flags indicating what actions are needed.
"""
url = f"{GITHUB_BASE_URL}/search/issues"
query = f"repo:{OWNER}/{REPO} is:open is:issue label:planned"
query = f"repo:{OWNER}/{REPO} is:open is:issue"
params = {
"q": query,
"sort": "created",
"order": "desc",
"per_page": issue_count,
"per_page": 100, # Fetch more to filter
"page": 1,
}
@@ -103,29 +108,46 @@ def list_planned_untriaged_issues(issue_count: int) -> dict[str, Any]:
return error_response(f"Error: {e}")
issues = response.get("items", [])
# Filter out issues that already have component labels
component_labels = set(LABEL_TO_OWNER.keys())
untriaged_issues = []
for issue in issues:
issue_labels = {label["name"] for label in issue.get("labels", [])}
# If the issue only has "planned" but no component labels, it's untriaged
if not (issue_labels & component_labels):
assignees = issue.get("assignees", [])
existing_component_labels = issue_labels & component_labels
has_component = bool(existing_component_labels)
has_planned = "planned" in issue_labels
# Determine what actions are needed
needs_component_label = not has_component
needs_owner = has_planned and not assignees
# Include issue if it needs any action
if needs_component_label or needs_owner:
issue["has_planned_label"] = has_planned
issue["has_component_label"] = has_component
issue["existing_component_label"] = (
list(existing_component_labels)[0]
if existing_component_labels
else None
)
issue["needs_component_label"] = needs_component_label
issue["needs_owner"] = needs_owner
untriaged_issues.append(issue)
if len(untriaged_issues) >= issue_count:
break
return {"status": "success", "issues": untriaged_issues}
def add_label_and_owner_to_issue(
issue_number: int, label: str
) -> dict[str, Any]:
"""Add the specified label and owner to the given issue number.
def add_label_to_issue(issue_number: int, label: str) -> dict[str, Any]:
"""Add the specified component label to the given issue number.
Args:
issue_number: issue number of the GitHub issue.
label: label to assign
Returns:
The the status of this request, with the applied label and assigned owner
when successful.
The status of this request, with the applied label when successful.
"""
print(f"Attempting to add label '{label}' to issue #{issue_number}")
if label not in LABEL_TO_OWNER:
@@ -143,15 +165,38 @@ def add_label_and_owner_to_issue(
except requests.exceptions.RequestException as e:
return error_response(f"Error: {e}")
return {
"status": "success",
"message": response,
"applied_label": label,
}
def add_owner_to_issue(issue_number: int, label: str) -> dict[str, Any]:
"""Assign an owner to the issue based on the component label.
This should only be called for issues that have the 'planned' label.
Args:
issue_number: issue number of the GitHub issue.
label: component label that determines the owner to assign
Returns:
The status of this request, with the assigned owner when successful.
"""
print(
f"Attempting to assign owner for label '{label}' to issue #{issue_number}"
)
if label not in LABEL_TO_OWNER:
return error_response(
f"Error: Label '{label}' is not a valid component label."
)
owner = LABEL_TO_OWNER.get(label, None)
if not owner:
return {
"status": "warning",
"message": (
f"{response}\n\nLabel '{label}' does not have an owner. Will not"
" assign."
),
"applied_label": label,
"message": f"Label '{label}' does not have an owner. Will not assign.",
}
assignee_url = (
@@ -167,7 +212,6 @@ def add_label_and_owner_to_issue(
return {
"status": "success",
"message": response,
"applied_label": label,
"assigned_owner": owner,
}
@@ -223,29 +267,46 @@ root_agent = Agent(
- If it's about BigQuery integrations, label it with "bq".
- If you can't find an appropriate labels for the issue, follow the previous instruction that starts with "IMPORTANT:".
Call the `add_label_and_owner_to_issue` tool to label the issue, which will also assign the issue to the owner of the label.
## Triaging Workflow
After you label the issue, call the `change_issue_type` tool to change the issue type:
- If the issue is a bug report, change the issue type to "Bug".
- If the issue is a feature request, change the issue type to "Feature".
- Otherwise, **do not change the issue type**.
Each issue will have flags indicating what actions are needed:
- `needs_component_label`: true if the issue needs a component label
- `needs_owner`: true if the issue needs an owner assigned (has 'planned' label but no assignee)
For each issue, perform ONLY the required actions based on the flags:
1. **If `needs_component_label` is true**:
- Use `add_label_to_issue` to add the appropriate component label
- Use `change_issue_type` to set the issue type:
- Bug report → "Bug"
- Feature request → "Feature"
- Otherwise → do not change the issue type
2. **If `needs_owner` is true**:
- Use `add_owner_to_issue` to assign an owner based on the component label
- Note: If the issue already has a component label (`has_component_label: true`), use that existing label to determine the owner
Do NOT add a component label if `needs_component_label` is false.
Do NOT assign an owner if `needs_owner` is false.
Response quality requirements:
- Summarize the issue in your own words without leaving template
placeholders (never output text like "[fill in later]").
- Justify the chosen label with a short explanation referencing the issue
details.
- Mention the assigned owner when a label maps to one.
- Mention the assigned owner only when you actually assign one (i.e., when
the issue has the 'planned' label).
- If no label is applied, clearly state why.
Present the following in an easy to read format highlighting issue number and your label.
- the issue summary in a few sentence
- your label recommendation and justification
- the owner of the label if you assign the issue to an owner
- the owner of the label if you assign the issue to an owner (only for planned issues)
""",
tools=[
list_planned_untriaged_issues,
add_label_and_owner_to_issue,
list_untriaged_issues,
add_label_to_issue,
add_owner_to_issue,
change_issue_type,
],
)
+37 -21
View File
@@ -46,24 +46,41 @@ async def fetch_specific_issue_details(issue_number: int):
issue_data = get_request(url)
labels = issue_data.get("labels", [])
label_names = {label["name"] for label in labels}
assignees = issue_data.get("assignees", [])
# Check if issue has "planned" label but no component labels
# Check issue state
component_labels = set(LABEL_TO_OWNER.keys())
has_planned = "planned" in label_names
has_component = bool(label_names & component_labels)
existing_component_labels = label_names & component_labels
has_component = bool(existing_component_labels)
has_assignee = len(assignees) > 0
if has_planned and not has_component:
print(f"Issue #{issue_number} is planned but not triaged. Proceeding.")
# Determine what actions are needed
needs_component_label = not has_component
needs_owner = has_planned and not has_assignee
if needs_component_label or needs_owner:
print(
f"Issue #{issue_number} needs triaging. "
f"needs_component_label={needs_component_label}, "
f"needs_owner={needs_owner}"
)
return {
"number": issue_data["number"],
"title": issue_data["title"],
"body": issue_data.get("body", ""),
"has_planned_label": has_planned,
"has_component_label": has_component,
"existing_component_label": (
list(existing_component_labels)[0]
if existing_component_labels
else None
),
"needs_component_label": needs_component_label,
"needs_owner": needs_owner,
}
else:
print(
f"Issue #{issue_number} is already triaged or doesn't have"
" 'planned' label. Skipping."
)
print(f"Issue #{issue_number} is already fully triaged. Skipping.")
return None
except requests.exceptions.RequestException as e:
print(f"Error fetching issue #{issue_number}: {e}")
@@ -127,25 +144,24 @@ async def main():
issue_title = ISSUE_TITLE or specific_issue["title"]
issue_body = ISSUE_BODY or specific_issue["body"]
needs_component_label = specific_issue.get("needs_component_label", True)
needs_owner = specific_issue.get("needs_owner", False)
existing_component_label = specific_issue.get("existing_component_label")
prompt = (
f"A GitHub issue #{issue_number} has been labeled as 'planned'."
f' Title: "{issue_title}"\nBody:'
f' "{issue_body}"\n\nBased on the rules, recommend an'
" appropriate component label and its justification."
" Then, use the 'add_label_and_owner_to_issue' tool to apply the"
" label directly to this issue. Only label it, do not"
" process any other issues."
f"Triage GitHub issue #{issue_number}.\n\n"
f'Title: "{issue_title}"\n'
f'Body: "{issue_body}"\n\n'
f"Issue state: needs_component_label={needs_component_label}, "
f"needs_owner={needs_owner}, "
f"existing_component_label={existing_component_label}"
)
else:
print(f"EVENT: Processing batch of issues (event: {EVENT_NAME}).")
issue_count = parse_number_string(ISSUE_COUNT_TO_PROCESS, default_value=3)
prompt = (
"Please use the 'list_planned_untriaged_issues' tool to find the"
f" most recent {issue_count} planned issues that haven't been"
" triaged yet (i.e., issues with 'planned' label but no component"
" labels like 'core', 'tools', etc.). Then triage each of them by"
" applying appropriate component labels. If you cannot find any planned"
" issues, please don't try to triage any issues."
f"Please use 'list_untriaged_issues' to find {issue_count} issues that"
" need triaging, then triage each one according to your instructions."
)
response = await call_agent_async(runner, USER_ID, session.id, prompt)