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, and Button.
  • 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:

  1. Initial Greeting & Query: When a user first interacts, the agent needs to ask what they’re looking for.
  2. Processing Input: Once the user provides a search query (e.g., “Italian restaurants in downtown”), the agent must parse it.
  3. 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).
  4. Displaying Results: The retrieved data needs to be transformed into A2UI components that effectively display information like restaurant name, cuisine, rating, and location.
  5. 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:

flowchart TD A[User Starts Interaction] --> B{"Agent: 'What are you looking for?' + Input Field"} B --> C[User Enters Query] C --> D{Agent Processes Query & Searches Data} D --> E{Agent Generates UI with Search Results} E --> F["User Views Results & Interacts (e.g., Filter, Details)"] F --> D

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 restaurant Cards 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_DB as a simple list of dictionaries to act as our data source.
  • The generate_welcome_ui() function constructs an A2UI payload. It uses a card to encapsulate the content, a text element for the greeting, and an input element where the user can type their query.
  • Crucially, the input element includes an actions array with a submit action. This actionId: "search_restaurants" is how our agent will know what the user intends to do when they submit the input.
  • handle_agent_request is our agent’s entry point. If input_data is 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.

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 through RESTAURANTS_DB and returns restaurants that match the query in their name, cuisine, location, or description.
  • generate_restaurant_list_ui(restaurants) function:
    • It checks if restaurants is empty and provides a “no results” message if so.
    • For each found restaurant, it creates an A2UI card containing text elements for its details.
    • Crucially, each restaurant card now includes a button with actionId: "view_details". This button also carries data: {"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 list component 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".
  • handle_agent_request updates:
    • It now extracts actionId and values from the input_data.
    • If actionId is "search_restaurants", it calls search_restaurants and then generate_restaurant_list_ui with the results.
    • If actionId is "start_new_search", it simply returns the initial welcome UI.
    • If actionId is "view_details", it retrieves the restaurant_id from the data payload, finds the corresponding restaurant, and generates a detailed view in a new card.

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:

  1. 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.
  2. Network Requests: Your agent’s search_restaurants function would make HTTP requests to the API endpoint (e.g., using Python’s requests library).
  3. Error Handling: Implement robust error handling for network issues, API rate limits, or invalid responses.
  4. 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:

  1. After displaying search results, add a Select component above the list of restaurant cards.
  2. Populate this Select component with unique cuisine types available in the current search results.
  3. When a user selects a cuisine, the agent should re-render the list, showing only restaurants of that chosen cuisine.
  4. Add an “All Cuisines” option to reset the filter.

Hint:

  • You’ll need to update generate_restaurant_list_ui to include the Select component.
  • The Select component will have an actionId and options. Each option should have a value (the cuisine type) and a label.
  • You’ll need a new elif block in handle_agent_request to process the actionId from the Select component. This new action will receive the selected cuisine value.
  • 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_restaurants in 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

  1. 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.
  2. 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 actionId in your A2UI component (e.g., button, input) exactly matches the actionId your handle_agent_request function is looking for. Ensure data or values are correctly passed and parsed.
  3. 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 data within action components (as we did with restaurant_id). For more complex scenarios, you’ll need proper session management in your agent backend to store user-specific state.
  4. 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-except blocks for robust error handling.

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 data with 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

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.