Skip to content

Commit de1c889

Browse files
xuanyang15copybara-github
authored andcommitted
chore: create an initial ADK docs updater agent to create doc update PRs
PiperOrigin-RevId: 806348675
1 parent 2d98b2c commit de1c889

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import sys
17+
18+
SAMPLES_DIR = os.path.abspath(
19+
os.path.join(os.path.dirname(__file__), "..", "..")
20+
)
21+
if SAMPLES_DIR not in sys.path:
22+
sys.path.append(SAMPLES_DIR)
23+
24+
from adk_documentation.settings import CODE_OWNER
25+
from adk_documentation.settings import CODE_REPO
26+
from adk_documentation.settings import DOC_OWNER
27+
from adk_documentation.settings import DOC_REPO
28+
from adk_documentation.settings import IS_INTERACTIVE
29+
from adk_documentation.settings import LOCAL_REPOS_DIR_PATH
30+
from adk_documentation.tools import clone_or_pull_repo
31+
from adk_documentation.tools import create_pull_request_from_changes
32+
from adk_documentation.tools import get_issue
33+
from adk_documentation.tools import list_directory_contents
34+
from adk_documentation.tools import read_local_git_repo_file_content
35+
from adk_documentation.tools import search_local_git_repo
36+
from google.adk import Agent
37+
38+
if IS_INTERACTIVE:
39+
APPROVAL_INSTRUCTION = (
40+
"Ask for user approval or confirmation for creating the pull request."
41+
)
42+
else:
43+
APPROVAL_INSTRUCTION = (
44+
"**Do not** wait or ask for user approval or confirmation for creating"
45+
" the pull request."
46+
)
47+
48+
root_agent = Agent(
49+
model="gemini-2.5-pro",
50+
name="adk_docs_updater",
51+
description=(
52+
"Update the ADK docs based on the code in the ADK Python codebase"
53+
" according to the instructions in the ADK docs issues."
54+
),
55+
instruction=f"""
56+
# 1. Identity
57+
You are a helper bot that updates ADK docs in Github Repository {DOC_OWNER}/{DOC_REPO}
58+
based on the code in the ADK Python codebase in Github Repository {CODE_OWNER}/{CODE_REPO} according to the instructions in the ADK docs issues.
59+
60+
You are very familiar with Github, expecially how to search for files in a Github repository using git grep.
61+
62+
# 2. Responsibilities
63+
Your core responsibility includes:
64+
- Read the doc update instructions in the ADK docs issues.
65+
- Find **all** the related Python files in ADK Python codebase.
66+
- Compare the ADK docs with **all** the related Python files and analyze the differences and the doc update instructions.
67+
- Create a pull request to update the ADK docs.
68+
69+
# 3. Workflow
70+
1. Always call the `clone_or_pull_repo` tool to make sure the ADK docs and codebase repos exist in the local folder {LOCAL_REPOS_DIR_PATH}/repo_name and are the latest version.
71+
2. Read the issue specified by user using the `get_issue` tool.
72+
3. If the issue contains instructions about how to update the ADK docs, follow the instructions to update the ADK docs.
73+
4. Understand the doc update instructions.
74+
- Ignore and skip the instructions about updating API reference docs, since it will be automatically generated by the ADK team.
75+
5. Read the doc to update using the `read_local_git_repo_file_content` tool from the local ADK docs repo under {LOCAL_REPOS_DIR_PATH}/{DOC_REPO}.
76+
6. Find the related Python files in the ADK Python codebase.
77+
- If the doc update instructions specify paths to the Python files, use them directly, otherwise use a list of regex search patterns to find the related Python files through the `search_local_git_repo` tool.
78+
- You should focus on the main ADK Python codebase, ignore the changes in tests or other auxiliary files.
79+
- You should find all the related Python files, not only the most relevant one.
80+
7. Read the specified or found Python files using the `read_local_git_repo_file_content` tool to find all the related code.
81+
- You can ignore unit test files, unless you are sure that the test code is uesful to understand the related concepts.
82+
- You should read all the the found files to find all the related code, unless you already know the content of the file or you are sure that the file is not related to the ADK doc.
83+
8. Update the ADK doc file according to the doc update instructions and the related code.
84+
9. Create pull requests to update the ADK doc file using the `create_pull_request_from_changes` tool.
85+
- For each recommended change, create a separate pull request.
86+
- The title of the pull request should be "Update ADK doc according to issue #<issue number> - <change id>", where <issue number> is the number of the ADK docs issue and <change id> is the id of the recommended change (e.g. "1", "2", etc.).
87+
- The body of the pull request should be the instructions about how to update the ADK docs.
88+
- **{APPROVAL_INSTRUCTION}**
89+
90+
# 4. Guidelines & Rules
91+
- **File Paths:** Always use absolute paths when calling the tools to read files, list directories, or search the codebase.
92+
- **Tool Call Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
93+
- **Explaination:** Provide concise explanations for your actions and reasoning for each step.
94+
95+
# 5. Output
96+
Present the followings in an easy to read format as the final output to the user.
97+
- The actions you took and the reasoning
98+
- The summary of the pull request created
99+
""",
100+
tools=[
101+
clone_or_pull_repo,
102+
list_directory_contents,
103+
search_local_git_repo,
104+
read_local_git_repo_file_content,
105+
create_pull_request_from_changes,
106+
get_issue,
107+
],
108+
)

contributing/samples/adk_documentation/tools.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from datetime import datetime
1516
import os
1617
import subprocess
18+
from subprocess import CompletedProcess
1719
from typing import Any
1820
from typing import Dict
1921
from typing import List
@@ -299,6 +301,89 @@ def search_local_git_repo(
299301
return error_response(f"An unexpected error occurred: {e}")
300302

301303

304+
def create_pull_request_from_changes(
305+
repo_owner: str,
306+
repo_name: str,
307+
local_path: str,
308+
base_branch: str,
309+
changes: Dict[str, str],
310+
commit_message: str,
311+
pr_title: str,
312+
pr_body: str,
313+
) -> Dict[str, Any]:
314+
"""Creates a new branch, applies file changes, commits, pushes, and creates a PR.
315+
316+
Args:
317+
repo_owner: The username or organization that owns the repository.
318+
repo_name: The name of the repository.
319+
local_path: The local absolute path to the cloned repository.
320+
base_branch: The name of the branch to merge the changes into (e.g.,
321+
"main").
322+
changes: A dictionary where keys are file paths relative to the repo root
323+
and values are the new and full content for those files.
324+
commit_message: The message for the git commit.
325+
pr_title: The title for the pull request.
326+
pr_body: The body/description for the pull request.
327+
328+
Returns:
329+
A dictionary containing the status and the pull request object on success,
330+
or an error message on failure.
331+
"""
332+
try:
333+
# Step 0: Ensure we are on the base branch and it's up to date.
334+
_run_git_command(["checkout", base_branch], local_path)
335+
_run_git_command(["pull", "origin", base_branch], local_path)
336+
337+
# Step 1: Create a new, unique branch from the base branch.
338+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
339+
new_branch = f"agent-changes-{timestamp}"
340+
_run_git_command(["checkout", "-b", new_branch], local_path)
341+
print(f"Created and switched to new branch: {new_branch}")
342+
343+
# Step 2: Apply the file changes.
344+
if not changes:
345+
return error_response("No changes provided to apply.")
346+
347+
for relative_path, new_content in changes.items():
348+
full_path = os.path.join(local_path, relative_path)
349+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
350+
with open(full_path, "w", encoding="utf-8") as f:
351+
f.write(new_content)
352+
print(f"Applied changes to {relative_path}")
353+
354+
# Step 3: Stage the changes.
355+
_run_git_command(["add", "."], local_path)
356+
print("Staged all changes.")
357+
358+
# Step 4: Commit the changes.
359+
_run_git_command(["commit", "-m", commit_message], local_path)
360+
print(f"Committed changes with message: '{commit_message}'")
361+
362+
# Step 5: Push the new branch to the remote repository.
363+
_run_git_command(["push", "-u", "origin", new_branch], local_path)
364+
print(f"Pushed branch '{new_branch}' to origin.")
365+
366+
# Step 6: Create the pull request via GitHub API.
367+
url = f"{GITHUB_BASE_URL}/repos/{repo_owner}/{repo_name}/pulls"
368+
payload = {
369+
"title": pr_title,
370+
"body": pr_body,
371+
"head": new_branch,
372+
"base": base_branch,
373+
}
374+
pr_response = post_request(url, payload)
375+
print(f"Successfully created pull request: {pr_response.get('html_url')}")
376+
377+
return {"status": "success", "pull_request": pr_response}
378+
379+
except subprocess.CalledProcessError as e:
380+
return error_response(f"A git command failed: {e.stderr}")
381+
except requests.exceptions.RequestException as e:
382+
return error_response(f"GitHub API request failed: {e}")
383+
except (IOError, OSError) as e:
384+
return error_response(f"A file system error occurred: {e}")
385+
386+
302387
def get_issue(
303388
repo_owner: str, repo_name: str, issue_number: int
304389
) -> Dict[str, Any]:
@@ -378,6 +463,19 @@ def update_issue(
378463
return {"status": "success", "issue": response}
379464

380465

466+
def _run_git_command(command: List[str], cwd: str) -> CompletedProcess[str]:
467+
"""A helper to run a git command and raise an exception on error."""
468+
base_command = ["git"]
469+
process = subprocess.run(
470+
base_command + command,
471+
cwd=cwd,
472+
capture_output=True,
473+
text=True,
474+
check=True, # This will raise CalledProcessError if the command fails
475+
)
476+
return process
477+
478+
381479
def _find_head_commit_sha(repo_path: str) -> str:
382480
"""Checks the head commit hash of a Git repository."""
383481
head_sha_command = ["git", "rev-parse", "HEAD"]

0 commit comments

Comments
 (0)