chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# ADK Release Analyzer Agent
|
||||
|
||||
The ADK Release Analyzer Agent is a Python-based agent designed to help keep
|
||||
documentation up-to-date with code changes. It analyzes the differences between
|
||||
two releases of the `google/adk-python` repository, identifies required updates
|
||||
in the `google/adk-docs` repository, and automatically generates a GitHub issue
|
||||
with detailed instructions for documentation changes.
|
||||
|
||||
This agent can be operated in two distinct modes:
|
||||
|
||||
- an interactive mode for local use
|
||||
- a fully automated mode for integration into workflows.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Interactive Mode
|
||||
|
||||
This mode allows you to run the agent locally to review its recommendations in
|
||||
real-time before any changes are made.
|
||||
|
||||
### 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 creating an issue on GitHub with the documentation
|
||||
update instructions.
|
||||
- **Question & Answer**: You ask questions about the releases and code changes.
|
||||
The agent will provide answers based on related information.
|
||||
|
||||
### Running in Interactive Mode
|
||||
|
||||
To run the agent in interactive mode, first set the required environment
|
||||
variables, ensuring `INTERACTIVE` is set to `1` or is unset. Then, execute the
|
||||
following command in your terminal:
|
||||
|
||||
```bash
|
||||
adk web contributing/samples/adk_documentation
|
||||
```
|
||||
|
||||
This will start a local server and provide a URL to access the agent's web
|
||||
interface in your browser.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Automated Mode
|
||||
|
||||
For automated, hands-off analysis, the agent can be run as a script (`main.py`),
|
||||
for example as part of a CI/CD pipeline. The workflow is configured in
|
||||
`.github/workflows/analyze-releases-for-adk-docs-updates.yml` and automatically
|
||||
checks the most recent two releases for docs updates.
|
||||
|
||||
### Workflow Triggers
|
||||
|
||||
The GitHub workflow is configured to run on specific triggers:
|
||||
|
||||
- **Release Events**: The workflow executes automatically whenever a new release
|
||||
is `published`.
|
||||
|
||||
- **Manual Dispatch**: The workflow also runs when manually triggered for
|
||||
testing and retrying.
|
||||
|
||||
### Automated Issue Creation
|
||||
|
||||
When running in automated mode, the agent operates non-interactively. It creates
|
||||
a GitHub issue with the documentation update instructions directly without
|
||||
requiring user approval. This behavior is configured by setting the
|
||||
`INTERACTIVE` environment variable to `0`.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Setup and Configuration
|
||||
|
||||
Whether running in interactive or automated 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 issues:write permissions for the documentation repository.
|
||||
- `GOOGLE_API_KEY`: **(Required)** Your API key for the Gemini API.
|
||||
- `DOC_OWNER`: The GitHub organization or username that owns the documentation repository (defaults to `google`).
|
||||
- `CODE_OWNER`: The GitHub organization or username that owns the code repository (defaults to `google`).
|
||||
- `DOC_REPO`: The name of the documentation repository (defaults to `adk-docs`).
|
||||
- `CODE_REPO`: The name of the code repository (defaults to `adk-python`).
|
||||
- `LOCAL_REPOS_DIR_PATH`: The local directory to clone the repositories into (defaults to `/tmp`).
|
||||
- `INTERACTIVE`: Controls the agent's interaction mode. Set to 1 for interactive mode (default), and 0 for automated mode.
|
||||
|
||||
For local execution, you can place these variables in a `.env` file in the
|
||||
project's root directory. For automated workflows, they should be configured as
|
||||
environment variables or secrets.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,691 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
"""ADK Release Analyzer Agent - Multi-agent architecture for analyzing releases.
|
||||
|
||||
This agent uses a SequentialAgent + LoopAgent pattern to handle large releases
|
||||
without context overflow:
|
||||
|
||||
1. PlannerAgent: Collects changed files and creates analysis groups
|
||||
2. LoopAgent + FileGroupAnalyzer: Processes one group at a time
|
||||
3. SummaryAgent: Compiles all findings and creates the GitHub issue
|
||||
|
||||
State keys used:
|
||||
- start_tag, end_tag: Release tags being compared
|
||||
- compare_url: GitHub compare URL
|
||||
- file_groups: List of file groups to analyze
|
||||
- current_group_index: Index of current group being processed
|
||||
- recommendations: Accumulated recommendations from all groups
|
||||
"""
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
SAMPLES_DIR = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
)
|
||||
if SAMPLES_DIR not in sys.path:
|
||||
sys.path.append(SAMPLES_DIR)
|
||||
|
||||
from adk_documentation.settings import CODE_OWNER
|
||||
from adk_documentation.settings import CODE_REPO
|
||||
from adk_documentation.settings import DOC_OWNER
|
||||
from adk_documentation.settings import DOC_REPO
|
||||
from adk_documentation.settings import IS_INTERACTIVE
|
||||
from adk_documentation.settings import LOCAL_REPOS_DIR_PATH
|
||||
from adk_documentation.tools import clone_or_pull_repo
|
||||
from adk_documentation.tools import create_issue
|
||||
from adk_documentation.tools import get_changed_files_summary
|
||||
from adk_documentation.tools import get_file_diff_for_release
|
||||
from adk_documentation.tools import list_directory_contents
|
||||
from adk_documentation.tools import list_releases
|
||||
from adk_documentation.tools import read_local_git_repo_file_content
|
||||
from adk_documentation.tools import search_local_git_repo
|
||||
from google.adk import Agent
|
||||
from google.adk.agents.loop_agent import LoopAgent
|
||||
from google.adk.agents.readonly_context import ReadonlyContext
|
||||
from google.adk.agents.sequential_agent import SequentialAgent
|
||||
from google.adk.models import Gemini
|
||||
from google.adk.tools.exit_loop_tool import exit_loop
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
# Retry configuration for handling API rate limits and overload
|
||||
_RETRY_OPTIONS = types.HttpRetryOptions(
|
||||
initial_delay=10,
|
||||
attempts=8,
|
||||
exp_base=2,
|
||||
max_delay=300,
|
||||
http_status_codes=[429, 503],
|
||||
)
|
||||
|
||||
# Use gemini-3.1-pro-preview for planning and summary (better quality)
|
||||
GEMINI_PRO_WITH_RETRY = Gemini(
|
||||
model="gemini-3.1-pro-preview",
|
||||
retry_options=_RETRY_OPTIONS,
|
||||
)
|
||||
|
||||
# Maximum number of files per analysis group to avoid context overflow
|
||||
MAX_FILES_PER_GROUP = 5
|
||||
|
||||
if IS_INTERACTIVE:
|
||||
APPROVAL_INSTRUCTION = (
|
||||
"Ask for user approval or confirmation for creating or updating the"
|
||||
" issue."
|
||||
)
|
||||
else:
|
||||
APPROVAL_INSTRUCTION = (
|
||||
"**Do not** wait or ask for user approval or confirmation for creating"
|
||||
" or updating the issue."
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tool functions for state management
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_next_file_group(tool_context: ToolContext) -> dict[str, Any]:
|
||||
"""Gets the next group of files to analyze from the state.
|
||||
|
||||
This tool retrieves the next file group from state["file_groups"]
|
||||
and increments the current_group_index.
|
||||
|
||||
Args:
|
||||
tool_context: The tool context providing access to state.
|
||||
|
||||
Returns:
|
||||
A dictionary with the next file group or indication that all groups
|
||||
are processed.
|
||||
"""
|
||||
file_groups = tool_context.state.get("file_groups", [])
|
||||
current_index = tool_context.state.get("current_group_index", 0)
|
||||
|
||||
if current_index >= len(file_groups):
|
||||
print(f"[Progress] All {len(file_groups)} groups processed.")
|
||||
return {
|
||||
"status": "complete",
|
||||
"message": "All file groups have been processed.",
|
||||
"total_groups": len(file_groups),
|
||||
"processed": current_index,
|
||||
}
|
||||
|
||||
current_group = file_groups[current_index]
|
||||
file_paths = [f.get("relative_path", "?") for f in current_group]
|
||||
print(
|
||||
f"[Progress] Starting group {current_index + 1}/{len(file_groups)}:"
|
||||
f" {file_paths}"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"group_index": current_index,
|
||||
"total_groups": len(file_groups),
|
||||
"remaining": len(file_groups) - current_index - 1,
|
||||
"files": current_group,
|
||||
}
|
||||
|
||||
|
||||
def save_group_recommendations(
|
||||
tool_context: ToolContext,
|
||||
group_index: int,
|
||||
recommendations: list[dict[str, str]],
|
||||
) -> dict[str, Any]:
|
||||
"""Saves recommendations for a file group to state.
|
||||
|
||||
Args:
|
||||
tool_context: The tool context providing access to state.
|
||||
group_index: The index of the group these recommendations belong to.
|
||||
recommendations: List of recommendation dicts with keys:
|
||||
- summary: Brief summary of the change
|
||||
- doc_file: Path to the doc file to update
|
||||
- current_state: Current content in the doc
|
||||
- proposed_change: What should be changed
|
||||
- reasoning: Why this change is needed
|
||||
- reference: Reference to the code file
|
||||
|
||||
Returns:
|
||||
A dictionary confirming the save operation.
|
||||
"""
|
||||
all_recommendations = tool_context.state.get("recommendations", [])
|
||||
all_recommendations.extend(recommendations)
|
||||
tool_context.state["recommendations"] = all_recommendations
|
||||
# Advance index only after recommendations are saved, so interrupted
|
||||
# groups get retried on resume instead of being skipped.
|
||||
tool_context.state["current_group_index"] = group_index + 1
|
||||
|
||||
total_groups = len(tool_context.state.get("file_groups", []))
|
||||
print(
|
||||
f"[Progress] Group {group_index + 1}/{total_groups} done."
|
||||
f" +{len(recommendations)} recommendations"
|
||||
f" ({len(all_recommendations)} total)"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"group_index": group_index,
|
||||
"new_recommendations": len(recommendations),
|
||||
"total_recommendations": len(all_recommendations),
|
||||
}
|
||||
|
||||
|
||||
def get_all_recommendations(tool_context: ToolContext) -> dict[str, Any]:
|
||||
"""Retrieves all accumulated recommendations from state.
|
||||
|
||||
Args:
|
||||
tool_context: The tool context providing access to state.
|
||||
|
||||
Returns:
|
||||
A dictionary with all recommendations and metadata.
|
||||
"""
|
||||
recommendations = tool_context.state.get("recommendations", [])
|
||||
start_tag = tool_context.state.get("start_tag", "unknown")
|
||||
end_tag = tool_context.state.get("end_tag", "unknown")
|
||||
compare_url = tool_context.state.get("compare_url", "")
|
||||
|
||||
print(
|
||||
f"[Summary] Retrieving recommendations: {len(recommendations)} total,"
|
||||
f" release {start_tag} → {end_tag}"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"start_tag": start_tag,
|
||||
"end_tag": end_tag,
|
||||
"compare_url": compare_url,
|
||||
"total_recommendations": len(recommendations),
|
||||
"recommendations": recommendations,
|
||||
}
|
||||
|
||||
|
||||
def save_release_info(
|
||||
tool_context: ToolContext,
|
||||
start_tag: str,
|
||||
end_tag: str,
|
||||
compare_url: str,
|
||||
file_groups: list[list[dict[str, Any]]],
|
||||
release_summary: str,
|
||||
all_changed_files: list[str],
|
||||
) -> dict[str, Any]:
|
||||
"""Saves release info and file groups to state for processing.
|
||||
|
||||
Args:
|
||||
tool_context: The tool context providing access to state.
|
||||
start_tag: The starting release tag.
|
||||
end_tag: The ending release tag.
|
||||
compare_url: The GitHub compare URL.
|
||||
file_groups: List of file groups, where each group is a list of file
|
||||
info dicts.
|
||||
release_summary: A high-level summary of all changes in this release,
|
||||
including the main themes (e.g., "new feature X", "refactoring Y",
|
||||
"bug fixes in Z"). This helps individual analyzers understand the
|
||||
bigger picture.
|
||||
all_changed_files: List of all changed file paths (for cross-reference).
|
||||
|
||||
Returns:
|
||||
A dictionary confirming the save operation.
|
||||
"""
|
||||
tool_context.state["start_tag"] = start_tag
|
||||
tool_context.state["end_tag"] = end_tag
|
||||
tool_context.state["compare_url"] = compare_url
|
||||
tool_context.state["file_groups"] = file_groups
|
||||
tool_context.state["current_group_index"] = 0
|
||||
tool_context.state["recommendations"] = []
|
||||
tool_context.state["release_summary"] = release_summary
|
||||
tool_context.state["all_changed_files"] = all_changed_files
|
||||
|
||||
total_files = sum(len(group) for group in file_groups)
|
||||
print(
|
||||
f"[Planning] Release {start_tag} → {end_tag}:"
|
||||
f" {total_files} files in {len(file_groups)} groups"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"start_tag": start_tag,
|
||||
"end_tag": end_tag,
|
||||
"total_groups": len(file_groups),
|
||||
"total_files": sum(len(group) for group in file_groups),
|
||||
}
|
||||
|
||||
|
||||
def get_release_context(tool_context: ToolContext) -> dict[str, Any]:
|
||||
"""Gets the global release context for cross-group awareness.
|
||||
|
||||
This allows individual file group analyzers to understand:
|
||||
- The overall theme of the release
|
||||
- What other files were changed (for identifying related changes)
|
||||
- What recommendations have already been made (to avoid duplicates)
|
||||
|
||||
Args:
|
||||
tool_context: The tool context providing access to state.
|
||||
|
||||
Returns:
|
||||
A dictionary with global release context.
|
||||
"""
|
||||
return {
|
||||
"status": "success",
|
||||
"start_tag": tool_context.state.get("start_tag", "unknown"),
|
||||
"end_tag": tool_context.state.get("end_tag", "unknown"),
|
||||
"release_summary": tool_context.state.get("release_summary", ""),
|
||||
"all_changed_files": tool_context.state.get("all_changed_files", []),
|
||||
"existing_recommendations": tool_context.state.get("recommendations", []),
|
||||
"current_group_index": tool_context.state.get("current_group_index", 0),
|
||||
"total_groups": len(tool_context.state.get("file_groups", [])),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent 1: Planner Agent
|
||||
# =============================================================================
|
||||
|
||||
planner_agent = Agent(
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="release_planner",
|
||||
description=(
|
||||
"Plans the analysis by fetching release info and organizing files into"
|
||||
" groups for incremental processing."
|
||||
),
|
||||
instruction=f"""
|
||||
# 1. Identity
|
||||
You are the Release Planner, responsible for setting up the analysis of ADK
|
||||
Python releases. You gather information about changes and organize them for
|
||||
efficient processing.
|
||||
|
||||
# 2. Workflow
|
||||
1. First, call `clone_or_pull_repo` for both repositories:
|
||||
- ADK Python codebase: owner={CODE_OWNER}, repo={CODE_REPO}, path={LOCAL_REPOS_DIR_PATH}/{CODE_REPO}
|
||||
- ADK Docs: owner={DOC_OWNER}, repo={DOC_REPO}, path={LOCAL_REPOS_DIR_PATH}/{DOC_REPO}
|
||||
|
||||
2. Call `list_releases` to find the release tags for {CODE_OWNER}/{CODE_REPO}.
|
||||
- By default, compare the two most recent releases.
|
||||
- If the user specifies tags, use those instead.
|
||||
|
||||
3. Call `get_changed_files_summary` to get the list of changed files WITHOUT
|
||||
the full patches (to save context space).
|
||||
- **IMPORTANT**: Pass these parameters:
|
||||
- `local_repo_path="{LOCAL_REPOS_DIR_PATH}/{CODE_REPO}"` to avoid 300-file limit
|
||||
- `path_filter="src/google/adk/"` to only get ADK source files (reduces token usage)
|
||||
|
||||
4. Further filter the returned files:
|
||||
- **EXCLUDE** test files and `__init__.py` files
|
||||
- **IMPORTANT**: Do NOT exclude any file just because it has few changes.
|
||||
Even single-line changes to public APIs need documentation updates.
|
||||
- **PRIORITIZE** by importance:
|
||||
a) New files (status: "added") - ALWAYS include these
|
||||
b) CLI files (cli/) - often contain user-facing flags and options
|
||||
c) Tool files (tools/) - may contain new tools or tool parameters
|
||||
d) Core files (agents/, models/, sessions/, memory/, a2a/, flows/,
|
||||
plugins/, evaluation/)
|
||||
e) Files with many changes (high additions + deletions)
|
||||
|
||||
5. **Create a high-level release summary** based on the changed files:
|
||||
- Identify the main themes (e.g., "new tool X added", "refactoring of Y")
|
||||
- Note any files that appear related (e.g., same feature area)
|
||||
- This summary will be shared with individual file analyzers so they
|
||||
understand the bigger picture.
|
||||
|
||||
6. Group the filtered files into groups of at most {MAX_FILES_PER_GROUP} files each.
|
||||
- **IMPORTANT**: Group RELATED files together (same directory or feature)
|
||||
- Files that are part of the same feature should be in the same group
|
||||
- Each group should be independently analyzable
|
||||
|
||||
7. Call `save_release_info` to save:
|
||||
- start_tag, end_tag
|
||||
- compare_url
|
||||
- file_groups (the organized groups)
|
||||
- release_summary (the high-level summary you created)
|
||||
- all_changed_files (list of all file paths for cross-reference)
|
||||
|
||||
# 3. Output
|
||||
Provide a summary of:
|
||||
- Which releases are being compared
|
||||
- The high-level themes of this release
|
||||
- How many files changed in total
|
||||
- How many files are relevant for doc analysis
|
||||
- How many groups were created
|
||||
""",
|
||||
tools=[
|
||||
clone_or_pull_repo,
|
||||
list_releases,
|
||||
get_changed_files_summary,
|
||||
save_release_info,
|
||||
],
|
||||
output_key="planner_output",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent 2: File Group Analyzer (runs inside LoopAgent)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def file_analyzer_instruction(readonly_context: ReadonlyContext) -> str:
|
||||
"""Dynamic instruction that includes current state info."""
|
||||
start_tag = readonly_context.state.get("start_tag", "unknown")
|
||||
end_tag = readonly_context.state.get("end_tag", "unknown")
|
||||
release_summary = readonly_context.state.get("release_summary", "")
|
||||
|
||||
return f"""
|
||||
# 1. Identity
|
||||
You are the File Group Analyzer, responsible for analyzing a group of changed
|
||||
files and finding related documentation that needs updating.
|
||||
|
||||
# 2. Context
|
||||
- Comparing releases: {start_tag} to {end_tag}
|
||||
- Code repository: {CODE_OWNER}/{CODE_REPO}
|
||||
- Docs repository: {DOC_OWNER}/{DOC_REPO}
|
||||
- Docs local path: {LOCAL_REPOS_DIR_PATH}/{DOC_REPO}
|
||||
- Code local path: {LOCAL_REPOS_DIR_PATH}/{CODE_REPO}
|
||||
|
||||
## Release Summary (from Planner)
|
||||
{release_summary}
|
||||
|
||||
# 3. Workflow
|
||||
1. Call `get_next_file_group` to get the next group of files to analyze.
|
||||
- If status is "complete", call the `exit_loop` tool to exit the loop.
|
||||
|
||||
2. **FIRST**, call `get_release_context` to understand:
|
||||
- The overall release themes (to understand how your files fit in)
|
||||
- What other files were changed (to identify related changes)
|
||||
- What recommendations already exist (to AVOID DUPLICATES)
|
||||
|
||||
3. For each file in the group:
|
||||
a) Call `get_file_diff_for_release` to get the patch content for that file.
|
||||
b) Analyze the changes THOROUGHLY. Look for:
|
||||
**API Changes:**
|
||||
- New functions, classes, methods (especially public ones)
|
||||
- New parameters added to existing functions
|
||||
- New CLI arguments or flags (look for argparse, click decorators)
|
||||
- New environment variables (look for os.environ, getenv)
|
||||
- New tools or features being added
|
||||
- Renamed or deprecated functionality
|
||||
**Behavior Changes (even without API changes):**
|
||||
- Default values changed
|
||||
- Error handling or exception types changed
|
||||
- Return value format or content changed
|
||||
- Side effects added or removed
|
||||
- Performance characteristics changed
|
||||
- Edge case handling changed
|
||||
- Validation rules changed
|
||||
c) Consider how this file relates to OTHER changed files in this release.
|
||||
d) Generate MULTIPLE search patterns based on:
|
||||
- Class/function names that changed
|
||||
- Feature names mentioned in the file path
|
||||
- Keywords from the patch content (e.g., "local_storage", "allow_origins")
|
||||
- Tool names, parameter names, environment variable names
|
||||
|
||||
4. For EACH significant change, call `search_local_git_repo` to find related docs
|
||||
in {LOCAL_REPOS_DIR_PATH}/{DOC_REPO}/docs/
|
||||
- Search for the feature name, class name, or related keywords
|
||||
- **ALWAYS** pass `ignored_dirs=["api-reference"]` to skip auto-generated API
|
||||
reference docs (they are updated automatically by code, not manually)
|
||||
- If no docs found, recommend creating new documentation
|
||||
|
||||
5. Call `read_local_git_repo_file_content` to read the relevant doc files
|
||||
and check if they need updating.
|
||||
- **SKIP** any files under `docs/api-reference/` — these are auto-generated.
|
||||
|
||||
6. For each documentation update needed, create a recommendation with:
|
||||
- summary: Brief summary of what needs to change
|
||||
- doc_file: Relative path in the docs repo (e.g., docs/tools/google-search.md)
|
||||
- current_state: What the doc currently says
|
||||
- proposed_change: What it should say instead
|
||||
- reasoning: Why this update is needed
|
||||
- reference: The source code file path
|
||||
- related_files: Other changed files that are part of the same change (if any)
|
||||
|
||||
7. Call `save_group_recommendations` with all recommendations for this group.
|
||||
|
||||
8. After saving, output a brief summary of what you found for this group.
|
||||
|
||||
# 4. Rules
|
||||
- **BE THOROUGH**: Check EVERY change in the diff that could affect users.
|
||||
This includes API changes AND behavior changes (default values, error handling,
|
||||
return formats, side effects, etc.).
|
||||
- Focus on changes that users need to know about
|
||||
- Include behavior changes even if the API signature stays the same
|
||||
- If a change only affects auto-generated API reference docs, note that
|
||||
regeneration is needed instead of manual updates
|
||||
- **AVOID DUPLICATES**: Check existing_recommendations before adding new ones
|
||||
- **CROSS-REFERENCE**: If files in your group relate to files in other groups,
|
||||
mention this in your recommendation so the Summary agent can consolidate
|
||||
- **DON'T MISS ITEMS**: Better to have too many recommendations than too few.
|
||||
If unsure whether something needs documentation, include it.
|
||||
- For new features with no existing docs, recommend creating a new page
|
||||
"""
|
||||
|
||||
|
||||
file_group_analyzer = Agent(
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="file_group_analyzer",
|
||||
description=(
|
||||
"Analyzes a group of changed files and generates recommendations."
|
||||
),
|
||||
instruction=file_analyzer_instruction,
|
||||
include_contents="none",
|
||||
tools=[
|
||||
get_next_file_group,
|
||||
get_release_context, # Get global context to avoid duplicates
|
||||
get_file_diff_for_release,
|
||||
search_local_git_repo,
|
||||
read_local_git_repo_file_content,
|
||||
list_directory_contents,
|
||||
save_group_recommendations,
|
||||
exit_loop, # Call this when all groups are processed
|
||||
],
|
||||
output_key="analyzer_output",
|
||||
)
|
||||
|
||||
# Loop agent that processes file groups one at a time
|
||||
file_analysis_loop = LoopAgent(
|
||||
name="file_analysis_loop",
|
||||
sub_agents=[file_group_analyzer],
|
||||
max_iterations=50, # Safety limit
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent 3: Summary Agent
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def summary_instruction(readonly_context: ReadonlyContext) -> str:
|
||||
"""Dynamic instruction with release info."""
|
||||
start_tag = readonly_context.state.get("start_tag", "unknown")
|
||||
end_tag = readonly_context.state.get("end_tag", "unknown")
|
||||
|
||||
return f"""
|
||||
# 1. Identity
|
||||
You are the Summary Agent, responsible for compiling all recommendations into
|
||||
a well-formatted GitHub issue.
|
||||
|
||||
# 2. Workflow
|
||||
1. Call `get_all_recommendations` to retrieve all accumulated recommendations.
|
||||
|
||||
2. Organize the recommendations:
|
||||
- Group by importance: Feature changes > Bug fixes > Other
|
||||
- Within each group, sort by number of affected files
|
||||
- Remove duplicates or merge similar recommendations
|
||||
|
||||
3. Format the issue body using this template for each recommendation:
|
||||
```
|
||||
### N. **Summary of the change**
|
||||
|
||||
**Doc file**: path/to/doc.md
|
||||
|
||||
**Current state**:
|
||||
> Current content in the doc
|
||||
|
||||
**Proposed Change**:
|
||||
> What it should say instead
|
||||
|
||||
**Reasoning**:
|
||||
Explanation of why this change is necessary.
|
||||
|
||||
**Reference**: src/google/adk/path/to/file.py
|
||||
```
|
||||
|
||||
4. Create the GitHub issue:
|
||||
- Title: "Found docs updates needed from ADK python release {start_tag} to {end_tag}"
|
||||
- Include the compare link at the top
|
||||
- {APPROVAL_INSTRUCTION}
|
||||
|
||||
5. Call `create_issue` for {DOC_OWNER}/{DOC_REPO} with the formatted content.
|
||||
|
||||
# 3. Output
|
||||
Present a summary of:
|
||||
- Total recommendations created
|
||||
- Issue URL if created
|
||||
- Any notes about the analysis
|
||||
"""
|
||||
|
||||
|
||||
summary_agent = Agent(
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="summary_agent",
|
||||
description="Compiles recommendations and creates the GitHub issue.",
|
||||
instruction=summary_instruction,
|
||||
include_contents="none",
|
||||
tools=[
|
||||
get_all_recommendations,
|
||||
create_issue,
|
||||
],
|
||||
output_key="summary_output",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pipeline Agent: Sequential orchestration of the analysis
|
||||
# =============================================================================
|
||||
|
||||
analysis_pipeline = SequentialAgent(
|
||||
name="analysis_pipeline",
|
||||
description=(
|
||||
"Executes the release analysis pipeline: planning, file analysis, and"
|
||||
" summary generation."
|
||||
),
|
||||
sub_agents=[
|
||||
planner_agent,
|
||||
file_analysis_loop,
|
||||
summary_agent,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# Resume pipeline: skips planner, continues from where loop left off.
|
||||
# Deep copy agents since ADK agents can only have one parent.
|
||||
_resume_loop = copy.deepcopy(file_analysis_loop)
|
||||
_resume_loop.parent_agent = None
|
||||
_resume_summary = copy.deepcopy(summary_agent)
|
||||
_resume_summary.parent_agent = None
|
||||
|
||||
resume_pipeline = SequentialAgent(
|
||||
name="resume_pipeline",
|
||||
description=(
|
||||
"Resumes the release analysis pipeline from the file analysis loop,"
|
||||
" skipping the planning phase."
|
||||
),
|
||||
sub_agents=[
|
||||
_resume_loop,
|
||||
_resume_summary,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Root Agent: Entry point that understands user requests
|
||||
# =============================================================================
|
||||
|
||||
root_agent = Agent(
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="adk_release_analyzer",
|
||||
description=(
|
||||
"Analyzes ADK Python releases and generates documentation update"
|
||||
" recommendations."
|
||||
),
|
||||
instruction=f"""
|
||||
# 1. Identity
|
||||
You are the ADK Release Analyzer, a helper bot that analyzes changes between
|
||||
ADK Python releases and identifies documentation updates needed in the ADK
|
||||
Docs repository.
|
||||
|
||||
# 2. Capabilities
|
||||
You can help users in several ways:
|
||||
|
||||
## A. Full Release Analysis (delegate to analysis_pipeline)
|
||||
When users want a complete analysis of releases, delegate to the
|
||||
`analysis_pipeline` sub-agent. This will:
|
||||
- Clone/update repositories
|
||||
- Analyze all changed files
|
||||
- Generate recommendations
|
||||
- Create a GitHub issue
|
||||
|
||||
Use this when users say things like:
|
||||
- "Analyze the latest releases"
|
||||
- "Check what docs need updating for v1.15.0"
|
||||
- "Run a full analysis"
|
||||
|
||||
## B. Quick Queries (use your tools directly)
|
||||
For targeted questions, use your tools directly WITHOUT delegating:
|
||||
|
||||
- **"How should I modify doc1.md?"** → Use `search_local_git_repo` to find
|
||||
mentions of doc1.md in the codebase, then use `get_changed_files_summary`
|
||||
to see what changed, and provide specific guidance.
|
||||
|
||||
- **"What changed in the tools module?"** → Use `get_changed_files_summary`
|
||||
and filter for tools/ directory.
|
||||
|
||||
- **"Show me the recommendations from the last analysis"** → Use
|
||||
`get_all_recommendations` to retrieve stored recommendations.
|
||||
|
||||
- **"What releases are available?"** → Use `list_releases` directly.
|
||||
|
||||
# 3. Workflow Decision
|
||||
1. First, understand what the user is asking:
|
||||
- Full analysis request → delegate to analysis_pipeline
|
||||
- Specific question about a file/module → use tools directly
|
||||
- Query about previous results → use get_all_recommendations
|
||||
|
||||
2. For quick queries, ensure repos are cloned first using `clone_or_pull_repo`
|
||||
if needed.
|
||||
|
||||
3. Always explain what you're doing and provide clear, actionable answers.
|
||||
|
||||
# 4. Available Tools
|
||||
- `clone_or_pull_repo`: Ensure local repos are up to date
|
||||
- `list_releases`: See available release tags
|
||||
- `get_changed_files_summary`: Get list of changed files (lightweight)
|
||||
- `get_file_diff_for_release`: Get patch for a specific file
|
||||
- `search_local_git_repo`: Search for patterns in repos
|
||||
- `read_local_git_repo_file_content`: Read file contents
|
||||
- `get_all_recommendations`: Retrieve recommendations from previous analysis
|
||||
|
||||
# 5. Repository Info
|
||||
- Code repo: {CODE_OWNER}/{CODE_REPO} at {LOCAL_REPOS_DIR_PATH}/{CODE_REPO}
|
||||
- Docs repo: {DOC_OWNER}/{DOC_REPO} at {LOCAL_REPOS_DIR_PATH}/{DOC_REPO}
|
||||
""",
|
||||
tools=[
|
||||
clone_or_pull_repo,
|
||||
list_releases,
|
||||
get_changed_files_summary,
|
||||
get_file_diff_for_release,
|
||||
search_local_git_repo,
|
||||
read_local_git_repo_file_content,
|
||||
get_all_recommendations,
|
||||
],
|
||||
sub_agents=[analysis_pipeline],
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright 2026 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 logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from adk_documentation.adk_release_analyzer import agent
|
||||
from adk_documentation.settings import CODE_OWNER
|
||||
from adk_documentation.settings import CODE_REPO
|
||||
from adk_documentation.settings import DOC_OWNER
|
||||
from adk_documentation.settings import DOC_REPO
|
||||
from adk_documentation.utils import call_agent_async
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions import DatabaseSessionService
|
||||
|
||||
APP_NAME = "adk_release_analyzer"
|
||||
USER_ID = "adk_release_analyzer_user"
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), "sessions.db")
|
||||
DB_URL = f"sqlite+aiosqlite:///{DB_PATH}"
|
||||
|
||||
logs.setup_adk_logger(level=logging.INFO)
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="ADK Release Analyzer")
|
||||
parser.add_argument(
|
||||
"--resume",
|
||||
action="store_true",
|
||||
help="Resume from the last session instead of starting fresh.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-tag",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The older release tag (base) for comparison, e.g. v1.26.0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--end-tag",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The newer release tag (head) for comparison, e.g. v1.27.0.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
session_service = DatabaseSessionService(db_url=DB_URL)
|
||||
|
||||
if args.resume:
|
||||
# Find the most recent session to resume
|
||||
sessions_response = await session_service.list_sessions(
|
||||
app_name=APP_NAME, user_id=USER_ID
|
||||
)
|
||||
if not sessions_response.sessions:
|
||||
print("No previous session found. Starting fresh.")
|
||||
args.resume = False
|
||||
|
||||
if args.resume:
|
||||
# Resume: use existing session with resume_pipeline (skip planner)
|
||||
last_session = sessions_response.sessions[-1]
|
||||
session_id = last_session.id
|
||||
session = await session_service.get_session(
|
||||
app_name=APP_NAME, user_id=USER_ID, session_id=session_id
|
||||
)
|
||||
state = session.state
|
||||
group_index = state.get("current_group_index", 0)
|
||||
total_groups = len(state.get("file_groups", []))
|
||||
num_recs = len(state.get("recommendations", []))
|
||||
print(f"Resuming session {session_id}")
|
||||
print(
|
||||
f" Progress: group {group_index + 1}/{total_groups},"
|
||||
f" {num_recs} recommendations so far"
|
||||
)
|
||||
print(
|
||||
f" Release: {state.get('start_tag', '?')} →"
|
||||
f" {state.get('end_tag', '?')}"
|
||||
)
|
||||
|
||||
runner = Runner(
|
||||
agent=agent.resume_pipeline,
|
||||
app_name=APP_NAME,
|
||||
session_service=session_service,
|
||||
)
|
||||
prompt = "Resume analyzing the remaining file groups."
|
||||
else:
|
||||
# Fresh run
|
||||
runner = Runner(
|
||||
agent=agent.root_agent,
|
||||
app_name=APP_NAME,
|
||||
session_service=session_service,
|
||||
)
|
||||
session = await session_service.create_session(
|
||||
app_name=APP_NAME,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
session_id = session.id
|
||||
if args.start_tag and args.end_tag:
|
||||
prompt = (
|
||||
f"Please analyze ADK Python releases from {args.start_tag} to"
|
||||
f" {args.end_tag}!"
|
||||
)
|
||||
elif args.end_tag:
|
||||
prompt = (
|
||||
f"Please analyze the ADK Python release {args.end_tag} against its"
|
||||
" previous release!"
|
||||
)
|
||||
else:
|
||||
prompt = "Please analyze the most recent two releases of ADK Python!"
|
||||
|
||||
print(f"Session ID: {session_id}")
|
||||
print("-" * 80)
|
||||
|
||||
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 analyzing {CODE_OWNER}/{CODE_REPO} releases for"
|
||||
f" {DOC_OWNER}/{DOC_REPO} updates 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")
|
||||
Reference in New Issue
Block a user