Skip to content

Commit 1e052ad

Browse files
authored
Initial commit for a2ui_restaurant_finder and a generic a2UI extension. (#31)
* Initial commit for a2ui_restaurant_finder and a generic a2UI extension. * Update spec.md with catalog definition. * Only have data fetch be a tool. Pass the UI requirements in the prompt.
1 parent 5591d93 commit 1e052ad

37 files changed

+6952
-1
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# A2UI Extension Implementation
2+
3+
This is the Python implementation of the a2ui extension.
4+
5+
## Disclaimer
6+
7+
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
8+
9+
All data received from an external agent—including but not limited to its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide an AgentCard containing crafted data in its fields (e.g., description, name, skills.description). If this data is used without sanitization to construct prompts for a Large Language Model (LLM), it could expose your application to prompt injection attacks. Failure to properly validate and sanitize this data before use can introduce security vulnerabilities into your application.
10+
11+
Developers are responsible for implementing appropriate security measures, such as input validation and secure handling of credentials to protect their systems and users.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "a2ui_ext"
3+
version = "0.1.0"
4+
description = "a2ui Extension"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
dependencies = ["a2a-sdk>=0.3.0"]
8+
9+
[build-system]
10+
requires = ["hatchling"]
11+
build-backend = "hatchling.build"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# A2UI (Agent-to-Agent UI) Extension Spec
2+
## Overview
3+
This extension implements the A2UI (Agent-to-Agent UI) spec, a format for agents to send streaming, interactive user interfaces to clients.
4+
5+
## Extension URI
6+
The URI of this extension is https://github.com/google/a2ui/a2a_samples/a2ui_extension/spec/spec.md.
7+
8+
This is the only URI accepted for this extension.
9+
10+
## Core Concepts
11+
The A2UI extension is built on three main concepts:
12+
13+
Surfaces: A "Surface" is a distinct, controllable region of the client's UI. The spec uses a surfaceId to direct updates to specific surfaces (e.g., a main content area, a side panel, or a new chat bubble). This allows a single agent stream to manage multiple UI areas independently.
14+
15+
Catalog Definition Document: The a2ui extension is component-agnostic. All UI components (e.g., Text, Row, Button) and their stylings are defined in a separate Catalog Definition Schema. This allows clients and servers to negotiate which catalog to use.
16+
17+
Schemas: The a2ui extension is defined by three primary JSON schemas:
18+
19+
Catalog Definition Schema: A standard format for defining a library of components and styles.
20+
21+
Server-to-Client Message Schema: The core wire format for messages sent from the agent to the client (e.g., surfaceUpdate, dataModelUpdate).
22+
23+
Client-to-Server Event Schema: The core wire format for messages sent from the client to the agent (e.g., userAction, clientUiCapabilities).
24+
25+
Agent Capability Declaration
26+
Agents advertise their A2UI capabilities in their AgentCard within the AgentCapabilities.extensions list. The params object defines the agent's specific UI support, including which component catalogs it can generate and whether it accepts dynamic catalogs from the client.
27+
28+
Example AgentExtension block:
29+
30+
Parameter Definitions
31+
params.supportedSchemas: (REQUIRED) An array of strings, where each string is a URI pointing to a component Catalog Definition Schema that the agent can generate. This could include the default catalog or custom catalogs or both.
32+
33+
params.acceptsDynamicSchemas: (OPTIONAL) A boolean indicating if the agent can accept a clientUiCapabilities message containing a dynamicCatalog. If omitted, this defaults to false.
34+
35+
Client Capability Declaration
36+
The client-to-server spec includes a clientUiCapabilities message. If a client wishes to use a specific catalog (other than the server's default) or provide its own dynamic catalog, it MUST send this message after the connection is established and before the first user prompt.
37+
38+
This message allows the client to specify either:
39+
40+
A catalogUri: A URI for a known, shared catalog (which must be one of the supportedSchemas from the agent).
41+
42+
A dynamicCatalog: An inline Catalog Definition Schema object. This is only allowed if the agent's acceptsDynamicSchemas capability is true.
43+
44+
Extension Activation
45+
Clients indicate their desire to use the A2UI extension by specifying it via the transport-defined A2A extension activation mechanism.
46+
47+
For JSON-RPC and HTTP transports, this is indicated via the X-A2A-Extensions HTTP header.
48+
49+
For gRPC, this is indicated via the X-A2A-Extensions metadata value.
50+
51+
Activating this extension implies that the server can send A2UI-specific messages (like surfaceUpdate) and the client is expected to send A2UI-specific events (like userAction).
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import logging
2+
from typing import Any
3+
4+
from a2a.server.agent_execution import AgentExecutor, RequestContext
5+
from a2a.server.events.event_queue import EventQueue
6+
from a2a.types import AgentExtension, Task
7+
8+
logger = logging.getLogger(__name__)
9+
10+
# --- Define a2ui UI constants ---
11+
_CORE_PATH = "a2ui.org/ext/a2a-ui/v0.1"
12+
URI = f"https://{_CORE_PATH}"
13+
a2ui_MIME_TYPE = "application/json+a2ui"
14+
15+
16+
class a2uiExtension:
17+
"""A generic a2ui UI extension that activates UI mode."""
18+
19+
def agent_extension(self) -> AgentExtension:
20+
"""Get the AgentExtension representing this extension."""
21+
return AgentExtension(
22+
uri=URI,
23+
description="Provides a declarative a2ui UI JSON structure in messages.",
24+
params={
25+
"supportedSchemas": [
26+
"https://raw.githubusercontent.com/google/a2ui/refs/heads/main/schemas/v0.1/standard_catalog.json"
27+
],
28+
"acceptsDynamicSchemas": True,
29+
},
30+
)
31+
32+
def activate(self, context: RequestContext) -> bool:
33+
"""Checks if the a2ui UI extension was requested by the client."""
34+
if URI in context.requested_extensions:
35+
context.add_activated_extension(URI)
36+
return True
37+
return False
38+
39+
def wrap_executor(self, executor: AgentExecutor) -> AgentExecutor:
40+
"""Wrap an executor to activate the extension."""
41+
return _a2uiExecutor(executor, self)
42+
43+
44+
class _a2uiExecutor(AgentExecutor):
45+
"""Executor wrapper that activates the a2ui UI extension."""
46+
47+
def __init__(self, delegate: AgentExecutor, ext: a2uiExtension):
48+
self._delegate = delegate
49+
self._ext = ext
50+
51+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
52+
# The extension's ONLY job is to check for the header and log activation.
53+
logger.info(
54+
f"--- Client requested extensions: {context.requested_extensions} ---"
55+
)
56+
use_ui = self._ext.activate(context)
57+
if use_ui:
58+
logger.info("--- a2ui UI EXTENSION ACTIVATED ---")
59+
else:
60+
logger.info("--- a2ui UI EXTENSION *NOT* ACTIVE ---")
61+
62+
# All parsing logic is now handled correctly inside the delegate executor.
63+
# We pass the `use_ui` flag to the delegate.
64+
await self._delegate.execute(context, event_queue, use_ui=use_ui)
65+
66+
async def cancel(
67+
self, context: RequestContext, event_queue: EventQueue
68+
) -> Task | None:
69+
return await self._delegate.cancel(context, event_queue)
70+
71+
72+
__all__ = [
73+
"URI",
74+
"a2uiExtension",
75+
"a2ui_MIME_TYPE",
76+
]
Binary file not shown.

a2a_samples/a2ui_extension/src/a2ui_ext/py.typed

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# A2UI Restaurant finder and table reservation agent sample.
2+
3+
This sample uses the Agent Development Kit (ADK) along with the A2A protocol to create a simple "Restaurant finder and table reservation" agent that is hosted as an A2A server.
4+
5+
## Prerequisites
6+
7+
- Python 3.9 or higher
8+
- [UV](https://docs.astral.sh/uv/)
9+
- Access to an LLM and API Key
10+
11+
## Running the Sample
12+
13+
1. Navigate to the samples directory:
14+
15+
```bash
16+
cd a2a_samples/a2ui_restaurant_finder
17+
```
18+
19+
2. Create an environment file with your API key:
20+
21+
```bash
22+
echo "GEMINI_API_KEY=your_api_key_here" > .env
23+
```
24+
25+
3. Run an agent:
26+
27+
```bash
28+
uv run .
29+
```
30+
31+
32+
## Disclaimer
33+
34+
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
35+
36+
All data received from an external agent—including but not limited to its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide an AgentCard containing crafted data in its fields (e.g., description, name, skills.description). If this data is used without sanitization to construct prompts for a Large Language Model (LLM), it could expose your application to prompt injection attacks. Failure to properly validate and sanitize this data before use can introduce security vulnerabilities into your application.
37+
38+
Developers are responsible for implementing appropriate security measures, such as input validation and secure handling of credentials to protect their systems and users.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import agent
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import logging
2+
import os
3+
4+
import click
5+
from a2a.server.apps import A2AStarletteApplication
6+
from a2a.server.request_handlers import DefaultRequestHandler
7+
from a2a.server.tasks import InMemoryTaskStore
8+
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
9+
from a2ui_ext import a2uiExtension
10+
from agent import RestaurantAgent
11+
from agent_executor import RestaurantAgentExecutor
12+
from dotenv import load_dotenv
13+
from starlette.middleware.cors import CORSMiddleware
14+
from starlette.staticfiles import StaticFiles
15+
16+
load_dotenv()
17+
18+
logging.basicConfig(level=logging.INFO)
19+
logger = logging.getLogger(__name__)
20+
21+
22+
class MissingAPIKeyError(Exception):
23+
"""Exception for missing API key."""
24+
25+
26+
@click.command()
27+
@click.option("--host", default="localhost")
28+
@click.option("--port", default=10002)
29+
def main(host, port):
30+
try:
31+
# Check for API key only if Vertex AI is not configured
32+
if not os.getenv("GOOGLE_GENAI_USE_VERTEXAI") == "TRUE":
33+
if not os.getenv("GEMINI_API_KEY"):
34+
raise MissingAPIKeyError(
35+
"GEMINI_API_KEY environment variable not set and GOOGLE_GENAI_USE_VERTEXAI is not TRUE."
36+
)
37+
38+
hello_ext = a2uiExtension()
39+
capabilities = AgentCapabilities(
40+
streaming=True,
41+
extensions=[
42+
hello_ext.agent_extension(),
43+
],
44+
)
45+
skill = AgentSkill(
46+
id="find_restaurants",
47+
name="Find Restaurants Tool",
48+
description="Helps find restaurants based on user criteria (e.g., cuisine, location).",
49+
tags=["restaurant", "finder"],
50+
examples=["Find me the top 10 chinese restaurants in the US"],
51+
)
52+
53+
base_url = f"http://{host}:{port}"
54+
55+
agent_card = AgentCard(
56+
name="Restaurant Agent",
57+
description="This agent helps find restaurants based on user criteria.",
58+
url=base_url, # <-- Use base_url here
59+
version="1.0.0",
60+
default_input_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES,
61+
default_output_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES,
62+
capabilities=capabilities,
63+
skills=[skill],
64+
)
65+
66+
agent_executor = RestaurantAgentExecutor(base_url=base_url)
67+
68+
agent_executor = hello_ext.wrap_executor(agent_executor)
69+
70+
request_handler = DefaultRequestHandler(
71+
agent_executor=agent_executor,
72+
task_store=InMemoryTaskStore(),
73+
)
74+
server = A2AStarletteApplication(
75+
agent_card=agent_card, http_handler=request_handler
76+
)
77+
import uvicorn
78+
79+
app = server.build()
80+
81+
app.add_middleware(
82+
CORSMiddleware,
83+
allow_origins=["http://localhost:5173"],
84+
allow_credentials=True,
85+
allow_methods=["*"],
86+
allow_headers=["*"],
87+
)
88+
89+
app.mount("/static", StaticFiles(directory="images"), name="static")
90+
91+
uvicorn.run(app, host=host, port=port)
92+
except MissingAPIKeyError as e:
93+
logger.error(f"Error: {e}")
94+
exit(1)
95+
except Exception as e:
96+
logger.error(f"An error occurred during server startup: {e}")
97+
exit(1)
98+
99+
100+
if __name__ == "__main__":
101+
main()

0 commit comments

Comments
 (0)