Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
44 changes: 36 additions & 8 deletions contributing/samples/adk_triaging_agent/README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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. **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.

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).

2. **Scheduled Runs**: The workflow also runs on a recurring schedule (every 6 hours) to process any unlabelled issues that may have been missed.
### 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"

### 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.
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.
Expand All @@ -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.
117 changes: 89 additions & 28 deletions contributing/samples/adk_triaging_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,27 @@
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,
}

Expand All @@ -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:
Expand All @@ -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 = (
Expand All @@ -167,7 +212,6 @@ def add_label_and_owner_to_issue(
return {
"status": "success",
"message": response,
"applied_label": label,
"assigned_owner": owner,
}

Expand Down Expand Up @@ -223,29 +267,46 @@ def change_issue_type(issue_number: int, issue_type: str) -> dict[str, Any]:
- 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

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

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**.
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,
],
)
58 changes: 37 additions & 21 deletions contributing/samples/adk_triaging_agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading