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
84 changes: 73 additions & 11 deletions contributing/samples/adk_documentation/adk_docs_updater/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
from adk_documentation.settings import DOC_REPO
from adk_documentation.tools import get_issue
from adk_documentation.utils import call_agent_async
from adk_documentation.utils import parse_suggestions
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner

APP_NAME = "adk_docs_updater"
USER_ID = "adk_docs_updater_user"

logs.setup_adk_logger(level=logging.DEBUG)
logs.setup_adk_logger(level=logging.INFO)


def process_arguments():
Expand Down Expand Up @@ -68,23 +69,84 @@ async def main():
print(f"Failed to get issue {issue_number}: {get_issue_response}\n")
return
issue = get_issue_response["issue"]
issue_title = issue.get("title", "")
issue_body = issue.get("body", "")

# Parse numbered suggestions from issue body
suggestions = parse_suggestions(issue_body)

if not suggestions:
print(f"No numbered suggestions found in issue #{issue_number}.")
print("Falling back to processing the entire issue as a single task.")
suggestions = [(1, issue_body)]

print(f"Found {len(suggestions)} suggestion(s) in issue #{issue_number}.")
print("=" * 80)

runner = InMemoryRunner(
agent=agent.root_agent,
app_name=APP_NAME,
)
session = await runner.session_service.create_session(
app_name=APP_NAME,
user_id=USER_ID,
)

response = await call_agent_async(
runner,
USER_ID,
session.id,
f"Please update the ADK docs according to the following issue:\n{issue}",
results = []
for suggestion_num, suggestion_text in suggestions:
print(f"\n>>> Processing suggestion #{suggestion_num}...")
print("-" * 80)

# Create a new session for each suggestion to avoid context interference
session = await runner.session_service.create_session(
app_name=APP_NAME,
user_id=USER_ID,
)

prompt = f"""
Please update the ADK docs according to suggestion #{suggestion_num} from issue #{issue_number}.

Issue title: {issue_title}

Suggestion to process:
{suggestion_text}

Note: Focus only on this specific suggestion. Create exactly one pull request for this suggestion.
"""

try:
response = await call_agent_async(
runner,
USER_ID,
session.id,
prompt,
)
results.append({
"suggestion_num": suggestion_num,
"status": "success",
"response": response,
})
print(f"<<<< Suggestion #{suggestion_num} completed.")
except Exception as e:
results.append({
"suggestion_num": suggestion_num,
"status": "error",
"error": str(e),
})
print(f"<<<< Suggestion #{suggestion_num} failed: {e}")

print("-" * 80)

# Print summary
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
successful = [r for r in results if r["status"] == "success"]
failed = [r for r in results if r["status"] == "error"]
print(
f"Total: {len(results)}, Success: {len(successful)}, Failed:"
f" {len(failed)}"
)
print(f"<<<< Agent Final Output: {response}\n")
if failed:
print("\nFailed suggestions:")
for r in failed:
print(f" - Suggestion #{r['suggestion_num']}: {r['error']}")


if __name__ == "__main__":
Expand Down
46 changes: 46 additions & 0 deletions contributing/samples/adk_documentation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import re
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple

from adk_documentation.settings import GITHUB_TOKEN
from google.adk.agents.run_config import RunConfig
Expand Down Expand Up @@ -96,3 +98,47 @@ async def call_agent_async(
final_response_text += text

return final_response_text


def parse_suggestions(issue_body: str) -> List[Tuple[int, str]]:
"""Parse numbered suggestions from issue body.

Supports multiple formats:
- Format A (markdown headers): "### 1. Title"
- Format B (numbered list with bold): "1. **Title**"

Args:
issue_body: The body text of the GitHub issue.

Returns:
A list of tuples, where each tuple contains:
- The suggestion number (1-based)
- The full text of that suggestion
"""
# Try different patterns in order of preference
patterns = [
# Format A: "### 1. Title" (markdown header with number)
(r"(?=^###\s+\d+\.)", r"^###\s+(\d+)\."),
# Format B: "1. **Title**" (numbered list with bold)
(r"(?=^\d+\.\s+\*\*)", r"^(\d+)\.\s+\*\*"),
]

for split_pattern, match_pattern in patterns:
parts = re.split(split_pattern, issue_body, flags=re.MULTILINE)

suggestions = []
for part in parts:
part = part.strip()
if not part:
continue

match = re.match(match_pattern, part)
if match:
suggestion_num = int(match.group(1))
suggestions.append((suggestion_num, part))

# If we found suggestions with this pattern, return them
if suggestions:
return suggestions

return []
29 changes: 29 additions & 0 deletions src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,38 @@ def _validate_runner_params(
def _infer_agent_origin(
self, agent: BaseAgent
) -> tuple[Optional[str], Optional[Path]]:
"""Infer the origin app name and directory from an agent's module location.

Returns:
A tuple of (origin_app_name, origin_path):
- origin_app_name: The inferred app name (directory name containing the
agent), or None if inference is not possible/applicable.
- origin_path: The directory path where the agent is defined, or None
if the path cannot be determined.

Both values are None when:
- The agent has no associated module
- The agent is defined in google.adk.* (ADK internal modules)
- The module has no __file__ attribute
"""
# First, check for metadata set by AgentLoader (most reliable source).
# AgentLoader sets these attributes when loading agents.
origin_app_name = getattr(agent, '_adk_origin_app_name', None)
origin_path = getattr(agent, '_adk_origin_path', None)
if origin_app_name is not None and origin_path is not None:
return origin_app_name, origin_path

# Fall back to heuristic inference for programmatic usage.
module = inspect.getmodule(agent.__class__)
if not module:
return None, None

# Skip ADK internal modules. When users instantiate LlmAgent directly
# (not subclassed), inspect.getmodule() returns the ADK module. This
# could falsely match 'agents' in 'google/adk/agents/' path.
if module.__name__.startswith('google.adk.'):
return None, None

module_file = getattr(module, '__file__', None)
if not module_file:
return None, None
Expand Down
Loading
Loading