End-to-End Implementation 01: Production Commerce Agent with Session Persistence
Overview
This is a production-ready end-to-end implementation of a Commerce Agent that demonstrates essential ADK v1.17.0 capabilities in a clean, maintainable architecture. The agent handles real-world e-commerce scenarios including:
- Grounding metadata extraction from Google Search results for source attribution
- Multi-user session management with ADK state isolation (
user:prefix) - Product discovery via Google Search with site-specific filtering (Decathlon)
- Personalized recommendations based on saved user preferences
- Type-safe tool interfaces using TypedDict patterns
- Comprehensive testing covering unit, integration, and e2e scenarios
- Optional SQLite persistence for sessions that survive app restarts
This tutorial teaches you to build clean, testable agents that follow ADK best practices and scale from development through production deployment.
Key Implementation Highlights:
- ✅ Simple Architecture: One root agent with 3 tools (not complex multi-agent)
- ✅ Grounding Callback: Extract and monitor Google Search source attribution
- ✅ Two Persistence Modes: ADK state (default) or SQLite (optional)
- ✅ Vertex AI Ready: Optimized for Vertex AI with fallback to Gemini API
- ✅ TypedDict Safety: Type-safe tool returns with IDE autocomplete
Prerequisites
- ✅ Completed Tutorials 01-34 (especially #08 State Memory, #11 Built-in Tools, #19 Artifacts)
- ✅ Python 3.9 or higher
- ✅ Google API Key with Gemini access
- ✅ SQLite3 (usually pre-installed on macOS/Linux)
- ✅ Understanding of async/await patterns
- ✅ Familiarity with pytest and testing
Core Concepts
1. Simple Agent Architecture
This implementation uses a clean, single-agent design with three tools:
┌─────────────────────────────────────┐
│ Commerce Agent (Root) │
│ Personal Shopping Concierge │
└──────────────┬──────────────────────┘
│
┌─────────┼─────────┐
v v v
[Search] [Save] [Get]
Tool Prefs Prefs
│
v
AgentTool wrapping
Google Search Agent
(site:decathlon.com.hk)
Why this approach?
- ✅ Simpler to understand and maintain
- ✅ Follows ADK best practices from official samples
- ✅ Easier to test and debug
- ✅ Production-ready without overengineering
2. Multi-User State Isolation
The agent uses ADK's built-in state management with the user: prefix
for cross-session persistence:
User Alice (alice) User Bob (bob)
| |
v v
Session s1 ← ISOLATED → Session s2
| |
+-- state['user:pref_sport'] +-- state['user:pref_sport']
| = "running" | = "cycling"
| |
+-- state['user:pref_budget'] +-- state['user:pref_budget']
= 150 = 200
Each user has completely isolated preferences
Key Point: The user: prefix in ADK state automatically provides
multi-user isolation. No complex database setup required for basic use cases.
3. State Management Deep Dive
The agent uses ADK state scopes correctly for different data lifetimes:
| Scope | Prefix | Lifetime | Example |
|---|---|---|---|
| Session | none | Current chat | current_query |
| User | user: | Across sessions | user:pref_sport |
| App | app: | Shared globally | app:product_cache |
| Temp | temp: | Current invocation | temp:grounding_sources |
How it works:
# In save_preferences tool
def save_preferences(sport: str, budget_max: int, ..., tool_context: ToolContext):
# Saves to user-scoped state (persists across sessions)
tool_context.state["user:pref_sport"] = sport
tool_context.state["user:pref_budget"] = budget_max
# ✅ This data survives when user starts a new chat session
# In get_preferences tool
def get_preferences(tool_context: ToolContext):
# Retrieves user-scoped state
sport = tool_context.state.get("user:pref_sport")
budget = tool_context.state.get("user:pref_budget")
# ✅ Returns saved preferences from previous sessions
Critical: User-scoped data with user: prefix provides multi-user isolation.
User "alice" cannot access user "bob"'s preferences.
4. Optional SQLite Persistence
While ADK state (user: prefix) handles most use cases, the implementation
also supports SQLite for full session persistence:
Two modes available:
-
ADK State (Default):
make dev- Simple, works out-of-box
- Preferences persist across invocations
- Sessions lost on app restart
-
SQLite (Advanced):
make dev-sqlite- Full conversation history preserved
- Sessions survive app restarts
- SQL query capabilities
# SQLite mode (optional)
from google.adk.sessions import DatabaseSessionService
session_service = DatabaseSessionService(
db_url="sqlite:///./commerce_sessions.db?mode=wal"
)
# Or use CLI:
# adk web --session_service_uri "sqlite:///./sessions.db?mode=wal"
When to use SQLite:
- ✅ Need conversation history across restarts
- ✅ Want SQL query capabilities
- ✅ Production deployment requirements
When ADK state is enough:
- ✅ Simple user preferences (sport, budget, experience)
- ✅ Development and testing
- ✅ Single-server deployments
5. Grounding Metadata Extraction (NEW in v1.17.0)
A key feature of this implementation is the grounding callback that extracts source attribution from Google Search results:
from commerce_agent import create_grounding_callback
from google.adk.runners import Runner
runner = Runner(
agent=root_agent,
after_model_callbacks=[create_grounding_callback(verbose=True)]
)
What it extracts:
- ✅ Source URLs and titles from grounding chunks
- ✅ Domain names (e.g., "decathlon.com.hk", "alltricks.com")
- ✅ Segment-level attribution (which sources support which claims)
- ✅ Confidence scores based on multi-source agreement
Console output example:
====================================================================
✓ GROUNDING METADATA EXTRACTED
====================================================================
Total Sources: 5
Sources:
1. [decathlon.com.hk] Brooks Divide 5 - Trail Running Shoes
2. [alltricks.com] Brooks Divide 5 - €95 Free Shipping
3. [runningwarehouse.com] Brooks Divide 5 Review
Grounding Supports: 8 segments
1. [high] "Brooks Divide 5 costs €95" (3 sources)
2. [medium] "ideal for beginner trail runners" (2 sources)
... and 6 more
====================================================================
Why this matters:
- ✅ Transparency: Users see which retailers/sources support each claim
- ✅ Trust: Multiple sources = higher confidence in recommendations
- ✅ Debugging: Console logs help verify search quality during development
- ✅ Anti-hallucination: Validate that URLs are from real search results
Architecture Overview
Agent Structure
The commerce agent uses a simple, maintainable architecture:
Commerce Agent (Root)
├── Tool 1: search_products (AgentTool wrapping Google Search)
├── Tool 2: save_preferences (FunctionTool)
└── Tool 3: get_preferences (FunctionTool)
No complex sub-agents. This design:
- ✅ Follows ADK best practices from official samples
- ✅ Easier to test (fewer moving parts)
- ✅ Clearer debugging (single agent flow)
- ✅ Production-ready without overengineering
Data Flow
User Query ("Find running shoes under €100")
↓
Root Agent receives message
↓
┌───────────────────────────────────────┐
│ 1. Call get_preferences() │
│ → Check if user has saved prefs │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 2. If prefs missing: │
│ Ask clarifying questions │
│ Then call save_preferences() │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 3. Call search_products() │
│ → Executes Google Search │
│ → site:decathlon.com.hk filter │
│ → Returns 3-5 products │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 4. Grounding Callback (after_model) │
│ → Extracts source attribution │
│ → Logs to console │
│ → Stores in state['temp:*'] │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 5. Generate Response │
│ → Personalized recommendations │
│ → Why each product fits user needs │
│ → Purchase links with retailers │
└───────────────────────────────────────┘
↓
Response to User
Database Schema
The implementation includes a simple SQLite database used by the preference tools for storing historical data and favorites. This is optional and separate from ADK's session management.
Database file: commerce_agent_sessions.db (created automatically)
-- User preferences (managed by save_preferences/get_preferences tools)
CREATE TABLE user_preferences (
user_id TEXT PRIMARY KEY,
preferences_json TEXT, -- JSON: {sports, price_range, brands}
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Interaction history for analytics (optional)
CREATE TABLE interaction_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
session_id TEXT NOT NULL,
query TEXT,
result_count INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Favorite products (optional)
CREATE TABLE user_favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
product_id TEXT,
product_name TEXT,
url TEXT,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Important clarifications:
-
ADK State vs Database: The agent primarily uses ADK's
user:state for preferences. The database is for additional features (history, favorites). -
Not for ADK Sessions: This database does NOT store ADK session data. For that, use
DatabaseSessionServicewithmake dev-sqlite. -
Initialization: Database is created automatically on first
make setupviainit_database()call.
Implementation Deep Dive
Step 1: Setup and Running
# Navigate to the tutorial
cd tutorial_implementation/commerce_agent_e2e
# Option 1: Install dependencies only
make setup
# Option 2: Setup with Vertex AI authentication (recommended)
make setup-vertex-ai # Interactive script to configure service account
make setup
# Run all tests
make test
# Start development UI (ADK state persistence)
make dev
# OR start with SQLite persistence (survives restarts)
make dev-sqlite
Step 2: Understanding the Tool Implementations
Tool 1: Product Search (AgentTool wrapping Google Search)
from google.adk.agents import Agent
from google.adk.tools.agent_tool import AgentTool
from google.adk.tools.google_search_tool import google_search
# Search agent with Google Search grounding
_search_agent = Agent(
model="gemini-2.5-flash",
name="sports_product_search",
description="Search for sports products using Google Search with grounding",
instruction="""Search for sports products and provide detailed information.
When searching:
1. Use comprehensive queries like "best trail running shoes under 100 euros 2025"
2. Extract key product information: name, brand, price, features
3. **CRITICAL**: Display URLs from search results with clear retailer attribution
4. Present 3-5 products with clickable links
Response format:
- Product name and brand
- Price in EUR
- Key features (2-3 bullet points)
- **Purchase Link**: Show with visible retailer domain
- Brief explanation of why it fits user needs
""",
tools=[google_search],
)
# Export as AgentTool for use in main agent
search_products = AgentTool(agent=_search_agent)
Key points:
- ✅ Uses AgentTool pattern to wrap Google Search agent
- ✅ Site-restricted search via query params (e.g., "site:decathlon.com.hk")
- ✅ Grounding metadata automatically extracted by Google Search
- ✅ Works best with Vertex AI (Gemini API has site: operator limitations)
Tool 2: Save Preferences (FunctionTool)
from typing import Dict, Any
from google.adk.tools import ToolContext
def save_preferences(
sport: str,
budget_max: int,
experience_level: str,
tool_context: ToolContext
) -> Dict[str, Any]:
"""Save user preferences for personalized recommendations."""
try:
# Save to user state (persists across sessions)
tool_context.state["user:pref_sport"] = sport
tool_context.state["user:pref_budget"] = budget_max
tool_context.state["user:pref_experience"] = experience_level
return {
"status": "success",
"report": f"✓ Preferences saved: {sport}, max €{budget_max}, {experience_level} level",
"data": {
"sport": sport,
"budget_max": budget_max,
"experience_level": experience_level
}
}
except Exception as e:
return {
"status": "error",
"report": f"Failed to save preferences: {str(e)}",
"error": str(e)
}
Key points:
- ✅ Uses
tool_context.state["user:*"]for cross-session persistence - ✅ Returns structured dict matching ToolResult TypedDict (but not in signature)
- ✅ Proper error handling with descriptive messages
- ✅ Simple and testable
Tool 3: Get Preferences (FunctionTool)
def get_preferences(tool_context: ToolContext) -> Dict[str, Any]:
"""Retrieve saved user preferences."""
try:
state = tool_context.state
prefs = {
"sport": state.get("user:pref_sport"),
"budget_max": state.get("user:pref_budget"),
"experience_level": state.get("user:pref_experience")
}
# Filter out None values
prefs = {k: v for k, v in prefs.items() if v is not None}
if not prefs:
return {
"status": "success",
"report": "No preferences saved yet",
"data": {}
}
return {
"status": "success",
"report": f"Retrieved preferences: {', '.join(f'{k}={v}' for k, v in prefs.items())}",
"data": prefs
}
except Exception as e:
return {
"status": "error",
"report": f"Failed to retrieve preferences: {str(e)}",
"error": str(e),
"data": {}
}
Key points:
- ✅ Reads from
user:*state keys - ✅ Handles missing preferences gracefully
- ✅ Returns consistent format
Step 3: The Grounding Callback
from commerce_agent.callbacks import create_grounding_callback
def create_grounding_callback(verbose: bool = True):
"""Create a grounding metadata extraction callback.
Returns:
Async callback function for use with Runner
"""
async def extract_grounding_metadata(callback_context, llm_response):
"""Extract grounding metadata from LLM response."""
if not hasattr(llm_response, 'candidates'):
return None
candidate = llm_response.candidates[0]
if not hasattr(candidate, 'grounding_metadata'):
return None
metadata = candidate.grounding_metadata
# Extract sources from grounding_chunks
sources = []
if hasattr(metadata, 'grounding_chunks'):
for chunk in metadata.grounding_chunks:
if hasattr(chunk, 'web') and chunk.web:
sources.append({
"title": chunk.web.title,
"uri": chunk.web.uri,
"domain": extract_domain(chunk.web.uri)
})
# Store in temp state for current invocation
callback_context.state["temp:_grounding_sources"] = sources
if verbose:
print(f"\n{'='*60}")
print("✓ GROUNDING METADATA EXTRACTED")
print(f"Total Sources: {len(sources)}")
for i, source in enumerate(sources, 1):
print(f" {i}. [{source['domain']}] {source['title']}")
print(f"{'='*60}\n")
return None # ADK callbacks return None
return extract_grounding_metadata
Usage with Runner:
from google.adk.runners import Runner
from commerce_agent import root_agent, create_grounding_callback
runner = Runner(
agent=root_agent,
after_model_callbacks=[create_grounding_callback(verbose=True)]
)
Key points:
- ✅ Function-based callback (not class-based)
- ✅ Goes in Runner's
after_model_callbacks, not Agent - ✅ Extracts source URLs, titles, domains from grounding_chunks
- ✅ Console logging for development visibility
- ✅ Stores in
temp:state (current invocation only)
Step 3: Session Management Testing
The tutorial includes comprehensive tests for session isolation:
@pytest.mark.asyncio
async def test_multi_user_session_isolation():
"""Verify users cannot access each other's state"""
service = DatabaseSessionService(db_url="sqlite:///:memory:")
# Alice sets sport preference
alice = await service.create_session(
"commerce_agent", "alice", "session1",
state={"user:sport": "running"}
)
# Bob sets different preference
bob = await service.create_session(
"commerce_agent", "bob", "session1",
state={"user:sport": "cycling"}
)
# Verify isolation
alice_session = await service.get_session("commerce_agent", "alice", "session1")
assert alice_session.state["user:sport"] == "running"
bob_session = await service.get_session("commerce_agent", "bob", "session1")
assert bob_session.state["user:sport"] == "cycling"
# Cross-user access must fail
with pytest.raises(Exception):
await service.get_session("commerce_agent", "alice", "session1_bob_data")
Step 4: Testing with adk web
Once running, test interactively:
-
Test Preference Workflow:
- Open http://localhost:8000
- Select "commerce_agent" from dropdown
- Type: "I want running shoes"
- Agent should call
get_preferences→ ask for budget & experience - Type: "Under 150 euros, I'm a beginner"
- Agent should call
save_preferences→ confirm saved ✅
-
Test Product Search:
- Type: "Find trail running shoes"
- Agent calls
search_products - Verify results include Decathlon products ✅
- Check terminal for grounding metadata extraction logs
-
Test Preference Persistence:
- Refresh browser (new session, same user)
- Type: "What are my preferences?"
- Agent should retrieve saved preferences from previous session ✅
-
Test Personalized Recommendations:
- Type: "Recommend something for me"
- Agent should reference saved sport/budget/experience ✅
- Recommendations should be tailored to beginner level
Note on Multi-User Testing: The adk web UI doesn't have User ID input.
To test multi-user isolation, use the API endpoints directly (see
docs/TESTING_WITH_USER_IDENTITIES.md or run make test-guide).
Complete Testing Workflow
Test Organization
The test suite follows a clear structure:
tests/
├── conftest.py # Test fixtures and configuration
├── test_tools.py # Unit tests for individual tools
├── test_integration.py # Integration tests (agent + tools)
├── test_e2e.py # End-to-end user scenarios
├── test_agent_instructions.py # Agent prompt/instruction tests
└── test_callback_and_types.py # Callback and TypedDict tests
Tier 1: Unit Tests
pytest tests/test_tools.py -v
Tests:
- ✅
save_preferencesstores data in ADK state correctly - ✅
get_preferencesretrieves data from state - ✅ Tool return format matches ToolResult TypedDict structure
- ✅ Error handling with proper status/report fields
- ✅ Missing preferences handled gracefully
Tier 2: Integration Tests
pytest tests/test_integration.py -v
Tests:
- ✅ Agent configuration is valid (model, name, description)
- ✅ Agent has all 3 tools attached correctly
- ✅ Tool imports work (search_products, save_preferences, get_preferences)
- ✅ Package structure is correct
- ✅ Grounding callback imports successfully
Tier 3: End-to-End Tests
pytest tests/test_e2e.py -v
Tests:
- ✅ Complete new user workflow (set prefs → search → get recommendations)
- ✅ Returning customer scenario (preferences persist across sessions)
- ✅ Multi-user isolation (Alice's prefs don't affect Bob's)
- ✅ Database operations (if using optional SQLite features)
- ✅ Error recovery scenarios
Tier 4: Agent Instruction Tests
pytest tests/test_agent_instructions.py -v
Tests:
- ✅ Agent instruction contains preference workflow steps
- ✅ Instruction mentions all 3 tools
- ✅ Concierge persona is present
- ✅ Product presentation format specified
Tier 5: Callback and Type Tests
pytest tests/test_callback_and_types.py -v
Tests:
- ✅ Grounding callback creates function correctly
- ✅ TypedDict structures are importable
- ✅ ToolResult matches expected format
- ✅ Callback can be attached to Runner
Run All Tests with Coverage
make test
# Runs: pytest tests/ -v --cov=commerce_agent --cov-report=html
# Generates: htmlcov/index.html (opens automatically in browser)
Expected Results:
- ✅ 14+ tests passing
- ✅ 85%+ code coverage
- ✅ No import errors
- ✅ All test tiers green
Key Features Demonstrated
1. Grounding Metadata Extraction (NEW)
The grounding callback extracts source attribution from Google Search:
from commerce_agent import create_grounding_callback
from google.adk.runners import Runner
runner = Runner(
agent=root_agent,
after_model_callbacks=[create_grounding_callback(verbose=True)]
)
What it provides:
- ✅ Source URLs and titles from grounding_chunks
- ✅ Domain extraction (e.g., "decathlon.com.hk")
- ✅ Segment-level attribution (which sources support which claims)
- ✅ Console logging for debugging
- ✅ Anti-hallucination validation
2. ADK State Management (Primary Method)
Uses user: prefix for cross-session persistence:
def save_preferences(..., tool_context: ToolContext):
tool_context.state["user:pref_sport"] = sport
tool_context.state["user:pref_budget"] = budget
# ✅ Persists across invocations, isolated by user
Benefits:
- ✅ Zero configuration required
- ✅ Automatic multi-user isolation
- ✅ Works with any ADK deployment (web, CLI, API)
- ✅ Perfect for simple key-value preferences
3. Optional SQLite Persistence (Advanced)
Available via make dev-sqlite for full session history:
from google.adk.sessions import DatabaseSessionService
session_service = DatabaseSessionService(
db_url="sqlite:///./commerce_sessions.db?mode=wal"
)
# Or via CLI:
# adk web --session_service_uri "sqlite:///./sessions.db?mode=wal"
When to use:
- ✅ Need conversation history across restarts
- ✅ Want SQL query capabilities
- ✅ Production requirements for audit trails
4. TypedDict for Type Safety
All tools return structured dicts with TypedDict hints:
from commerce_agent.types import ToolResult
def my_tool(...) -> Dict[str, Any]: # Use Dict in signature (ADK requirement)
result: ToolResult = { # Can use TypedDict for hints
"status": "success",
"report": "Operation completed",
"data": {"key": "value"}
}
return result # ✅ IDE autocomplete + type checking
Benefits:
- ✅ Full IDE autocomplete
- ✅ Type checking with mypy
- ✅ Clear API contracts
- ✅ ADK compatibility maintained
5. Simple Agent Coordination
Clean single-agent design with specialized tools:
root_agent = Agent(
model="gemini-2.5-flash",
name="commerce_agent",
tools=[
search_products, # AgentTool (wraps Google Search)
FunctionTool(func=save_preferences),
FunctionTool(func=get_preferences),
]
)
Why this approach:
- ✅ Simpler than multi-agent orchestration
- ✅ Easier to test and debug
- ✅ Follows official ADK samples
- ✅ Production-ready without overengineering
Authentication & Setup
⚠️ Critical: Vertex AI vs Gemini API
The agent works with both authentication methods, but with key differences:
| Feature | Vertex AI | Gemini API |
|---|---|---|
| Google Search | ✅ Full support | ⚠️ Limited |
| site: operator | ✅ Works | ❌ Doesn't work |
| Search quality | ✅ Excellent | ⚠️ Mixed results |
| Grounding | ✅ Full metadata | ⚠️ Partial |
| Production | ✅ Recommended | ❌ Dev only |
Problem with Gemini API: The site:decathlon.com.hk search operator
doesn't work, causing the agent to return results from Amazon, eBay, Adidas,
and other non-Decathlon retailers. This breaks the core product discovery flow.
Setup Option 1: Vertex AI (Recommended)
# Navigate to tutorial
cd tutorial_implementation/commerce_agent_e2e
# Run interactive setup script
make setup-vertex-ai
# Follow prompts to:
# 1. Verify service account at ./credentials/commerce-agent-key.json
# 2. Unset any conflicting API keys
# 3. Set GOOGLE_CLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS
# 4. Test authentication
# Then install dependencies
make setup
The setup-vertex-ai script handles:
- ✅ Service account verification
- ✅ Environment variable configuration
- ✅ Credential testing
- ✅ Conflict resolution (removes GOOGLE_API_KEY if set)
Setup Option 2: Gemini API (Limited)
# Get API key from https://aistudio.google.com/app/apikey
export GOOGLE_API_KEY=your_key_here
# Install dependencies
cd tutorial_implementation/commerce_agent_e2e
make setup
Known Limitation: Search will return non-Decathlon results.
Verifying Authentication
# Check which credentials are active
echo $GOOGLE_API_KEY
echo $GOOGLE_APPLICATION_CREDENTIALS
# If both are set, Vertex AI takes precedence
# Manually unset API key if needed:
unset GOOGLE_API_KEY
# Restart agent
make dev
Deployment Scenarios
Local Development
# Option 1: ADK state (simple, preferences persist across invocations)
make dev
# Option 2: SQLite (full history, survives restarts)
make dev-sqlite
# Access at http://localhost:8000
Production (Cloud Run)
# Using Vertex AI
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
# Option 1: ADK state (simple)
adk deploy cloud_run --name commerce-agent
# Option 2: SQLite persistence
adk deploy cloud_run \
--name commerce-agent \
--session_service_uri "sqlite:///./sessions.db?mode=wal"
Enterprise Scale (Agent Engine + Cloud Spanner)
# Deploy to Agent Engine with Cloud Spanner persistence
adk deploy agent_engine \
--name commerce-agent \
--session_service_uri "spanner://projects/MY_PROJECT/instances/MY_INSTANCE/databases/commerce"
Benefits of Cloud Spanner:
- ✅ Multi-region deployment
- ✅ Automatic scaling
- ✅ High availability (99.999% SLA)
- ✅ ACID transactions
- ✅ SQL query capabilities
Success Criteria
You'll know everything is working when:
✅ All 14+ tests pass (make test)
✅ Agent starts without errors (make dev)
✅ Agent appears in dropdown at http://localhost:8000
✅ Agent calls get_preferences at conversation start
✅ Agent calls save_preferences when user provides info
✅ Agent searches products using Google Search
✅ Preferences persist across browser refresh
✅ Grounding metadata appears in server logs (terminal)
✅ Product recommendations include Decathlon links
✅ No "site: operator" issues (if using Vertex AI)
Common Issues & Solutions
| Issue | Solution |
|---|---|
| Agent not in dropdown | Run pip install -e . in tutorial root |
| Search returns non-Decathlon | Using Gemini API - switch to Vertex AI |
| "site: operator doesn't work" | Run make setup-vertex-ai |
| Tests fail with auth error | Set credentials (see Authentication section) |
| Grounding metadata not visible | Check terminal logs (not in UI) |
| Preferences not persisting | Verify user: prefix in state keys |
| Both API key and SA set | Unset GOOGLE_API_KEY (Vertex AI takes precedence) |
| Database locked error | Only happens if using SQLite mode, restart dev |
Detailed Troubleshooting
Issue: Search Returns Wrong Retailers
Symptom: Agent recommends products from Amazon, eBay, Adidas instead of Decathlon.
Cause: Using Gemini API instead of Vertex AI. The site:decathlon.com.hk
operator doesn't work with Gemini API.
Solution:
# 1. Check which auth is active
echo $GOOGLE_API_KEY
echo $GOOGLE_APPLICATION_CREDENTIALS
# 2. If GOOGLE_API_KEY is set, unset it
unset GOOGLE_API_KEY
# 3. Run Vertex AI setup
make setup-vertex-ai
# 4. Restart agent
make dev
Issue: Grounding Metadata Not Showing
Expected Behavior: Grounding metadata appears in terminal logs, not in the web UI.
Where to look:
# Terminal output after search_products call:
====================================================================
✓ GROUNDING METADATA EXTRACTED
====================================================================
Total Sources: 5
1. [decathlon.com.hk] Brooks Divide 5...
2. [alltricks.com] Brooks Divide 5...
====================================================================
Note: To display grounding in UI, you'd need custom frontend integration (CopilotKit or React components).
Issue: Preferences Not Persisting
Check:
- Verify tools use
user:prefix:
tool_context.state["user:pref_sport"] = sport # ✅ Correct
tool_context.state["pref_sport"] = sport # ❌ Wrong (session only)
- Check agent instruction mentions preference workflow
- Verify user isn't changing User ID between sessions
What You'll Learn
By completing this implementation, you'll master:
- Simple Agent Design: Clean single-agent with specialized tools
- ADK State Management: User-scoped state for multi-user isolation
- Grounding Metadata: Extracting and monitoring Google Search sources
- TypedDict Safety: Type-safe tool returns with IDE support
- Function Tools: Simple, testable tool implementations
- Testing Patterns: Unit, integration, and e2e test organization
- Authentication: Vertex AI vs Gemini API trade-offs
- Production Deployment: Cloud Run, Agent Engine, and Spanner options
Key Takeaways:
- ✅ Start simple (single agent) before going complex (multi-agent)
- ✅ Use ADK state for preferences (unless you need SQL queries)
- ✅ Vertex AI is required for site-restricted search
- ✅ Grounding callback provides transparency and anti-hallucination
- ✅ TypedDict helps but can't be used in tool signatures (ADK limitation)
Next Steps
After completing this tutorial:
- Customize the prompt: Edit
commerce_agent/prompt.pyfor different personalities - Add more tools: Create tools for cart management, order tracking, reviews
- Integrate frontend: Use CopilotKit to build custom UI with grounding display
- Switch to SQLite: Try
make dev-sqlitefor persistent conversation history - Deploy to production: Use
adk deploy cloud_runwith Vertex AI - Add analytics: Track user behavior, popular products, search patterns
- Implement ML recommendations: Use Vertex AI predictions for personalization
References
Official Resources
- ADK Documentation
- State Management Guide
- Google Search Tool
- Session Service
- Testing Guide
- Deployment Options
Implementation Files
Additional Documentation
docs/GROUNDING_CALLBACK_GUIDE.md- Complete grounding metadata usagedocs/SQLITE_SESSION_PERSISTENCE_GUIDE.md- SQLite persistence deep divedocs/TESTING_WITH_USER_IDENTITIES.md- Multi-user testing via APITESTING_GUIDE.md- Testing instructions and debugging
💬 Join the Discussion
Have questions or feedback? Discuss this tutorial with the community on GitHub Discussions.