Welcome back, aspiring A2UI developer! In the previous chapters, we’ve explored the fundamental building blocks of A2UI, understood how agents communicate through declarative UI, and even touched upon basic interactivity. Now, it’s time to put that knowledge into action by building a complete, practical project: an Interactive Restaurant Finder Agent.
This chapter will guide you through creating an agent that can understand your dining preferences, search for restaurants, and present the results in a dynamic, user-friendly interface powered entirely by A2UI. We’ll start from the ground up, simulating data, handling user input, and progressively enhancing the UI. Get ready to see your agent come alive with rich, interactive capabilities!
Prerequisites: Before diving in, ensure you’re comfortable with:
- The core concepts of A2UI, including components like
Card,Text,Input, andButton. - How agents generate and respond to A2UI messages.
- A basic Python environment setup, as we’ll be using Python for our agent’s backend logic.
Agent-Side Logic for Dynamic UI Generation
At the heart of any A2UI application is the agent’s ability to intelligently decide what UI to present based on the current context, user input, and its internal state. For our restaurant finder, this means:
- Initial Greeting & Query: When a user first interacts, the agent needs to ask what they’re looking for.
- Processing Input: Once the user provides a search query (e.g., “Italian restaurants in downtown”), the agent must parse it.
- Data Retrieval (Simulated): The agent will then “search” for restaurants. For this project, we’ll use a simple, hardcoded list of restaurants to keep things focused on A2UI, but in a real application, this would involve calling a database or an external API (like a restaurant directory service).
- Displaying Results: The retrieved data needs to be transformed into A2UI components that effectively display information like restaurant name, cuisine, rating, and location.
- Adding Interactivity: Users should be able to refine their search, view details, or perform other actions directly from the UI generated by the agent.
Let’s visualize this flow:
This iterative loop—user input, agent processing, UI generation—is a cornerstone of agent-driven interfaces.
A2UI Components for Our Restaurant Finder
We’ll primarily leverage a few key A2UI components to build our interface:
Card: Excellent for grouping related information, like details for a single restaurant.Text: To display restaurant names, descriptions, ratings, etc.Input: To allow users to type their search queries.Button: For initiating actions like searching, filtering, or viewing more details.List: To present multiple restaurantCards in an organized fashion.Select: (Optional, for advanced features) To allow users to choose from predefined categories like cuisine types or price ranges.
Remember, the agent doesn’t send HTML or JavaScript; it sends a structured JSON object that describes the UI, and the A2UI renderer takes care of displaying it natively.
Step-by-Step Implementation: Building Our Agent
For this project, we’ll assume a basic Python agent setup where your agent receives a JSON payload (representing user input or actions) and returns a JSON payload (representing the A2UI response).
Let’s begin by setting up a simple Python file, say restaurant_agent.py.
Step 1: Initial Agent Response – The Welcome Mat
Our agent should first greet the user and ask for their restaurant search query.
# restaurant_agent.py (Initial setup)
import json
# Define a simple, mock database of restaurants
RESTAURANTS_DB = [
{"id": "r1", "name": "Pasta Paradise", "cuisine": "Italian", "rating": 4.5, "location": "Downtown", "price": "$$", "description": "Authentic Italian dishes in a cozy setting."},
{"id": "r2", "name": "Sushi Haven", "cuisine": "Japanese", "rating": 4.8, "location": "Uptown", "price": "$$$", "description": "Finest sushi and sashimi."},
{"id": "r3", "name": "Burger Joint", "cuisine": "American", "rating": 3.9, "location": "Downtown", "price": "$", "description": "Classic burgers and fries."},
{"id": "r4", "name": "Taco Fiesta", "cuisine": "Mexican", "rating": 4.2, "location": "Midtown", "price": "$$", "description": "Vibrant spot for tacos and margaritas."},
{"id": "r5", "name": "Green Garden", "cuisine": "Vegetarian", "rating": 4.1, "location": "Uptown", "price": "$$", "description": "Fresh, healthy, and delicious vegetarian options."}
]
def generate_welcome_ui():
"""Generates the initial welcome message and input field."""
return {
"elements": [
{
"type": "card",
"content": {
"elements": [
{"type": "text", "text": "Hello! I'm your Restaurant Finder Agent. What kind of food are you craving or where are you located?", "style": "heading"},
{
"type": "input",
"name": "search_query",
"label": "e.g., 'Italian in Downtown' or 'Vegetarian'",
"placeholder": "Tell me your preferences...",
"actions": [
{
"type": "submit",
"label": "Search",
"actionId": "search_restaurants"
}
]
}
]
}
}
]
}
def handle_agent_request(input_data: dict):
"""
Main function to handle incoming requests to the agent.
In a real system, `input_data` would contain user actions/inputs.
"""
if not input_data: # Initial request, no user input yet
return generate_welcome_ui()
# Placeholder for handling user input later
print(f"Agent received input: {input_data}")
return {"elements": [{"type": "text", "text": "I received your input, but I'm not ready to process it yet!"}]}
# Example of how to "run" the agent locally for testing
if __name__ == "__main__":
print("--- Initial Agent Response ---")
initial_response = handle_agent_request({})
print(json.dumps(initial_response, indent=2))
# Simulate user entering a query
print("\n--- Simulating User Input ---")
mock_user_input = {
"actionId": "search_restaurants",
"values": {
"search_query": "Italian in Downtown"
}
}
user_response = handle_agent_request(mock_user_input)
print(json.dumps(user_response, indent=2))
Explanation:
- We define
RESTAURANTS_DBas a simple list of dictionaries to act as our data source. - The
generate_welcome_ui()function constructs an A2UI payload. It uses acardto encapsulate the content, atextelement for the greeting, and aninputelement where the user can type their query. - Crucially, the
inputelement includes anactionsarray with asubmitaction. ThisactionId: "search_restaurants"is how our agent will know what the user intends to do when they submit the input. handle_agent_requestis our agent’s entry point. Ifinput_datais empty, it’s the first interaction, so we show the welcome UI. Otherwise, we’ll process the input.
Run this script. You’ll see the JSON output for the welcome UI, and then a placeholder message for the simulated user input.
Step 2: Handling User Input and Simulating Search
Now, let’s modify handle_agent_request to actually process the user’s search query and “find” restaurants from our RESTAURANTS_DB.
# restaurant_agent.py (Continued)
# ... (RESTAURANTS_DB and generate_welcome_ui remain the same) ...
def search_restaurants(query: str):
"""
Simulates searching the RESTAURANTS_DB based on a query.
In a real application, this would involve a more sophisticated search
or an external API call.
"""
query = query.lower()
results = []
for restaurant in RESTAURANTS_DB:
if query in restaurant["name"].lower() or \
query in restaurant["cuisine"].lower() or \
query in restaurant["location"].lower() or \
query in restaurant["description"].lower():
results.append(restaurant)
return results
def generate_restaurant_list_ui(restaurants: list):
"""Generates a UI to display a list of restaurants."""
if not restaurants:
return {
"elements": [
{"type": "text", "text": "Sorry, I couldn't find any restaurants matching your criteria.", "style": "paragraph"}
]
}
restaurant_cards = []
for restaurant in restaurants:
restaurant_cards.append({
"type": "card",
"content": {
"elements": [
{"type": "text", "text": restaurant["name"], "style": "heading"},
{"type": "text", "text": f"Cuisine: {restaurant['cuisine']} | Rating: {restaurant['rating']} ⭐", "style": "paragraph"},
{"type": "text", "text": f"Location: {restaurant['location']} | Price: {restaurant['price']}", "style": "paragraph"},
{"type": "text", "text": restaurant['description'], "style": "caption"},
{
"type": "button",
"label": "View Details",
"actionId": "view_details",
"data": {"restaurant_id": restaurant["id"]} # Pass data with the action
}
]
}
})
return {
"elements": [
{"type": "text", "text": "Here are some restaurants I found:", "style": "heading"},
{"type": "list", "elements": restaurant_cards}, # Using a list to group cards
# Add a button to go back to search
{
"type": "button",
"label": "Start New Search",
"actionId": "start_new_search",
"style": "secondary"
}
]
}
def handle_agent_request(input_data: dict):
"""
Main function to handle incoming requests to the agent.
`input_data` contains user actions/inputs.
"""
if not input_data:
return generate_welcome_ui()
action_id = input_data.get("actionId")
values = input_data.get("values", {})
if action_id == "search_restaurants":
query = values.get("search_query", "")
print(f"Agent searching for: '{query}'")
found_restaurants = search_restaurants(query)
return generate_restaurant_list_ui(found_restaurants)
elif action_id == "start_new_search":
print("Agent initiating new search.")
return generate_welcome_ui()
elif action_id == "view_details":
restaurant_id = input_data.get("data", {}).get("restaurant_id")
print(f"Agent requested details for restaurant ID: {restaurant_id}")
# In a real app, you'd fetch more details here
restaurant = next((r for r in RESTAURANTS_DB if r["id"] == restaurant_id), None)
if restaurant:
return {
"elements": [
{"type": "card", "content": {
"elements": [
{"type": "text", "text": f"{restaurant['name']} Details", "style": "heading"},
{"type": "text", "text": f"Cuisine: {restaurant['cuisine']}", "style": "paragraph"},
{"type": "text", "text": f"Rating: {restaurant['rating']} ⭐", "style": "paragraph"},
{"type": "text", "text": f"Location: {restaurant['location']}", "style": "paragraph"},
{"type": "text", "text": f"Price Level: {restaurant['price']}", "style": "paragraph"},
{"type": "text", "text": f"Description: {restaurant['description']}", "style": "paragraph"},
{
"type": "button",
"label": "Back to Results",
"actionId": "back_to_results", # We'll need to store previous results for this
"style": "secondary"
}
]
}}
]
}
else:
return {"elements": [{"type": "text", "text": "Restaurant not found.", "style": "paragraph"}]}
# Fallback for unhandled actions
return {"elements": [{"type": "text", "text": "I didn't understand that request.", "style": "paragraph"}]}
# Example of how to "run" the agent locally for testing
if __name__ == "__main__":
print("--- Initial Agent Response ---")
initial_response = handle_agent_request({})
print(json.dumps(initial_response, indent=2))
print("\n--- Simulating User Search: 'Italian' ---")
mock_search_input = {
"actionId": "search_restaurants",
"values": {
"search_query": "Italian"
}
}
search_results_response = handle_agent_request(mock_search_input)
print(json.dumps(search_results_response, indent=2))
print("\n--- Simulating User Search: 'Vegan' (no results) ---")
mock_no_results_input = {
"actionId": "search_restaurants",
"values": {
"search_query": "Vegan"
}
}
no_results_response = handle_agent_request(mock_no_results_input)
print(json.dumps(no_results_response, indent=2))
print("\n--- Simulating User Clicks 'View Details' for Pasta Paradise (r1) ---")
mock_view_details_input = {
"actionId": "view_details",
"data": {"restaurant_id": "r1"}
}
details_response = handle_agent_request(mock_view_details_input)
print(json.dumps(details_response, indent=2))
print("\n--- Simulating User Clicks 'Start New Search' ---")
mock_new_search_input = {
"actionId": "start_new_search"
}
new_search_response = handle_agent_request(mock_new_search_input)
print(json.dumps(new_search_response, indent=2))
Explanation of Changes:
search_restaurants(query)function: This is our mock data retrieval logic. It iterates throughRESTAURANTS_DBand returns restaurants that match the query in their name, cuisine, location, or description.generate_restaurant_list_ui(restaurants)function:- It checks if
restaurantsis empty and provides a “no results” message if so. - For each found restaurant, it creates an A2UI
cardcontainingtextelements for its details. - Crucially, each restaurant card now includes a
buttonwithactionId: "view_details". This button also carriesdata: {"restaurant_id": restaurant["id"]}, which is how the agent knows which restaurant’s details to show when the button is clicked. This is a powerful pattern for passing contextual information with actions. - We wrap all restaurant cards in an A2UI
listcomponent for better organization. - A “Start New Search” button is added, which, when clicked, will send an action back to the agent with
actionId: "start_new_search".
- It checks if
handle_agent_requestupdates:- It now extracts
actionIdandvaluesfrom theinput_data. - If
actionIdis"search_restaurants", it callssearch_restaurantsand thengenerate_restaurant_list_uiwith the results. - If
actionIdis"start_new_search", it simply returns the initial welcome UI. - If
actionIdis"view_details", it retrieves therestaurant_idfrom thedatapayload, finds the corresponding restaurant, and generates a detailed view in a newcard.
- It now extracts
Run this updated script. Observe how the agent’s output changes based on the simulated user actions, demonstrating a multi-turn, interactive flow.
Step 3: Integrating with Real Data (Conceptual) and API Keys
While our RESTAURANTS_DB is simple, in a production environment, you would integrate with an actual restaurant API (e.g., Yelp Fusion API, Foursquare Places API, or a custom backend service).
Key Considerations for External APIs:
- API Keys: Most external APIs require an API key for authentication and rate limiting. You would typically load this key from environment variables (e.g.,
os.environ.get("YELP_API_KEY")) rather than hardcoding it. - Network Requests: Your agent’s
search_restaurantsfunction would make HTTP requests to the API endpoint (e.g., using Python’srequestslibrary). - Error Handling: Implement robust error handling for network issues, API rate limits, or invalid responses.
- Data Mapping: You’d need to map the API’s response structure to your agent’s internal restaurant data model, and then to A2UI components.
Example (Conceptual, not runnable without actual API setup):
import os
import requests
YELP_API_KEY = os.environ.get("YELP_API_KEY")
YELP_API_ENDPOINT = "https://api.yelp.com/v3/businesses/search"
def search_restaurants_yelp(query: str, location: str = "San Francisco"):
"""
Conceptual function to search restaurants using Yelp Fusion API.
Requires YELP_API_KEY to be set in environment variables.
"""
if not YELP_API_KEY:
print("Yelp API Key not found. Please set YELP_API_KEY environment variable.")
return []
headers = {
"Authorization": f"Bearer {YELP_API_KEY}"
}
params = {
"term": query,
"location": location,
"limit": 5 # Get top 5 results
}
try:
response = requests.get(YELP_API_ENDPOINT, headers=headers, params=params)
response.raise_for_status() # Raise an exception for bad status codes
data = response.json()
# Process Yelp's response into our simpler format
yelp_results = []
for business in data.get("businesses", []):
yelp_results.append({
"id": business["id"],
"name": business["name"],
"cuisine": ", ".join([cat["title"] for cat in business.get("categories", [])]),
"rating": business.get("rating"),
"location": business.get("location", {}).get("address1", ""),
"price": business.get("price", "N/A"),
"description": business.get("alias", "") # Using alias as a short description
})
return yelp_results
except requests.exceptions.RequestException as e:
print(f"Error calling Yelp API: {e}")
return []
Reference: Yelp Fusion API Documentation
You would replace the call to search_restaurants(query) with search_restaurants_yelp(query, location) (you’d need to extract location from the user’s query as well). This demonstrates how A2UI seamlessly integrates with powerful backend services, leveraging API keys for secure access.
Mini-Challenge: Enhance Filtering by Cuisine
Currently, our restaurant list is just a flat list. Let’s add a Select component to allow users to filter the displayed restaurants by cuisine type after an initial search.
Challenge:
- After displaying search results, add a
Selectcomponent above the list of restaurant cards. - Populate this
Selectcomponent with unique cuisine types available in the current search results. - When a user selects a cuisine, the agent should re-render the list, showing only restaurants of that chosen cuisine.
- Add an “All Cuisines” option to reset the filter.
Hint:
- You’ll need to update
generate_restaurant_list_uito include theSelectcomponent. - The
Selectcomponent will have anactionIdandoptions. Each option should have avalue(the cuisine type) and alabel. - You’ll need a new
elifblock inhandle_agent_requestto process theactionIdfrom theSelectcomponent. This new action will receive the selected cuisinevalue. - To implement “Back to Results” and filtering, you’ll need a way for the agent to remember the original search results. A simple way for a single-user agent is to store the
found_restaurantsin a variable (if it’s a multi-user system, you’d need per-user state management). For this challenge, you can re-run the search with the original query if the user selects a filter.
Common Pitfalls & Troubleshooting
- Invalid A2UI JSON: The most common issue is syntax errors or incorrect component properties in your generated JSON.
- Troubleshooting: Use a JSON validator (online or IDE extension). Refer to the official A2UI documentation for exact component schemas. A simple print of
json.dumps(response, indent=2)helps immensely for debugging.
- Troubleshooting: Use a JSON validator (online or IDE extension). Refer to the official A2UI documentation for exact component schemas. A simple print of
- Agent Not Responding to Actions: If clicking a button or submitting an input doesn’t trigger the expected agent behavior.
- Troubleshooting: Double-check that the
actionIdin your A2UI component (e.g.,button,input) exactly matches theactionIdyourhandle_agent_requestfunction is looking for. Ensuredataorvaluesare correctly passed and parsed.
- Troubleshooting: Double-check that the
- State Management: In a more complex multi-turn conversation, you might lose context (e.g., forgetting the original search query when a user clicks “View Details” and then “Back to Results”).
- Troubleshooting: For simple cases, you can pass necessary context as
datawithin action components (as we did withrestaurant_id). For more complex scenarios, you’ll need proper session management in your agent backend to store user-specific state.
- Troubleshooting: For simple cases, you can pass necessary context as
- API Integration Issues: When connecting to external APIs.
- Troubleshooting: Check API keys, network connectivity, API rate limits, and ensure your data parsing matches the API’s response format. Use
try-exceptblocks for robust error handling.
- Troubleshooting: Check API keys, network connectivity, API rate limits, and ensure your data parsing matches the API’s response format. Use
Summary
Congratulations! In this chapter, you’ve built a functional Interactive Restaurant Finder Agent from scratch, applying many of the A2UI concepts we’ve learned:
- Multi-Turn Interaction: Your agent now handles a conversational flow, from initial query to displaying results and handling detailed views.
- Dynamic UI Generation: You saw how an agent can dynamically construct A2UI components (
Card,Text,Input,Button,List) based on backend logic and data. - Action Handling: You implemented how agents receive and respond to user actions, passing contextual
datawith those actions. - Conceptual API Integration: We discussed how to integrate with external data sources using API keys, laying the groundwork for real-world applications.
This project demonstrates the power of A2UI in creating rich, agent-driven user experiences without the complexities of traditional frontend development. You’re well on your way to building sophisticated AI agents!
What’s Next? In the upcoming chapters, we’ll delve into more advanced A2UI features, explore best practices for agent design, and consider how to deploy your agent-driven interfaces to production environments.
References
- A2UI Official Website
- Google Developers Blog: Introducing A2UI
- A2UI GitHub Repository
- Yelp Fusion API Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.