diff --git a/.gitignore b/.gitignore
index 25ca3ad..85c491c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@
.ruff_cache
uv.lock
.venv
-**/node_modules
\ No newline at end of file
+**/node_modules
diff --git a/LICENSE b/LICENSE
index 7a4a3ea..d645695 100644
--- a/LICENSE
+++ b/LICENSE
@@ -199,4 +199,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
- limitations under the License.
\ No newline at end of file
+ limitations under the License.
diff --git a/README.md b/README.md
index 2e460f0..e30a896 100644
--- a/README.md
+++ b/README.md
@@ -25,39 +25,37 @@ Universal Commerce Protocol (UCP).
A reference implementation of a UCP Merchant Server using Python and FastAPI.
-* **Server**: [Documentation](rest/python/server/README.md)
-
- * Located in `rest/python/server/`.
- * Demonstrates capability discovery, checkout session management, payment
- processing, and order lifecycle.
- * Includes simulation endpoints for testing.
-
-* **Client**:
- [Happy Path Script](rest/python/client/flower_shop/simple_happy_path_client.py)
-
- * Located in `rest/python/client/`.
- * A script demonstrating a full "happy path" user journey (discovery ->
- checkout -> payment).
+- **Server**: [Documentation](rest/python/server/README.md)
+ - Located in `rest/python/server/`.
+ - Demonstrates capability discovery, checkout session management, payment
+ processing, and order lifecycle.
+ - Includes simulation endpoints for testing.
+
+- **Client**:
+ [Happy Path Script](rest/python/client/flower_shop/simple_happy_path_client.py)
+ - Located in `rest/python/client/`.
+ - A script demonstrating a full "happy path" user journey (discovery ->
+ checkout -> payment).
### Node.js
A reference implementation of a UCP Merchant Server using Node.js, Hono, and
Zod.
-* **Server**: [Documentation](rest/nodejs/README.md)
- * Located in `rest/nodejs/`.
- * Demonstrates implementation of UCP specifications for shopping,
- checkout, and order management using a Node.js stack.
+- **Server**: [Documentation](rest/nodejs/README.md)
+ - Located in `rest/nodejs/`.
+ - Demonstrates implementation of UCP specifications for shopping,
+ checkout, and order management using a Node.js stack.
### A2A (Agent-to-Agent)
An AI-powered retail agent implementing UCP via the A2A protocol.
-* **Cymbal Retail Agent**: [Documentation](a2a/README.md)
- * Located in `a2a/business_agent/`.
- * Demonstrates A2A protocol integration with UCP Extension.
- * Includes AI-powered shopping assistant with Google ADK and Gemini.
- * React-based chat client for user interaction.
+- **Cymbal Retail Agent**: [Documentation](a2a/README.md)
+ - Located in `a2a/business_agent/`.
+ - Demonstrates A2A protocol integration with UCP Extension.
+ - Includes AI-powered shopping assistant with Google ADK and Gemini.
+ - React-based chat client for user interaction.
## Getting Started
diff --git a/a2a/DEVELOPER_GUIDE.md b/a2a/DEVELOPER_GUIDE.md
index c46b7f0..834f7aa 100644
--- a/a2a/DEVELOPER_GUIDE.md
+++ b/a2a/DEVELOPER_GUIDE.md
@@ -13,6 +13,7 @@ This guide is for developers who want to understand how the sample works interna
**Prerequisites:** Complete the [Quick Start in README.md](README.md#quick-start) first.
**What you'll learn:**
+
- System architecture and component responsibilities
- How UCP and A2A protocols integrate with ADK
- The checkout state machine and commerce flows
@@ -22,16 +23,16 @@ This guide is for developers who want to understand how the sample works interna
Choose your path based on your goal:
-| Goal | Start Here | Then Read |
-|------|------------|-----------|
-| **New to AI agents?** | [Glossary](docs/00-glossary.md) | This guide → 01-architecture |
-| **Understand the system** | [Architecture](docs/01-architecture.md) | 02-adk-agent → 03-ucp-integration |
-| **Add a new tool** | [ADK Agent](docs/02-adk-agent.md) | 06-extending |
-| **Modify checkout flow** | [Commerce Flows](docs/04-commerce-flows.md) | 06-extending |
-| **Customize the UI** | [Frontend](docs/05-frontend.md) | - |
-| **Debug an issue** | [Testing Guide](docs/07-testing-guide.md) | - |
-| **Deploy to production** | [Production Notes](docs/08-production-notes.md) | - |
-| **Use AI assistant** | [SKILLS.md](SKILLS.md) | Context for Claude Code, Gemini CLI, Cursor, Codex |
+| Goal | Start Here | Then Read |
+| ------------------------- | ----------------------------------------------- | -------------------------------------------------- |
+| **New to AI agents?** | [Glossary](docs/00-glossary.md) | This guide → 01-architecture |
+| **Understand the system** | [Architecture](docs/01-architecture.md) | 02-adk-agent → 03-ucp-integration |
+| **Add a new tool** | [ADK Agent](docs/02-adk-agent.md) | 06-extending |
+| **Modify checkout flow** | [Commerce Flows](docs/04-commerce-flows.md) | 06-extending |
+| **Customize the UI** | [Frontend](docs/05-frontend.md) | - |
+| **Debug an issue** | [Testing Guide](docs/07-testing-guide.md) | - |
+| **Deploy to production** | [Production Notes](docs/08-production-notes.md) | - |
+| **Use AI assistant** | [SKILLS.md](SKILLS.md) | Context for Claude Code, Gemini CLI, Cursor, Codex |
## Architecture
@@ -44,39 +45,39 @@ Choose your path based on your goal:
### Key Files
-| File | Purpose |
-|------|---------|
-| `business_agent/src/business_agent/agent.py` | ADK agent + 8 tools |
-| `business_agent/src/business_agent/store.py` | Checkout state machine |
-| `business_agent/src/business_agent/agent_executor.py` | A2A ↔ ADK bridge |
-| `chat-client/App.tsx` | React app + A2A messaging |
+| File | Purpose |
+| ----------------------------------------------------- | ------------------------- |
+| `business_agent/src/business_agent/agent.py` | ADK agent + 8 tools |
+| `business_agent/src/business_agent/store.py` | Checkout state machine |
+| `business_agent/src/business_agent/agent_executor.py` | A2A ↔ ADK bridge |
+| `chat-client/App.tsx` | React app + A2A messaging |
### Endpoints
-| Endpoint | Purpose |
-|----------|---------|
-| `GET /.well-known/agent-card.json` | A2A agent discovery |
-| `GET /.well-known/ucp` | UCP merchant profile |
-| `POST /` | A2A JSON-RPC endpoint |
+| Endpoint | Purpose |
+| ---------------------------------- | --------------------- |
+| `GET /.well-known/agent-card.json` | A2A agent discovery |
+| `GET /.well-known/ucp` | UCP merchant profile |
+| `POST /` | A2A JSON-RPC endpoint |
### State Keys
-| Key | Purpose |
-|-----|---------|
+| Key | Purpose |
+| ------------------ | ------------------------ |
| `user:checkout_id` | Current checkout session |
-| `__ucp_metadata__` | Negotiated capabilities |
-| `__payment_data__` | Payment instrument |
+| `__ucp_metadata__` | Negotiated capabilities |
+| `__payment_data__` | Payment instrument |
## Deep Dive Guides
-| Guide | Topics |
-|-------|--------|
-| [Glossary](docs/00-glossary.md) | Key terms, acronyms, state keys |
-| [Architecture](docs/01-architecture.md) | System components, data flow |
-| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, session management |
-| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation |
-| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment |
-| [Frontend](docs/05-frontend.md) | React components, A2A client |
-| [Extending](docs/06-extending.md) | Add tools, products, capabilities |
-| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting |
-| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist |
+| Guide | Topics |
+| ----------------------------------------------- | ------------------------------------ |
+| [Glossary](docs/00-glossary.md) | Key terms, acronyms, state keys |
+| [Architecture](docs/01-architecture.md) | System components, data flow |
+| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, session management |
+| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation |
+| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment |
+| [Frontend](docs/05-frontend.md) | React components, A2A client |
+| [Extending](docs/06-extending.md) | Add tools, products, capabilities |
+| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting |
+| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist |
diff --git a/a2a/README.md b/a2a/README.md
index 7e22ad1..b8cd431 100644
--- a/a2a/README.md
+++ b/a2a/README.md
@@ -54,6 +54,7 @@ The sample uses **[Google ADK](https://google.github.io/adk-docs/)** (Agent Deve
**Key points:**
+
- **Client** sends requests with `UCP-Agent` header containing its profile URL
- **Cymbal Retail Agent** serves both `/.well-known/agent-card.json` (A2A) and `/.well-known/ucp` (UCP Profile)
- **Capability Negotiation** happens before processing - agent and client agree on supported features
@@ -86,12 +87,14 @@ uv run business_agent
```
**Expected output:**
+
```
INFO: Started server process
INFO: Uvicorn running on http://0.0.0.0:10999
```
This starts the Cymbal Retail Agent on port 10999. You can verify by accessing:
+
- **Agent Card:** http://localhost:10999/.well-known/agent-card.json
- **UCP Profile:** http://localhost:10999/.well-known/ucp
@@ -127,6 +130,7 @@ npm run dev
```
**Expected output:**
+
```
VITE v5.x.x ready
➜ Local: http://localhost:3000/
@@ -164,18 +168,19 @@ The Chat Client UCP Profile can be found at http://localhost:3000/profile/agent-
Ready to understand how it works?
-| Goal | Resource |
-|------|----------|
-| **Understand the architecture** | [Developer Guide](DEVELOPER_GUIDE.md) |
-| **Deep dive into code** | [Architecture](docs/01-architecture.md) |
-| **Extend the sample** | [Extending Guide](docs/06-extending.md) |
-| **AI assistant context** | [SKILLS.md](SKILLS.md) - Context for Claude Code, Gemini CLI, Cursor, Codex |
+| Goal | Resource |
+| ------------------------------- | --------------------------------------------------------------------------- |
+| **Understand the architecture** | [Developer Guide](DEVELOPER_GUIDE.md) |
+| **Deep dive into code** | [Architecture](docs/01-architecture.md) |
+| **Extend the sample** | [Extending Guide](docs/06-extending.md) |
+| **AI assistant context** | [SKILLS.md](SKILLS.md) - Context for Claude Code, Gemini CLI, Cursor, Codex |
## What is UCP?
**Universal Commerce Protocol (UCP)** is an open standard that enables interoperability between commerce platforms, merchants, and payment providers. It provides standardized data types for commerce transactions.
This sample uses the following UCP capabilities:
+
- `dev.ucp.shopping.checkout` - Checkout session management with status lifecycle: `incomplete` → `ready_for_complete` → `completed`
- `dev.ucp.shopping.fulfillment` - Shipping and delivery handling
- `dev.ucp.shopping.discount` - Discount and promotional codes
@@ -184,11 +189,11 @@ This sample uses the following UCP capabilities:
## Technology Stack
-| Technology | Purpose | Used For |
-|------------|---------|----------|
-| **[Google ADK](https://google.github.io/adk-docs/)** | Agent Framework | AI agent with tools, Gemini LLM integration, session management |
-| **[A2A Protocol](https://a2a-protocol.org/latest/)** | Communication | Agent discovery via Agent Card, JSON-RPC messaging, task management |
-| **[UCP](https://ucp.dev)** | Commerce Standard | Standardized product, checkout, payment, and order data types |
+| Technology | Purpose | Used For |
+| ---------------------------------------------------- | ----------------- | ------------------------------------------------------------------- |
+| **[Google ADK](https://google.github.io/adk-docs/)** | Agent Framework | AI agent with tools, Gemini LLM integration, session management |
+| **[A2A Protocol](https://a2a-protocol.org/latest/)** | Communication | Agent discovery via Agent Card, JSON-RPC messaging, task management |
+| **[UCP](https://ucp.dev)** | Commerce Standard | Standardized product, checkout, payment, and order data types |
## Related Resources
diff --git a/a2a/SKILLS.md b/a2a/SKILLS.md
index 04eba4e..05fbf17 100644
--- a/a2a/SKILLS.md
+++ b/a2a/SKILLS.md
@@ -21,14 +21,14 @@ AI-powered shopping agent built with Google ADK, demonstrating UCP commerce inte
## Tech Stack
-| Layer | Technology |
-|-------|------------|
-| Agent Framework | [Google ADK](https://google.github.io/adk-docs/) (Agent Development Kit) |
-| LLM | Gemini 3.0 Flash |
-| Commerce Protocol | [UCP](https://ucp.dev/) (Universal Commerce Protocol) |
-| Agent Protocol | [A2A](https://a2a-protocol.org/) (Agent-to-Agent) JSON-RPC 2.0 |
-| Backend | Python 3.13, Uvicorn, Starlette, Pydantic |
-| Frontend | React 19, TypeScript, Vite, Tailwind |
+| Layer | Technology |
+| ----------------- | ------------------------------------------------------------------------ |
+| Agent Framework | [Google ADK](https://google.github.io/adk-docs/) (Agent Development Kit) |
+| LLM | Gemini 3.0 Flash |
+| Commerce Protocol | [UCP](https://ucp.dev/) (Universal Commerce Protocol) |
+| Agent Protocol | [A2A](https://a2a-protocol.org/) (Agent-to-Agent) JSON-RPC 2.0 |
+| Backend | Python 3.13, Uvicorn, Starlette, Pydantic |
+| Frontend | React 19, TypeScript, Vite, Tailwind |
## Directory Structure
@@ -58,12 +58,12 @@ a2a/
## Core Concepts
-| Term | Definition |
-|------|------------|
-| **A2A** | Agent-to-Agent Protocol - How agents discover and communicate |
-| **UCP** | Universal Commerce Protocol - Standard commerce data types |
-| **ADK** | Agent Development Kit - Google's framework for building agents |
-| **Tool** | Python function the LLM can invoke (has `ToolContext` parameter) |
+| Term | Definition |
+| -------------- | ------------------------------------------------------------------ |
+| **A2A** | Agent-to-Agent Protocol - How agents discover and communicate |
+| **UCP** | Universal Commerce Protocol - Standard commerce data types |
+| **ADK** | Agent Development Kit - Google's framework for building agents |
+| **Tool** | Python function the LLM can invoke (has `ToolContext` parameter) |
| **Capability** | Feature set the agent supports (e.g., `dev.ucp.shopping.checkout`) |
## State Keys (constants.py)
@@ -92,16 +92,16 @@ UCP_AGENT_HEADER = "UCP-Agent" # HTTP header for client pr
## Agent Tools (agent.py)
-| Tool | Purpose | Returns |
-|------|---------|---------|
-| `search_shopping_catalog(query)` | Search products by keyword | ProductResults |
-| `add_to_checkout(product_id, quantity)` | Add item to checkout | Checkout |
-| `remove_from_checkout(product_id)` | Remove item from checkout | Checkout |
-| `update_checkout(product_id, quantity)` | Update item quantity | Checkout |
-| `get_checkout()` | Get current checkout state | Checkout |
-| `update_customer_details(email, address...)` | Set buyer and delivery info | Checkout |
-| `start_payment()` | Validate checkout, set ready status | Checkout |
-| `complete_checkout()` | Process payment, create order | Checkout + OrderConfirmation |
+| Tool | Purpose | Returns |
+| -------------------------------------------- | ----------------------------------- | ---------------------------- |
+| `search_shopping_catalog(query)` | Search products by keyword | ProductResults |
+| `add_to_checkout(product_id, quantity)` | Add item to checkout | Checkout |
+| `remove_from_checkout(product_id)` | Remove item from checkout | Checkout |
+| `update_checkout(product_id, quantity)` | Update item quantity | Checkout |
+| `get_checkout()` | Get current checkout state | Checkout |
+| `update_customer_details(email, address...)` | Set buyer and delivery info | Checkout |
+| `start_payment()` | Validate checkout, set ready status | Checkout |
+| `complete_checkout()` | Process payment, create order | Checkout + OrderConfirmation |
## Checkout State Machine
@@ -111,11 +111,11 @@ incomplete → ready_for_complete → completed
add item start_payment complete_checkout
```
-| State | Meaning | Transition |
-|-------|---------|------------|
-| `incomplete` | Missing buyer email or fulfillment address | Add required info |
-| `ready_for_complete` | All info collected, awaiting payment | Call `complete_checkout()` |
-| `completed` | Order created with OrderConfirmation | Terminal state |
+| State | Meaning | Transition |
+| -------------------- | ------------------------------------------ | -------------------------- |
+| `incomplete` | Missing buyer email or fulfillment address | Add required info |
+| `ready_for_complete` | All info collected, awaiting payment | Call `complete_checkout()` |
+| `completed` | Order created with OrderConfirmation | Terminal state |
## UCP Capabilities
@@ -129,6 +129,7 @@ dev.ucp.shopping.buyer_consent # Consent management (extends checkout)
## Common Tasks
### Add a New Tool
+
```python
# In agent.py
def my_tool(tool_context: ToolContext, param: str) -> dict:
@@ -155,19 +156,27 @@ root_agent = Agent(..., tools=[..., my_tool])
```
### Add a Product
+
Edit `data/products.json`:
+
```json
{
"productID": "NEW-001",
"name": "New Product",
"image": ["http://localhost:10999/images/new.jpg"],
- "brand": {"name": "Brand"},
- "offers": {"price": "9.99", "priceCurrency": "USD", "availability": "InStock"}
+ "brand": { "name": "Brand" },
+ "offers": {
+ "price": "9.99",
+ "priceCurrency": "USD",
+ "availability": "InStock"
+ }
}
```
### Modify Checkout Flow
+
Key methods in `store.py`:
+
- `add_to_checkout()` - Creates checkout, adds items
- `_recalculate_checkout()` - Updates totals, tax, shipping
- `start_payment()` - Validates readiness, transitions state
@@ -175,15 +184,15 @@ Key methods in `store.py`:
## Key Files for Changes
-| Change | File |
-|--------|------|
-| Add/modify tools | `agent.py` |
-| Checkout logic | `store.py` |
-| A2A/ADK bridging | `agent_executor.py` |
-| UCP profiles | `data/ucp.json`, `chat-client/profile/agent_profile.json` |
-| Products | `data/products.json` |
-| Frontend components | `chat-client/components/` |
-| Frontend types | `chat-client/types.ts` |
+| Change | File |
+| ------------------- | --------------------------------------------------------- |
+| Add/modify tools | `agent.py` |
+| Checkout logic | `store.py` |
+| A2A/ADK bridging | `agent_executor.py` |
+| UCP profiles | `data/ucp.json`, `chat-client/profile/agent_profile.json` |
+| Products | `data/products.json` |
+| Frontend components | `chat-client/components/` |
+| Frontend types | `chat-client/types.ts` |
## Commands
@@ -216,43 +225,45 @@ return {"message": "Error description", "status": "error"}
> **WARNING**: This sample is NOT production-ready. See `docs/08-production-notes.md`.
-| Component | Current | Production |
-|-----------|---------|------------|
-| Session Storage | In-memory | Redis |
-| Checkout Storage | Python dict | PostgreSQL |
-| Authentication | None | JWT/API key |
-| Secrets | Plaintext .env | Secret Manager |
+| Component | Current | Production |
+| ---------------- | -------------- | -------------- |
+| Session Storage | In-memory | Redis |
+| Checkout Storage | Python dict | PostgreSQL |
+| Authentication | None | JWT/API key |
+| Secrets | Plaintext .env | Secret Manager |
## Documentation
-| Guide | Topics |
-|-------|--------|
-| [Glossary](docs/00-glossary.md) | Key terms, acronyms, external resources |
-| [Architecture](docs/01-architecture.md) | System components, data flow, mock store |
-| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, prompt engineering |
-| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation |
-| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment flow |
-| [Frontend](docs/05-frontend.md) | React components, A2A client |
-| [Extending](docs/06-extending.md) | Add tools, products, capabilities |
-| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting |
-| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist |
+| Guide | Topics |
+| ----------------------------------------------- | ---------------------------------------- |
+| [Glossary](docs/00-glossary.md) | Key terms, acronyms, external resources |
+| [Architecture](docs/01-architecture.md) | System components, data flow, mock store |
+| [ADK Agent](docs/02-adk-agent.md) | Tools, callbacks, prompt engineering |
+| [UCP Integration](docs/03-ucp-integration.md) | Capabilities, profiles, negotiation |
+| [Commerce Flows](docs/04-commerce-flows.md) | Checkout lifecycle, payment flow |
+| [Frontend](docs/05-frontend.md) | React components, A2A client |
+| [Extending](docs/06-extending.md) | Add tools, products, capabilities |
+| [Testing Guide](docs/07-testing-guide.md) | Testing, debugging, troubleshooting |
+| [Production Notes](docs/08-production-notes.md) | Security gaps, deployment checklist |
## External Resources
-| Resource | URL |
-|----------|-----|
-| **ADK Docs** | https://google.github.io/adk-docs/ |
-| **A2A Protocol** | https://a2a-protocol.org/latest/ |
+| Resource | URL |
+| --------------------- | --------------------------------------- |
+| **ADK Docs** | https://google.github.io/adk-docs/ |
+| **A2A Protocol** | https://a2a-protocol.org/latest/ |
| **UCP Specification** | https://ucp.dev/specification/overview/ |
-| **Gemini API** | https://ai.google.dev/gemini-api/docs |
+| **Gemini API** | https://ai.google.dev/gemini-api/docs |
## Dependencies
**Backend** (pyproject.toml):
+
- `google-adk[a2a]>=1.22.0`
- `ucp-sdk==0.1.0`
- `pydantic>=2.12.3`
**Frontend** (package.json):
+
- `react ^19.2.0`
- `vite ^6.2.0`
diff --git a/a2a/business_agent/README.md b/a2a/business_agent/README.md
index 7a45684..54c32f5 100644
--- a/a2a/business_agent/README.md
+++ b/a2a/business_agent/README.md
@@ -30,4 +30,4 @@ Example agent implementing A2A Extension for UCP
2. Copy env.example to .env and update it with relevant Gemini API key.
3. Run `uv run business_agent`
4. This starts the Cymbal Retail Agent on port 10999. You can verify by accessing
-the agent card at http://localhost:10999/.well-known/agent-card.json
+ the agent card at http://localhost:10999/.well-known/agent-card.json
diff --git a/a2a/business_agent/env.example b/a2a/business_agent/env.example
index fea08a8..8d82236 100644
--- a/a2a/business_agent/env.example
+++ b/a2a/business_agent/env.example
@@ -1 +1 @@
-GOOGLE_API_KEY=
\ No newline at end of file
+GOOGLE_API_KEY=
diff --git a/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py b/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py
index 76828c7..5ebc01a 100644
--- a/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py
+++ b/a2a/business_agent/src/business_agent/a2a_extensions/base_extension.py
@@ -19,14 +19,13 @@
from a2a.server.agent_execution import RequestContext
from a2a.types import AgentCard, AgentExtension
+
class A2AExtensionBase(ABC):
"""Base class for A2A extensions."""
URI: str
- def __init__(
- self, description: str = "", params: dict[str, Any] | None = None
- ):
+ def __init__(self, description: str = "", params: dict[str, Any] | None = None):
"""Initialize the extension base.
Args:
diff --git a/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py b/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py
index c65f390..43c6a22 100644
--- a/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py
+++ b/a2a/business_agent/src/business_agent/a2a_extensions/ucp_extension.py
@@ -18,6 +18,7 @@
from ..constants import A2A_UCP_EXTENSION_URL
from .base_extension import A2AExtensionBase
+
class UcpExtension(A2AExtensionBase):
"""UCP extension implementation."""
diff --git a/a2a/business_agent/src/business_agent/agent.py b/a2a/business_agent/src/business_agent/agent.py
index 9f4f3b9..b619ae7 100644
--- a/a2a/business_agent/src/business_agent/agent.py
+++ b/a2a/business_agent/src/business_agent/agent.py
@@ -45,7 +45,7 @@
def _create_error_response(message: str) -> dict:
- return {"message": message, "status": "error"}
+ return {"message": message, "status": "error"}
def search_shopping_catalog(tool_context: ToolContext, query: str) -> dict:
@@ -88,9 +88,7 @@ def add_to_checkout(
ucp_metadata = tool_context.state.get(ADK_UCP_METADATA_STATE)
if not ucp_metadata:
- return _create_error_response(
- "There was an error creating UCP metadata"
- )
+ return _create_error_response("There was an error creating UCP metadata")
try:
checkout = store.add_to_checkout(
@@ -139,18 +137,14 @@ def remove_from_checkout(tool_context: ToolContext, product_id: str) -> dict:
}
except ValueError:
logging.exception(
- "There was an error removing item from checkout, "
- "please retry later."
+ "There was an error removing item from checkout, please retry later."
)
return _create_error_response(
- "There was an error removing item from checkout, "
- "please retry later."
+ "There was an error removing item from checkout, please retry later."
)
-def update_checkout(
- tool_context: ToolContext, product_id: str, quantity: int
-) -> dict:
+def update_checkout(tool_context: ToolContext, product_id: str, quantity: int) -> dict:
"""Update the quantity of a product in the checkout session.
Args:
@@ -169,9 +163,9 @@ def update_checkout(
try:
return {
UCP_CHECKOUT_KEY: (
- store.update_checkout(
- checkout_id, product_id, quantity
- ).model_dump(mode="json")
+ store.update_checkout(checkout_id, product_id, quantity).model_dump(
+ mode="json"
+ )
),
"status": "success",
}
@@ -285,9 +279,7 @@ async def complete_checkout(tool_context: ToolContext) -> dict:
checkout = store.get_checkout(checkout_id)
if checkout is None:
- return _create_error_response(
- "Checkout not found for the current session."
- )
+ return _create_error_response("Checkout not found for the current session.")
payment_data: dict[str, Any] = tool_context.state.get(ADK_PAYMENT_STATE)
@@ -307,15 +299,11 @@ async def complete_checkout(tool_context: ToolContext) -> dict:
)
if task is None:
- return _create_error_response(
- "Failed to receive a valid response from MPP"
- )
+ return _create_error_response("Failed to receive a valid response from MPP")
if task.status is not None and task.status.state == TaskState.completed:
payment_instrument = payment_data.get(UCP_PAYMENT_DATA_KEY)
- checkout.payment.selected_instrument_id = (
- payment_instrument.root.id
- )
+ checkout.payment.selected_instrument_id = payment_instrument.root.id
checkout.payment.instruments = [payment_instrument]
response = store.place_order(checkout_id)
@@ -332,8 +320,7 @@ async def complete_checkout(tool_context: ToolContext) -> dict:
except Exception:
logging.exception("There was an error completing the checkout.")
return _create_error_response(
- "Sorry, there was an error completing the checkout, "
- "please try again."
+ "Sorry, there was an error completing the checkout, please try again."
)
diff --git a/a2a/business_agent/src/business_agent/agent_executor.py b/a2a/business_agent/src/business_agent/agent_executor.py
index 96abc16..2d10709 100644
--- a/a2a/business_agent/src/business_agent/agent_executor.py
+++ b/a2a/business_agent/src/business_agent/agent_executor.py
@@ -135,9 +135,7 @@ async def cancel(
"Cancellation is not implemented for ADKAgentExecutor."
)
- async def _get_or_create_session(
- self, context: RequestContext, user_id: str
- ):
+ async def _get_or_create_session(self, context: RequestContext, user_id: str):
"""Get an existing session or create a new one.
Args:
@@ -237,9 +235,7 @@ def _prepare_input(
if key in data_part:
value = data_part.pop(key)
if key == UCP_PAYMENT_DATA_KEY:
- payment_payload[key] = (
- PaymentInstrument.model_validate(value)
- )
+ payment_payload[key] = PaymentInstrument.model_validate(value)
else:
payment_payload[key] = value
@@ -295,9 +291,7 @@ async def _run_agent_and_process_response(
list[Part]: The response parts.
"""
- content = types.Content(
- role="user", parts=[types.Part.from_text(text=query)]
- )
+ content = types.Content(role="user", parts=[types.Part.from_text(text=query)])
state_delta = self._build_initial_state_delta(
context, ucp_metadata, payment_data
diff --git a/a2a/business_agent/src/business_agent/data/agent_card.json b/a2a/business_agent/src/business_agent/data/agent_card.json
index 4732431..08e2f73 100644
--- a/a2a/business_agent/src/business_agent/data/agent_card.json
+++ b/a2a/business_agent/src/business_agent/data/agent_card.json
@@ -33,16 +33,8 @@
],
"streaming": false
},
- "defaultInputModes": [
- "text",
- "text/plain",
- "application/json"
- ],
- "defaultOutputModes": [
- "text",
- "text/plain",
- "application/json"
- ],
+ "defaultInputModes": ["text", "text/plain", "application/json"],
+ "defaultOutputModes": ["text", "text/plain", "application/json"],
"description": "SuperStore Merchant Agent",
"name": "SuperStore Merchant Agent",
"preferredTransport": "JSONRPC",
@@ -54,16 +46,10 @@
"skills": [
{
"description": "Helps with product search for given user criteria",
- "examples": [
- "Help me find a shirt for my weekend trip"
- ],
+ "examples": ["Help me find a shirt for my weekend trip"],
"id": "product_search",
"name": "Perform product search",
- "tags": [
- "shopping",
- "search",
- "catalog search"
- ]
+ "tags": ["shopping", "search", "catalog search"]
},
{
"description": "Adds Checkout functionality for the agent",
@@ -74,11 +60,9 @@
],
"id": "checkout",
"name": "Checkout",
- "tags": [
- "checkout"
- ]
+ "tags": ["checkout"]
}
],
"url": "http://localhost:10999",
"version": "1.0.0"
-}
\ No newline at end of file
+}
diff --git a/a2a/business_agent/src/business_agent/data/products.json b/a2a/business_agent/src/business_agent/data/products.json
index 880ad33..03e326b 100644
--- a/a2a/business_agent/src/business_agent/data/products.json
+++ b/a2a/business_agent/src/business_agent/data/products.json
@@ -4,9 +4,7 @@
"productID": "BISC-001",
"name": "Chocochip Cookies",
"sku": "COOKIES-001",
- "image": [
- "http://localhost:10999/images/cookies.jpg"
- ],
+ "image": ["http://localhost:10999/images/cookies.jpg"],
"brand": {
"@type": "Brand",
"name": "CookieCo"
@@ -25,15 +23,13 @@
"gtin": "9876543210125",
"mpn": "CC-SB-001",
"category": "Groceries > Snacks > Cookies & Biscuits"
- },
+ },
{
"@type": "Product",
"productID": "STRAW-001",
"name": "Fresh Strawberries",
"sku": "STRAW-001",
- "image": [
- "http://localhost:10999/images/strawberries.jpg"
- ],
+ "image": ["http://localhost:10999/images/strawberries.jpg"],
"brand": {
"@type": "Brand",
"name": "FarmFresh"
@@ -58,9 +54,7 @@
"productID": "CHIPS-001",
"name": "Classic Potato Chips",
"sku": "CHIPS-001",
- "image": [
- "http://localhost:10999/images/chips.jpg"
- ],
+ "image": ["http://localhost:10999/images/chips.jpg"],
"brand": {
"@type": "Brand",
"name": "SaltySnacks"
@@ -79,14 +73,13 @@
"gtin": "9876543210128",
"mpn": "SS-PC-001",
"category": "Groceries > Snacks > Chips & Crisps"
- },{
+ },
+ {
"@type": "Product",
"productID": "SW-CHIPS-001",
"name": "Baked Sweet Potato Chips",
"sku": "SW-CHIPS-001",
- "image": [
- "http://localhost:10999/images/chips.jpg"
- ],
+ "image": ["http://localhost:10999/images/chips.jpg"],
"brand": {
"@type": "Brand",
"name": "SaltySnacks"
@@ -111,9 +104,7 @@
"productID": "O-COOKIES-001",
"name": "Classic Oat Cookies",
"sku": "O-COOKIES-001",
- "image": [
- "http://localhost:10999/images/oat_cookies.jpg"
- ],
+ "image": ["http://localhost:10999/images/oat_cookies.jpg"],
"brand": {
"@type": "Brand",
"name": "CookieCo"
@@ -138,9 +129,7 @@
"productID": "NUTRIBAR-001",
"name": "Nutri-Bar",
"sku": "NUTRIBAR-001",
- "image": [
- "http://localhost:10999/images/nutribar.jpg"
- ],
+ "image": ["http://localhost:10999/images/nutribar.jpg"],
"brand": {
"@type": "Brand",
"name": "HealthEats"
diff --git a/a2a/business_agent/src/business_agent/data/ucp.json b/a2a/business_agent/src/business_agent/data/ucp.json
index 034ff6d..75e31d8 100644
--- a/a2a/business_agent/src/business_agent/data/ucp.json
+++ b/a2a/business_agent/src/business_agent/data/ucp.json
@@ -27,18 +27,20 @@
]
},
"payment": {
- "handlers": [{
- "id": "example_payment_provider",
- "name": "example.payment.provider",
- "version": "2026-01-11",
- "spec": "https://pay.provider.example/specs/handlers/payments",
- "config_schema": "https://pay.provider.example/specs/handlers/config.json",
- "instrument_schemas": [
- "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json"
- ],
- "config": {
- "business_id": "1234567890"
+ "handlers": [
+ {
+ "id": "example_payment_provider",
+ "name": "example.payment.provider",
+ "version": "2026-01-11",
+ "spec": "https://pay.provider.example/specs/handlers/payments",
+ "config_schema": "https://pay.provider.example/specs/handlers/config.json",
+ "instrument_schemas": [
+ "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json"
+ ],
+ "config": {
+ "business_id": "1234567890"
+ }
}
- }]
+ ]
}
-}
\ No newline at end of file
+}
diff --git a/a2a/business_agent/src/business_agent/main.py b/a2a/business_agent/src/business_agent/main.py
index d1c95b6..39d0f95 100644
--- a/a2a/business_agent/src/business_agent/main.py
+++ b/a2a/business_agent/src/business_agent/main.py
@@ -123,4 +123,4 @@ async def run(host, port):
if __name__ == "__main__":
- run()
+ run()
diff --git a/a2a/business_agent/src/business_agent/models/product_types.py b/a2a/business_agent/src/business_agent/models/product_types.py
index 98d67f3..a09f8e0 100644
--- a/a2a/business_agent/src/business_agent/models/product_types.py
+++ b/a2a/business_agent/src/business_agent/models/product_types.py
@@ -42,18 +42,14 @@ class ImageObject(ProductDiscoveryModel):
url: str
caption: str | None = None
- schema_type: Literal["ImageObject"] = Field(
- default="ImageObject", alias="@type"
- )
+ schema_type: Literal["ImageObject"] = Field(default="ImageObject", alias="@type")
class Organization(ProductDiscoveryModel):
"""Corresponds to schema.org/Organization."""
name: str
- schema_type: Literal["Organization"] = Field(
- default="Organization", alias="@type"
- )
+ schema_type: Literal["Organization"] = Field(default="Organization", alias="@type")
class Brand(ProductDiscoveryModel):
@@ -114,9 +110,9 @@ class BasePriceSpecification(ProductDiscoveryModel):
name: str | None = None
price: str
price_currency: str = Field(alias="priceCurrency")
- valid_for_member_tier: (
- MemberProgramTier | list[MemberProgramTier] | None
- ) = Field(default=None, alias="validForMemberTier")
+ valid_for_member_tier: MemberProgramTier | list[MemberProgramTier] | None = Field(
+ default=None, alias="validForMemberTier"
+ )
membership_points_earned: str | None = Field(
default=None, alias="membershipPointsEarned"
)
@@ -217,9 +213,7 @@ class DefinedRegion(ProductDiscoveryModel):
default="DefinedRegion", alias="@type"
)
address_country: str | None = Field(default=None, alias="addressCountry")
- address_region: list[str] | None = Field(
- default=None, alias="addressRegion"
- )
+ address_region: list[str] | None = Field(default=None, alias="addressRegion")
class ShippingQuantitativeValue(ProductDiscoveryModel):
@@ -239,9 +233,7 @@ class ShippingDeliveryTime(ProductDiscoveryModel):
schema_type: Literal["ShippingDeliveryTime"] = Field(
default="ShippingDeliveryTime", alias="@type"
)
- handling_time: ShippingQuantitativeValue | None = Field(
- alias="handlingTime"
- )
+ handling_time: ShippingQuantitativeValue | None = Field(alias="handlingTime")
transit_time: ShippingQuantitativeValue | None = Field(alias="transitTime")
@@ -294,12 +286,8 @@ class MerchantReturnPolicy(ProductDiscoveryModel):
alias="returnPolicyCategory"
)
- merchant_return_days: int | None = Field(
- default=None, alias="merchantReturnDays"
- )
- return_fees: ReturnFeesEnumeration | None = Field(
- default=None, alias="returnFees"
- )
+ merchant_return_days: int | None = Field(default=None, alias="merchantReturnDays")
+ return_fees: ReturnFeesEnumeration | None = Field(default=None, alias="returnFees")
return_method: ReturnMethodEnumeration | None = Field(alias="returnMethod")
return_shipping_fees_amount: MonetaryAmount | None = Field(
default=None, alias="returnShippingFeesAmount"
@@ -311,9 +299,7 @@ class Rating(ProductDiscoveryModel):
schema_type: Literal["Rating"] = Field(default="Rating", alias="@type")
rating_value: float = Field(alias="ratingValue")
- rating_explanation: str | None = Field(
- default=None, alias="ratingExplanation"
- )
+ rating_explanation: str | None = Field(default=None, alias="ratingExplanation")
class Certification(ProductDiscoveryModel):
@@ -340,13 +326,11 @@ class Offer(ProductDiscoveryModel):
price_specification: (
UnitPriceSpecification | list[UnitPriceSpecification] | None
) = Field(default=None, alias="priceSpecification")
- shipping_details: (
- OfferShippingDetails | list[OfferShippingDetails] | None
- ) = Field(default=None, alias="shippingDetails")
- availability: ItemAvailability | None = None
- item_condition: ItemCondition | None = Field(
- default=None, alias="itemCondition"
+ shipping_details: OfferShippingDetails | list[OfferShippingDetails] | None = Field(
+ default=None, alias="shippingDetails"
)
+ availability: ItemAvailability | None = None
+ item_condition: ItemCondition | None = Field(default=None, alias="itemCondition")
has_merchant_return_policy: MerchantReturnPolicy | None = Field(
default=None, alias="hasMerchantReturnPolicy"
)
@@ -356,9 +340,7 @@ class Offer(ProductDiscoveryModel):
class MediaObject(ProductDiscoveryModel):
"""Corresponds to schema.org/MediaObject."""
- schema_type: Literal["MediaObject"] = Field(
- default="MediaObject", alias="@type"
- )
+ schema_type: Literal["MediaObject"] = Field(default="MediaObject", alias="@type")
content_url: str | None = Field(default=None, alias="contentUrl")
@@ -397,9 +379,7 @@ class Product(ProductDiscoveryModel):
has_certification: Certification | list[Certification] | None = Field(
default=None, alias="hasCertification"
)
- subject_of: Model3D | list[Model3D] | None = Field(
- default=None, alias="subjectOf"
- )
+ subject_of: Model3D | list[Model3D] | None = Field(default=None, alias="subjectOf")
width: QuantitativeValue | None = None
height: QuantitativeValue | None = None
depth: QuantitativeValue | None = None
@@ -416,9 +396,7 @@ class ProductGroup(ProductDiscoveryModel):
product_group_id: str = Field(alias="productGroupID")
image: str | list[str] | list[ImageObject] | None = None
has_variant: list[Product] = Field(alias="hasVariant")
- schema_type: Literal["ProductGroup"] = Field(
- default="ProductGroup", alias="@type"
- )
+ schema_type: Literal["ProductGroup"] = Field(default="ProductGroup", alias="@type")
context: Literal["https://schema.org/"] = Field(
default="https://schema.org/", alias="@context"
)
diff --git a/a2a/business_agent/src/business_agent/store.py b/a2a/business_agent/src/business_agent/store.py
index 26fe800..962d3af 100644
--- a/a2a/business_agent/src/business_agent/store.py
+++ b/a2a/business_agent/src/business_agent/store.py
@@ -117,10 +117,7 @@ def search_products(self, query: str) -> ProductResults:
for product in all_products:
if product.product_id not in matching_products and (
keyword in product.name.lower()
- or (
- product.category
- and keyword in product.category.lower()
- )
+ or (product.category and keyword in product.category.lower())
):
matching_products[product.product_id] = product
@@ -253,9 +250,7 @@ def get_checkout(self, checkout_id: str) -> Checkout | None:
"""
return self._checkouts.get(checkout_id)
- def remove_from_checkout(
- self, checkout_id: str, product_id: str
- ) -> Checkout:
+ def remove_from_checkout(self, checkout_id: str, product_id: str) -> Checkout:
"""Remove a product from the checkout.
Args:
@@ -398,13 +393,9 @@ def _recalculate_checkout(self, checkout: Checkout) -> None:
totals.append(Total(type="tax", display_text="Tax", amount=tax))
final_total += shipping + tax
- totals.append(
- Total(type="total", display_text="Total", amount=final_total)
- )
+ totals.append(Total(type="total", display_text="Total", amount=final_total))
checkout.totals = totals
- checkout.continue_url = AnyUrl(
- f"https://example.com/checkout?id={checkout.id}"
- )
+ checkout.continue_url = AnyUrl(f"https://example.com/checkout?id={checkout.id}")
def add_delivery_address(
self, checkout_id: str, address: PostalAddress
@@ -426,9 +417,7 @@ def add_delivery_address(
if isinstance(checkout, FulfillmentCheckout):
dest_id = f"dest_{uuid4().hex[:8]}"
destination = FulfillmentDestinationResponse(
- root=ShippingDestinationResponse(
- id=dest_id, **address.model_dump()
- )
+ root=ShippingDestinationResponse(id=dest_id, **address.model_dump())
)
fulfillment_options = self._get_fulfillment_options()
@@ -481,10 +470,7 @@ def start_payment(self, checkout_id: str) -> Checkout | str:
if checkout.buyer is None:
messages.append("Provide a buyer email address")
- if (
- isinstance(checkout, FulfillmentCheckout)
- and checkout.fulfillment is None
- ):
+ if isinstance(checkout, FulfillmentCheckout) and checkout.fulfillment is None:
messages.append("Provide a fulfillment address")
if messages:
diff --git a/a2a/business_agent/src/business_agent/ucp_profile_resolver.py b/a2a/business_agent/src/business_agent/ucp_profile_resolver.py
index 3a84c05..0681a28 100644
--- a/a2a/business_agent/src/business_agent/ucp_profile_resolver.py
+++ b/a2a/business_agent/src/business_agent/ucp_profile_resolver.py
@@ -86,9 +86,7 @@ def resolve_profile(self, client_profile_url: str) -> dict:
merchant_version = self.merchant_profile.get("ucp").get("version")
client_version = datetime.strptime(client_version, "%Y-%m-%d").date()
- merchant_version = datetime.strptime(
- merchant_version, "%Y-%m-%d"
- ).date()
+ merchant_version = datetime.strptime(merchant_version, "%Y-%m-%d").date()
if client_version > merchant_version:
raise ServerError(
diff --git a/a2a/chat-client/App.tsx b/a2a/chat-client/App.tsx
index 0886e2c..c497344 100644
--- a/a2a/chat-client/App.tsx
+++ b/a2a/chat-client/App.tsx
@@ -13,18 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {useEffect, useRef, useState} from 'react';
-import ChatInput from './components/ChatInput';
-import ChatMessageComponent from './components/ChatMessage';
-import Header from './components/Header';
-import {appConfig} from './config';
-import {CredentialProviderProxy} from './mocks/credentialProviderProxy';
-
-import {type ChatMessage, type PaymentInstrument, type Product, Sender, type Checkout, type PaymentHandler} from './types';
+import { useEffect, useRef, useState } from "react";
+import ChatInput from "./components/ChatInput";
+import ChatMessageComponent from "./components/ChatMessage";
+import Header from "./components/Header";
+import { appConfig } from "./config";
+import { CredentialProviderProxy } from "./mocks/credentialProviderProxy";
+
+import {
+ type ChatMessage,
+ type PaymentInstrument,
+ type Product,
+ Sender,
+ type Checkout,
+ type PaymentHandler,
+} from "./types";
type RequestPart =
- | {type: 'text'; text: string}
- | {type: 'data'; data: Record};
+ | { type: "text"; text: string }
+ | { type: "data"; data: Record };
function createChatMessage(
sender: Sender,
@@ -42,7 +49,7 @@ function createChatMessage(
const initialMessage: ChatMessage = createChatMessage(
Sender.MODEL,
appConfig.defaultMessage,
- {id: 'initial'},
+ { id: "initial" },
);
/**
@@ -50,7 +57,9 @@ const initialMessage: ChatMessage = createChatMessage(
* Only for demo purposes, not intended for production use.
*/
function App() {
- const [user_email, _setUserEmail] = useState('foo@example.com');
+ const [user_email, _setUserEmail] = useState(
+ "foo@example.com",
+ );
const [messages, setMessages] = useState([initialMessage]);
const [isLoading, setIsLoading] = useState(false);
const [contextId, setContextId] = useState(null);
@@ -69,15 +78,15 @@ function App() {
const handleAddToCheckout = (productToAdd: Product) => {
const actionPayload = JSON.stringify({
- action: 'add_to_checkout',
+ action: "add_to_checkout",
product_id: productToAdd.productID,
quantity: 1,
});
- handleSendMessage(actionPayload, {isUserAction: true});
+ handleSendMessage(actionPayload, { isUserAction: true });
};
const handleStartPayment = () => {
- const actionPayload = JSON.stringify({action: 'start_payment'});
+ const actionPayload = JSON.stringify({ action: "start_payment" });
handleSendMessage(actionPayload, {
isUserAction: true,
});
@@ -95,7 +104,7 @@ function App() {
//find the handler with id "example_payment_provider"
const handler = checkout.payment.handlers.find(
- (handler: PaymentHandler) => handler.id === 'example_payment_provider',
+ (handler: PaymentHandler) => handler.id === "example_payment_provider",
);
if (!handler) {
const errorMessage = createChatMessage(
@@ -114,12 +123,12 @@ function App() {
);
const paymentMethods = paymentResponse.payment_method_aliases;
- const paymentSelectorMessage = createChatMessage(Sender.MODEL, '', {
+ const paymentSelectorMessage = createChatMessage(Sender.MODEL, "", {
paymentMethods,
});
setMessages((prev) => [...prev, paymentSelectorMessage]);
} catch (error) {
- console.error('Failed to resolve mandate:', error);
+ console.error("Failed to resolve mandate:", error);
const errorMessage = createChatMessage(
Sender.MODEL,
"Sorry, I couldn't retrieve payment methods.",
@@ -136,13 +145,13 @@ function App() {
const userActionMessage = createChatMessage(
Sender.USER,
`User selected payment method: ${selectedMethod}`,
- {isUserAction: true},
+ { isUserAction: true },
);
setMessages((prev) => [...prev, userActionMessage]);
try {
if (!user_email) {
- throw new Error('User email is not set.');
+ throw new Error("User email is not set.");
}
const paymentInstrument =
@@ -152,15 +161,15 @@ function App() {
);
if (!paymentInstrument || !paymentInstrument.credential) {
- throw new Error('Failed to retrieve payment credential');
+ throw new Error("Failed to retrieve payment credential");
}
- const paymentInstrumentMessage = createChatMessage(Sender.MODEL, '', {
+ const paymentInstrumentMessage = createChatMessage(Sender.MODEL, "", {
paymentInstrument,
});
setMessages((prev) => [...prev, paymentInstrumentMessage]);
} catch (error) {
- console.error('Failed to process payment mandate:', error);
+ console.error("Failed to process payment mandate:", error);
const errorMessage = createChatMessage(
Sender.MODEL,
"Sorry, I couldn't process the payment. Please try again.",
@@ -174,7 +183,7 @@ function App() {
const userActionMessage = createChatMessage(
Sender.USER,
`User confirmed payment.`,
- {isUserAction: true},
+ { isUserAction: true },
);
// Let handleSendMessage manage the loading indicator
setMessages((prev) => [
@@ -184,12 +193,12 @@ function App() {
try {
const parts: RequestPart[] = [
- {type: 'data', data: {'action': 'complete_checkout'}},
+ { type: "data", data: { action: "complete_checkout" } },
{
- type: 'data',
+ type: "data",
data: {
- 'a2a.ucp.checkout.payment_data': paymentInstrument,
- 'a2a.ucp.checkout.risk_signals': {'data': 'some risk data'},
+ "a2a.ucp.checkout.payment_data": paymentInstrument,
+ "a2a.ucp.checkout.risk_signals": { data: "some risk data" },
},
},
];
@@ -198,10 +207,10 @@ function App() {
isUserAction: true,
});
} catch (error) {
- console.error('Error confirming payment:', error);
+ console.error("Error confirming payment:", error);
const errorMessage = createChatMessage(
Sender.MODEL,
- 'Sorry, there was an issue confirming your payment.',
+ "Sorry, there was an issue confirming your payment.",
);
// If handleSendMessage wasn't called, we might need to manually update state
// In this case, we remove the loading indicator that handleSendMessage would have added
@@ -212,17 +221,17 @@ function App() {
const handleSendMessage = async (
messageContent: string | RequestPart[],
- options?: {isUserAction?: boolean; headers?: Record},
+ options?: { isUserAction?: boolean; headers?: Record },
) => {
if (isLoading) return;
const userMessage = createChatMessage(
Sender.USER,
options?.isUserAction
- ? ''
- : typeof messageContent === 'string'
+ ? ""
+ : typeof messageContent === "string"
? messageContent
- : 'Sent complex data',
+ : "Sent complex data",
);
if (userMessage.text) {
// Only add if there's text
@@ -230,14 +239,14 @@ function App() {
}
setMessages((prev) => [
...prev,
- createChatMessage(Sender.MODEL, '', {isLoading: true}),
+ createChatMessage(Sender.MODEL, "", { isLoading: true }),
]);
setIsLoading(true);
try {
const requestParts =
- typeof messageContent === 'string'
- ? [{type: 'text', text: messageContent}]
+ typeof messageContent === "string"
+ ? [{ type: "text", text: messageContent }]
: messageContent;
const requestParams: {
@@ -254,10 +263,10 @@ function App() {
};
} = {
message: {
- role: 'user',
+ role: "user",
parts: requestParts,
messageId: crypto.randomUUID(),
- kind: 'message',
+ kind: "message",
},
configuration: {
historyLength: 0,
@@ -272,20 +281,20 @@ function App() {
}
const defaultHeaders = {
- 'Content-Type': 'application/json',
- 'X-A2A-Extensions':
- 'https://ucp.dev/specification/reference?v=2026-01-11',
- 'UCP-Agent':
+ "Content-Type": "application/json",
+ "X-A2A-Extensions":
+ "https://ucp.dev/specification/reference?v=2026-01-11",
+ "UCP-Agent":
'profile="http://localhost:3000/profile/agent_profile.json"',
};
- const response = await fetch('/api', {
- method: 'POST',
- headers: {...defaultHeaders, ...options?.headers},
+ const response = await fetch("/api", {
+ method: "POST",
+ headers: { ...defaultHeaders, ...options?.headers },
body: JSON.stringify({
- jsonrpc: '2.0',
+ jsonrpc: "2.0",
id: crypto.randomUUID(),
- method: 'message/send',
+ method: "message/send",
params: requestParams,
}),
});
@@ -303,7 +312,7 @@ function App() {
//if there is a task and it's in one of the active states
if (
data.result?.id &&
- data.result?.status?.state in ['working', 'submitted', 'input-required']
+ data.result?.status?.state in ["working", "submitted", "input-required"]
) {
setTaskId(data.result.id);
} else {
@@ -311,7 +320,7 @@ function App() {
setTaskId(undefined);
}
- const combinedBotMessage = createChatMessage(Sender.MODEL, '');
+ const combinedBotMessage = createChatMessage(Sender.MODEL, "");
const responseParts =
data.result?.parts || data.result?.status?.message?.parts || [];
@@ -320,17 +329,17 @@ function App() {
if (part.text) {
// Simple text
combinedBotMessage.text +=
- (combinedBotMessage.text ? '\n' : '') + part.text;
- } else if (part.data?.['a2a.product_results']) {
+ (combinedBotMessage.text ? "\n" : "") + part.text;
+ } else if (part.data?.["a2a.product_results"]) {
// Product results
combinedBotMessage.text +=
- (combinedBotMessage.text ? '\n' : '') +
- (part.data['a2a.product_results'].content || '');
+ (combinedBotMessage.text ? "\n" : "") +
+ (part.data["a2a.product_results"].content || "");
combinedBotMessage.products =
- part.data['a2a.product_results'].results;
- } else if (part.data?.['a2a.ucp.checkout']) {
+ part.data["a2a.product_results"].results;
+ } else if (part.data?.["a2a.ucp.checkout"]) {
// Checkout
- combinedBotMessage.checkout = part.data['a2a.ucp.checkout'];
+ combinedBotMessage.checkout = part.data["a2a.ucp.checkout"];
}
}
@@ -354,10 +363,10 @@ function App() {
]);
}
} catch (error) {
- console.error('Error sending message:', error);
+ console.error("Error sending message:", error);
const errorMessage = createChatMessage(
Sender.MODEL,
- 'Sorry, something went wrong. Please try again.',
+ "Sorry, something went wrong. Please try again.",
);
// Replace the placeholder with the error message
setMessages((prev) => [...prev.slice(0, -1), errorMessage]);
@@ -373,25 +382,27 @@ function App() {
+ className="flex-grow overflow-y-auto p-4 md:p-6 space-y-2"
+ >
{messages.map((msg, index) => (
+ isLastCheckout={index === lastCheckoutIndex}
+ >
))}
diff --git a/a2a/chat-client/README.md b/a2a/chat-client/README.md
index a11c94f..af21d9a 100644
--- a/a2a/chat-client/README.md
+++ b/a2a/chat-client/README.md
@@ -16,7 +16,7 @@
## Run Locally
-**Prerequisites:** Node.js
+**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
diff --git a/a2a/chat-client/components/BotLogo.tsx b/a2a/chat-client/components/BotLogo.tsx
index 3f018e0..4d3638c 100644
--- a/a2a/chat-client/components/BotLogo.tsx
+++ b/a2a/chat-client/components/BotLogo.tsx
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type React from 'react';
+import type React from "react";
const BotLogo = (props: React.SVGProps) => (
) => (
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
- {...props}>
+ {...props}
+ >
void;
@@ -30,20 +30,21 @@ function SendIcon(props: React.SVGProps) {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
- className="w-6 h-6">
+ className="w-6 h-6"
+ >
);
}
-function ChatInput({onSendMessage, isLoading}: ChatInputProps) {
- const [inputValue, setInputValue] = useState('');
+function ChatInput({ onSendMessage, isLoading }: ChatInputProps) {
+ const [inputValue, setInputValue] = useState("");
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (inputValue.trim() && !isLoading) {
onSendMessage(inputValue.trim());
- setInputValue('');
+ setInputValue("");
}
};
@@ -51,7 +52,8 @@ function ChatInput({onSendMessage, isLoading}: ChatInputProps) {
diff --git a/a2a/chat-client/components/ChatMessage.tsx b/a2a/chat-client/components/ChatMessage.tsx
index 8fdec6f..7a8500e 100644
--- a/a2a/chat-client/components/ChatMessage.tsx
+++ b/a2a/chat-client/components/ChatMessage.tsx
@@ -13,19 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {appConfig} from '@/config';
+import { appConfig } from "@/config";
import {
type ChatMessage,
type Checkout,
type PaymentInstrument,
type Product,
Sender,
-} from '../types';
-import CheckoutComponent from './Checkout';
-import PaymentConfirmationComponent from './PaymentConfirmation';
-import PaymentMethodSelector from './PaymentMethodSelector';
-import ProductCard from './ProductCard';
-import UserLogo from './UserLogo';
+} from "../types";
+import CheckoutComponent from "./Checkout";
+import PaymentConfirmationComponent from "./PaymentConfirmation";
+import PaymentMethodSelector from "./PaymentMethodSelector";
+import ProductCard from "./ProductCard";
+import UserLogo from "./UserLogo";
interface ChatMessageProps {
message: ChatMessage;
diff --git a/a2a/chat-client/components/Checkout.tsx b/a2a/chat-client/components/Checkout.tsx
index 607c2ba..d9e760e 100644
--- a/a2a/chat-client/components/Checkout.tsx
+++ b/a2a/chat-client/components/Checkout.tsx
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type React from 'react';
-import {useState} from 'react';
+import type React from "react";
+import { useState } from "react";
-import type {Checkout, CheckoutItem} from '../types';
+import type { Checkout, CheckoutItem } from "../types";
interface CheckoutProps {
checkout: Checkout;
@@ -35,7 +35,7 @@ const CheckoutComponent: React.FC
= ({
: checkout.line_items.slice(0, 5);
const formatCurrency = (amount: number, currency: string) => {
- const currencySymbol = currency === 'EUR' ? '€' : '$';
+ const currencySymbol = currency === "EUR" ? "€" : "$";
return `${currencySymbol}${(amount / 100).toFixed(2)}`;
};
@@ -44,10 +44,10 @@ const CheckoutComponent: React.FC = ({
};
const getItemTotal = (lineItem: CheckoutItem) => {
- return lineItem.totals.find((t) => t.type === 'total');
+ return lineItem.totals.find((t) => t.type === "total");
};
- const grandTotal = getTotal('total');
+ const grandTotal = getTotal("total");
return (
@@ -61,16 +61,17 @@ const CheckoutComponent: React.FC
= ({
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
- strokeWidth={2}>
+ strokeWidth={2}
+ >
- {checkout.status === 'completed'
- ? 'Order Confirmed'
- : 'Checkout Summary'}
+ {checkout.status === "completed"
+ ? "Order Confirmed"
+ : "Checkout Summary"}
{checkout.order?.id && (
@@ -105,20 +106,22 @@ const CheckoutComponent: React.FC = ({
setIsExpanded(!isExpanded)}
- className="text-sm text-blue-600 hover:underline w-full text-center">
+ className="text-sm text-blue-600 hover:underline w-full text-center"
+ >
{isExpanded
- ? 'Show less'
+ ? "Show less"
: `Show ${checkout.line_items.length - 5} more items`}
)}
{checkout.totals
- .filter((t) => t.type !== 'total' && t.amount > 0)
+ .filter((t) => t.type !== "total" && t.amount > 0)
.map((total) => (
+ className="flex justify-between items-center"
+ >
{total.display_text}
{formatCurrency(total.amount, checkout.currency)}
@@ -139,14 +142,15 @@ const CheckoutComponent: React.FC = ({
Checkout ID: {checkout.id}
- {checkout.status !== 'completed' && (
+ {checkout.status !== "completed" && (
{checkout.continue_url && (
+ className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded text-sm"
+ >
Go to Checkout
)}
@@ -154,7 +158,8 @@ const CheckoutComponent: React.FC
= ({
+ className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-sm"
+ >
Start Payment
)}
@@ -162,7 +167,8 @@ const CheckoutComponent: React.FC = ({
onCompletePayment?.(checkout)}
- className="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded text-sm">
+ className="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded text-sm"
+ >
Complete Payment
)}
@@ -171,7 +177,8 @@ const CheckoutComponent: React.FC = ({
{checkout.order?.permalink_url && (
+ className="block mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-center"
+ >
View Order
)}
diff --git a/a2a/chat-client/components/Header.tsx b/a2a/chat-client/components/Header.tsx
index 8d5c8a9..820b864 100644
--- a/a2a/chat-client/components/Header.tsx
+++ b/a2a/chat-client/components/Header.tsx
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {appConfig} from '@/config';
+import { appConfig } from "@/config";
function Header() {
return (
diff --git a/a2a/chat-client/components/PaymentConfirmation.tsx b/a2a/chat-client/components/PaymentConfirmation.tsx
index 592c6a9..6716dc1 100644
--- a/a2a/chat-client/components/PaymentConfirmation.tsx
+++ b/a2a/chat-client/components/PaymentConfirmation.tsx
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type React from 'react';
-import {useState} from 'react';
-import type {PaymentInstrument} from '../types';
+import type React from "react";
+import { useState } from "react";
+import type { PaymentInstrument } from "../types";
interface PaymentConfirmationProps {
paymentInstrument: PaymentInstrument;
@@ -42,7 +42,7 @@ const PaymentConfirmationComponent: React.FC = ({
Selected Payment Method
- {paymentInstrument.brand.toUpperCase()} ending in{' '}
+ {paymentInstrument.brand.toUpperCase()} ending in{" "}
{paymentInstrument.last_digits}
@@ -53,7 +53,8 @@ const PaymentConfirmationComponent: React.FC = ({
type="button"
onClick={handleConfirmClick}
disabled={isConfirming}
- className="flex justify-center items-center w-full text-center bg-green-500 text-white py-2 rounded-md hover:bg-green-600 transition-colors disabled:bg-green-400 disabled:cursor-wait">
+ className="flex justify-center items-center w-full text-center bg-green-500 text-white py-2 rounded-md hover:bg-green-600 transition-colors disabled:bg-green-400 disabled:cursor-wait"
+ >
{isConfirming ? (
<>
= ({
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
- viewBox="0 0 24 24">
+ viewBox="0 0 24 24"
+ >
+ strokeWidth="4"
+ >
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ >
Processing...
>
) : (
- 'Confirm Purchase'
+ "Confirm Purchase"
)}
diff --git a/a2a/chat-client/components/PaymentMethodSelector.tsx b/a2a/chat-client/components/PaymentMethodSelector.tsx
index 4b2fb56..42b45e0 100644
--- a/a2a/chat-client/components/PaymentMethodSelector.tsx
+++ b/a2a/chat-client/components/PaymentMethodSelector.tsx
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type React from 'react';
-import {useState} from 'react';
-import type {PaymentMethod} from '../types';
+import type React from "react";
+import { useState } from "react";
+import type { PaymentMethod } from "../types";
interface PaymentMethodSelectorProps {
paymentMethods: PaymentMethod[];
@@ -43,7 +43,8 @@ const PaymentMethodSelector: React.FC = ({
{paymentMethods.map((method) => (
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 cursor-pointer"
+ >
= ({
type="button"
onClick={handleContinue}
disabled={!selectedMethod}
- className="block w-full text-center bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed">
+ className="block w-full text-center bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
+ >
Continue
diff --git a/a2a/chat-client/components/ProductCard.tsx b/a2a/chat-client/components/ProductCard.tsx
index ae8c23f..ffd12e0 100644
--- a/a2a/chat-client/components/ProductCard.tsx
+++ b/a2a/chat-client/components/ProductCard.tsx
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type React from 'react';
-import type {Product} from '../types';
+import type React from "react";
+import type { Product } from "../types";
interface ProductCardProps {
product: Product;
onAddToCart: (product: Product) => void;
}
-const ProductCard: React.FC
= ({product, onAddToCart}) => {
- const isAvailable = product.offers.availability.includes('InStock');
+const ProductCard: React.FC = ({ product, onAddToCart }) => {
+ const isAvailable = product.offers.availability.includes("InStock");
const handleAddToCartClick = () => onAddToCart?.(product);
return (
@@ -35,25 +35,28 @@ const ProductCard: React.FC = ({product, onAddToCart}) => {
+ title={product.name}
+ >
{product.name}
{product.brand.name}
- {product.offers.priceCurrency === 'EUR' ? '€' : '$'}
+ {product.offers.priceCurrency === "EUR" ? "€" : "$"}
{product.offers.price}
- {isAvailable ? 'In Stock' : 'Out of Stock'}
+ className={`px-2 py-1 text-xs font-semibold rounded-full ${isAvailable ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}`}
+ >
+ {isAvailable ? "In Stock" : "Out of Stock"}
+ className="block w-full text-center bg-blue-500 text-white py-2 rounded-md mt-4 hover:bg-blue-600 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
+ >
Add to Checkout
diff --git a/a2a/chat-client/components/UserLogo.tsx b/a2a/chat-client/components/UserLogo.tsx
index eb5a4a5..4812c82 100644
--- a/a2a/chat-client/components/UserLogo.tsx
+++ b/a2a/chat-client/components/UserLogo.tsx
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import type React from 'react';
+import type React from "react";
const UserLogo = (props: React.SVGProps) => (
) => (
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
- {...props}>
+ {...props}
+ >
-
+
Agent
-
-
-
+
+
+
-
-
+
+