Skip to content

Add Google Ads management agent template#938

Open
itallstartedwithaidea wants to merge 1 commit intoGoogleCloudPlatform:mainfrom
itallstartedwithaidea:add-google-ads-agent
Open

Add Google Ads management agent template#938
itallstartedwithaidea wants to merge 1 commit intoGoogleCloudPlatform:mainfrom
itallstartedwithaidea:add-google-ads-agent

Conversation

@itallstartedwithaidea
Copy link
Copy Markdown

New Agent: Google Ads Management

What it does

An ADK agent for Google Ads campaign analysis, auditing, and optimization using the Google Ads API. This is the first advertising/PPC management agent in the starter pack.

Tools

Tool Purpose
get_account_summary Account-level spend, conversions, CPA, ROAS for any date range
list_campaigns All campaigns ranked by spend with performance metrics
find_wasted_spend Search terms with clicks but zero conversions
get_quality_scores Quality Score distribution with improvement recommendations
get_recommendations Google's optimization suggestions with projected conversion lift

How it works

The agent uses GAQL (Google Ads Query Language) to pull live data from the Google Ads API, then Gemini analyzes the results and provides expert-level recommendations. For full audits, the agent chains all 5 tools in sequence: account summary -> campaigns -> wasted spend -> quality scores -> recommendations.

Template configuration

  • Dependencies: google-ads>=25.1.0, google-adk>=1.15.0
  • Deployment targets: Agent Engine, Cloud Run
  • Tags: adk, google-ads, advertising
  • Requires session: Yes

Why this belongs in the starter pack

  • Google Ads is the largest digital advertising platform ($280B+ annual revenue)
  • No advertising management agent exists in the current template library
  • The Agent Garden already has "Marketing Agency" and "Brand Search Optimization" but no Google Ads campaign management
  • The pattern (GAQL queries + Gemini analysis) is clean and extensible

About the author

John Williams — Lead, Paid Media at Seer Interactive. Creator of googleadsagent.ai, an AI-native Google Ads management platform with a 1,000-pattern knowledge base.

Files

agent_starter_pack/agents/google_ads/
  .template/templateconfig.yaml   # Template metadata
  README.md                       # Documentation
  app/__init__.py
  app/agent.py                    # ADK agent with 5 Google Ads tools
  tests/test_agent.py             # Unit tests

Made with Cursor

New ADK agent for Google Ads campaign analysis, auditing, and optimization
using the Google Ads API (GAQL queries).

Tools:
- get_account_summary: Account-level spend, conversions, CPA, ROAS
- list_campaigns: Campaign performance ranked by spend
- find_wasted_spend: Search terms with zero conversions
- get_quality_scores: Quality Score distribution and recommendations
- get_recommendations: Google's optimization suggestions with projected lift

The agent chains these tools for comprehensive audits: account summary →
campaigns → wasted spend → quality scores → recommendations.

Uses google-ads Python client library for live API data and Gemini for
analysis. Follows the standard ADK agent pattern with templateconfig.yaml.

Made-with: Cursor
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 12, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new Google Ads Management Agent to the starter pack, providing tools for account auditing and optimization. The review feedback identifies a critical bug in the custom _row_to_dict function that fails to handle nested fields, which would cause runtime errors in the quality score tool. Additionally, the feedback recommends adding validation for the Google Ads Customer ID, handling potential division-by-zero errors in metrics calculations, and parameterizing the date range in the quality score tool to ensure consistency across the agent's functionality.

Comment on lines +64 to +74
def _row_to_dict(row) -> dict:
"""Convert a Google Ads API row to a flat dictionary."""
result = {}
for field in row._pb.DESCRIPTOR.fields:
val = getattr(row, field.name, None)
if val is not None:
try:
result[field.name] = {f.name: getattr(val, f.name, None) for f in val._pb.DESCRIPTOR.fields}
except AttributeError:
result[field.name] = val
return result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current implementation of _row_to_dict only flattens the first level of the GoogleAdsRow. For nested fields like ad_group_criterion.quality_info.quality_score, the second level remains as a message object rather than a dictionary. Since protobuf message objects do not have a .get() method, the code in get_quality_scores (line 229) will raise an AttributeError. Using the built-in to_dict method from the proto-plus library is a more robust and recursive way to convert the row.

Suggested change
def _row_to_dict(row) -> dict:
"""Convert a Google Ads API row to a flat dictionary."""
result = {}
for field in row._pb.DESCRIPTOR.fields:
val = getattr(row, field.name, None)
if val is not None:
try:
result[field.name] = {f.name: getattr(val, f.name, None) for f in val._pb.DESCRIPTOR.fields}
except AttributeError:
result[field.name] = val
return result
def _row_to_dict(row) -> dict:
"""Convert a Google Ads API row to a dictionary."""
return row.to_dict()

Comment on lines +233 to +242
total = sum(scores.values())
low = sum(v for k, v in scores.items() if k <= 4)
high = sum(v for k, v in scores.items() if k >= 7)

lines = [f"Quality Score Distribution ({total} keywords):\n"]
for qs in sorted(scores.keys()):
bar = "#" * min(scores[qs], 40)
lines.append(f" QS {qs:2d}: {scores[qs]:4d} keywords {bar}")
lines.append(f"\nLow QS (<=4): {low} ({low/total:.0%})")
lines.append(f"High QS (>=7): {high} ({high/total:.0%})")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The calculation of percentages for low and high Quality Scores will raise a ZeroDivisionError if total is zero. This can happen if the API returns rows but none of them contain a valid quality_score. A check should be added to handle this case.

Suggested change
total = sum(scores.values())
low = sum(v for k, v in scores.items() if k <= 4)
high = sum(v for k, v in scores.items() if k >= 7)
lines = [f"Quality Score Distribution ({total} keywords):\n"]
for qs in sorted(scores.keys()):
bar = "#" * min(scores[qs], 40)
lines.append(f" QS {qs:2d}: {scores[qs]:4d} keywords {bar}")
lines.append(f"\nLow QS (<=4): {low} ({low/total:.0%})")
lines.append(f"High QS (>=7): {high} ({high/total:.0%})")
total = sum(scores.values())
if total == 0:
return "No Quality Score data available for the keywords found."
low = sum(v for k, v in scores.items() if k <= 4)
high = sum(v for k, v in scores.items() if k >= 7)
lines = [f"Quality Score Distribution ({total} keywords):\n"]
for qs in sorted(scores.keys()):
bar = "#" * min(scores[qs], 40)
lines.append(f" QS {qs:2d}: {scores[qs]:4d} keywords {bar}")
lines.append(f"\nLow QS (<=4): {low} ({low/total:.0%})")
lines.append(f"High QS (>=7): {high} ({high/total:.0%})")

Comment on lines +52 to +53
cid = (customer_id or GOOGLE_ADS_CUSTOMER_ID).replace("-", "")
service = client.get_service("GoogleAdsService")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If both customer_id and GOOGLE_ADS_CUSTOMER_ID are empty, cid will be an empty string, which will cause the service.search call to fail with an unhelpful error from the API. It is better to validate that a Customer ID is present before attempting the API call.

    cid = (customer_id or GOOGLE_ADS_CUSTOMER_ID).replace("-", "")
    if not cid:
        return [{"error": "Google Ads Customer ID is missing. Set GOOGLE_ADS_CUSTOMER_ID environment variable."}]
    service = client.get_service("GoogleAdsService")

Comment on lines +203 to +220
def get_quality_scores() -> str:
"""Get Quality Score distribution for all active keywords.

Returns:
Quality Score breakdown with keyword count per score level and recommendations.
"""
query = """
SELECT ad_group_criterion.keyword.text,
ad_group_criterion.quality_info.quality_score,
ad_group_criterion.quality_info.creative_quality_score,
ad_group_criterion.quality_info.post_click_quality_score,
ad_group_criterion.quality_info.search_predicted_ctr,
campaign.name, metrics.impressions, metrics.cost_micros
FROM keyword_view
WHERE ad_group_criterion.quality_info.quality_score IS NOT NULL
AND segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The get_quality_scores tool has a hardcoded date range of LAST_30_DAYS in its GAQL query, unlike the other tools in this agent which allow the user (or the LLM) to specify a date_range. For consistency and flexibility, and to ensure relevant parameters are forwarded to the underlying service call, this should be a parameter.

def get_quality_scores(date_range: str = "LAST_30_DAYS") -> str:
    """Get Quality Score distribution for all active keywords.

    Args:
        date_range: Date range for metrics. Options: YESTERDAY, LAST_7_DAYS,
            LAST_14_DAYS, LAST_30_DAYS.

    Returns:
        Quality Score breakdown with keyword count per score level and recommendations.
    """
    query = f"""
        SELECT ad_group_criterion.keyword.text,
               ad_group_criterion.quality_info.quality_score,
               ad_group_criterion.quality_info.creative_quality_score,
               ad_group_criterion.quality_info.post_click_quality_score,
               ad_group_criterion.quality_info.search_predicted_ctr,
               campaign.name, metrics.impressions, metrics.cost_micros
        FROM keyword_view
        WHERE ad_group_criterion.quality_info.quality_score IS NOT NULL
          AND segments.date DURING {date_range}
        ORDER BY metrics.cost_micros DESC
    """
References
  1. When a function acts as a wrapper for another function, ensure that all relevant parameters are forwarded to the wrapped function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant