mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
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:
committed by
Copybara-Service
parent
77401132d1
commit
3aef9a18b1
@@ -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,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user