Add Google Ads management agent template#938
Add Google Ads management agent template#938itallstartedwithaidea wants to merge 1 commit intoGoogleCloudPlatform:mainfrom
Conversation
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
|
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. |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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() |
| 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%})") |
There was a problem hiding this comment.
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.
| 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%})") |
| cid = (customer_id or GOOGLE_ADS_CUSTOMER_ID).replace("-", "") | ||
| service = client.get_service("GoogleAdsService") |
There was a problem hiding this comment.
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")| 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 | ||
| """ |
There was a problem hiding this comment.
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
- When a function acts as a wrapper for another function, ensure that all relevant parameters are forwarded to the wrapped function.
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
get_account_summarylist_campaignsfind_wasted_spendget_quality_scoresget_recommendationsHow 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
google-ads>=25.1.0,google-adk>=1.15.0adk,google-ads,advertisingWhy this belongs in the starter pack
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
Made with Cursor