diff --git a/a2a/DEVELOPER_GUIDE.md b/a2a/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..c46b7f0 --- /dev/null +++ b/a2a/DEVELOPER_GUIDE.md @@ -0,0 +1,82 @@ +# Cymbal Retail Agent - Developer Guide + +## TL;DR + +- **ADK Agent** with 8 shopping tools (search, checkout, payment) using Gemini 3.0 Flash +- **UCP Integration** for standardized commerce data types and capability negotiation +- **A2A Protocol** for agent discovery and JSON-RPC messaging + +## About This Guide + +This guide is for developers who want to understand how the sample works internally. + +**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 +- How to extend and customize the sample + +## Reading Roadmap + +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 | + +## Architecture + +
+ Cymbal Retail Agent Architecture +

System architecture — Chat Client (React + A2A Client) communicates via JSON-RPC to the Cymbal Retail Agent (A2A Server → ADKAgentExecutor → ADK Agent → RetailStore). Discovery endpoints expose agent capabilities and UCP profile.

+
+ +## Quick Reference + +### 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 | + +### Endpoints + +| 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 | +|-----|---------| +| `user:checkout_id` | Current checkout session | +| `__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 | diff --git a/a2a/README.md b/a2a/README.md index cf95542..7e22ad1 100644 --- a/a2a/README.md +++ b/a2a/README.md @@ -39,7 +39,7 @@ The sample uses **[Google ADK](https://google.github.io/adk-docs/)** (Agent Deve Complete Shopping Flow
Product search → Add items to Checkout → Payment → Order confirmation

-UCP A2A Demo +UCP A2A Demo

▶️ Watch the full demo video

@@ -50,7 +50,7 @@ The sample uses **[Google ADK](https://google.github.io/adk-docs/)** (Agent Deve System Architecture
How Client, A2A Protocol, Cymbal Retail Agent, and Store interact

-Architecture Diagram +Architecture Diagram

**Key points:** @@ -103,13 +103,13 @@ This starts the Cymbal Retail Agent on port 10999. You can verify by accessing: Agent Card
/.well-known/agent-card.json

-Agent Card +Agent Card
Declares UCP extension with capabilities UCP Profile
/.well-known/ucp

-UCP Profile +UCP Profile
Defines supported capabilities & payment handlers @@ -139,7 +139,7 @@ The Chat Client UCP Profile can be found at http://localhost:3000/profile/agent-
-Demo +Demo
▶️ Watch full video
@@ -158,6 +158,19 @@ The Chat Client UCP Profile can be found at http://localhost:3000/profile/agent-
+> **Note**: This sample is for demonstration purposes only. See [Production Notes](docs/08-production-notes.md) for security considerations and deployment requirements. + +## Next Steps + +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 | + ## 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. @@ -177,40 +190,6 @@ This sample uses the following UCP capabilities: | **[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 | -## Components - -### Cymbal Retail Agent (`business_agent/`) - -AI shopping assistant built with Google ADK, exposed via A2A interface with UCP extension. - -| File | Purpose | -|------|---------| -| `agent.py` | ADK Agent with Gemini 3.0 Flash + 8 shopping tools | -| `agent_executor.py` | Bridges ADK ↔ A2A protocol | -| `store.py` | Mock RetailStore (products, checkouts, orders) | -| `data/ucp.json` | UCP Profile served at `/.well-known/ucp` | - -### Chat Client (`chat-client/`) - -React UI (TypeScript, Vite, Tailwind) that communicates via A2A and renders UCP data types. - -| File | Purpose | -|------|---------| -| `App.tsx` | A2A messaging + state management | -| `components/` | ProductCard, Checkout, PaymentMethodSelector | -| `profile/agent_profile.json` | Client's UCP capabilities | - -## Mock Store - -The Cymbal Retail Agent uses an in-memory `RetailStore` to simulate a real backend: - -- **Products** - Loaded from `data/products.json` (cookies, chips, fruits, etc.) -- **Checkouts** - Session-based checkout management with line items -- **Orders** - Created when checkout completes successfully -- **Payments** - Mock processor simulates payment flow - -Prices are in USD, tax is calculated at 10%, and shipping costs vary by method. - ## Related Resources - [UCP Specification](https://ucp.dev/specification/overview/) diff --git a/a2a/SKILLS.md b/a2a/SKILLS.md new file mode 100644 index 0000000..04eba4e --- /dev/null +++ b/a2a/SKILLS.md @@ -0,0 +1,258 @@ +--- +name: cymbal-retail-agent +description: AI-powered shopping agent built with Google ADK, demonstrating UCP commerce integration via A2A protocol. Use this context when working on the Cymbal Retail Agent codebase. +triggers: + - working on a2a sample + - modifying agent tools + - updating checkout flow + - adding UCP capabilities + - debugging A2A communication +globs: + - "a2a/**/*" + - "business_agent/**/*" + - "chat-client/**/*" +--- + +# Cymbal Retail Agent - AI Assistant Context + +> **Purpose**: This file provides context for AI coding assistants (Claude Code, Gemini CLI, Cursor, Codex, etc.) to understand and extend the Cymbal Retail Agent codebase. + +AI-powered shopping agent built with Google ADK, demonstrating UCP commerce integration via A2A protocol. + +## 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 | + +## Directory Structure + +``` +a2a/ +├── business_agent/src/business_agent/ +│ ├── agent.py # ADK agent with 8 shopping tools +│ ├── agent_executor.py # A2A ↔ ADK bridge +│ ├── store.py # Mock retail store (replace for production) +│ ├── main.py # Uvicorn server entry point +│ ├── ucp_profile_resolver.py # UCP capability negotiation +│ ├── payment_processor.py # Mock payment processing +│ ├── constants.py # State keys and extension URLs +│ ├── helpers/type_generator.py # Dynamic Pydantic checkout types +│ ├── a2a_extensions/ # A2A extension implementations +│ └── data/ # JSON configs and product images +├── chat-client/ +│ ├── App.tsx # React main component, A2A messaging +│ ├── components/ # ProductCard, Checkout, PaymentMethodSelector +│ ├── types.ts # TypeScript interfaces +│ └── profile/agent_profile.json # Client UCP capabilities +├── docs/ # Detailed documentation (see below) +├── DEVELOPER_GUIDE.md # Developer overview and reading roadmap +├── README.md # Quick start and demo +└── SKILLS.md # This file (AI assistant context) +``` + +## 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) | +| **Capability** | Feature set the agent supports (e.g., `dev.ucp.shopping.checkout`) | + +## State Keys (constants.py) + +```python +# Session state keys - naming conventions: +# - user: User-scoped data (persists across turns) +# - __xxx__ System/internal data (managed by framework) +# - temp: Temporary data (cleared after use) + +ADK_USER_CHECKOUT_ID = "user:checkout_id" # Current checkout session ID +ADK_PAYMENT_STATE = "__payment_data__" # PaymentInstrument from client +ADK_UCP_METADATA_STATE = "__ucp_metadata__" # Negotiated UCP capabilities +ADK_EXTENSIONS_STATE_KEY = "__session_extensions__" # Active A2A extensions +ADK_LATEST_TOOL_RESULT = "temp:LATEST_TOOL_RESULT" # Last tool result for output + +# Response data keys (used in tool returns) +UCP_CHECKOUT_KEY = "a2a.ucp.checkout" # Checkout data in response +UCP_PAYMENT_DATA_KEY = "a2a.ucp.checkout.payment_data" +UCP_RISK_SIGNALS_KEY = "a2a.ucp.checkout.risk_signals" + +# Extension constants +A2A_UCP_EXTENSION_URL = "https://ucp.dev/specification/reference?v=2026-01-11" +UCP_AGENT_HEADER = "UCP-Agent" # HTTP header for client profile +``` + +## 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 | + +## Checkout State Machine + +``` +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 | + +## UCP Capabilities + +``` +dev.ucp.shopping.checkout # Base checkout capability +dev.ucp.shopping.fulfillment # Shipping (extends checkout) +dev.ucp.shopping.discount # Promotional codes (extends checkout) +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: + """Tool docstring (visible to LLM for reasoning).""" + # 1. Access state + checkout_id = tool_context.state.get(ADK_USER_CHECKOUT_ID) + metadata = tool_context.state.get(ADK_UCP_METADATA_STATE) + + # 2. Validate + if not metadata: + return {"message": "Missing UCP metadata", "status": "error"} + + # 3. Business logic + result = store.some_method(...) + + # 4. Update state if needed + tool_context.state[ADK_USER_CHECKOUT_ID] = result.id + + # 5. Return UCP-formatted response + return {UCP_CHECKOUT_KEY: result.model_dump(mode="json")} + +# Add to root_agent tools list +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"} +} +``` + +### 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 +- `place_order()` - Creates OrderConfirmation + +## 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` | + +## Commands + +```bash +# Start backend +cd a2a/business_agent && uv sync && uv run business_agent + +# Start frontend +cd a2a/chat-client && npm install && npm run dev + +# Verify endpoints +curl http://localhost:10999/.well-known/agent-card.json +curl http://localhost:10999/.well-known/ucp +``` + +## Response Format Pattern + +```python +# For UCP data (checkout, products) +return { + UCP_CHECKOUT_KEY: checkout.model_dump(mode="json"), + "status": "success", +} + +# For errors +return {"message": "Error description", "status": "error"} +``` + +## Production Considerations + +> **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 | + +## 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 | + +## External Resources + +| 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 | + +## 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/assets/agent_card.png b/a2a/assets/agent_card.png deleted file mode 100644 index c25bf1c..0000000 Binary files a/a2a/assets/agent_card.png and /dev/null differ diff --git a/a2a/assets/agent_card.webp b/a2a/assets/agent_card.webp new file mode 100644 index 0000000..07be348 Binary files /dev/null and b/a2a/assets/agent_card.webp differ diff --git a/a2a/assets/architecture_diagram.jpeg b/a2a/assets/architecture_diagram.jpeg deleted file mode 100644 index e42a0a9..0000000 Binary files a/a2a/assets/architecture_diagram.jpeg and /dev/null differ diff --git a/a2a/assets/architecture_diagram.webp b/a2a/assets/architecture_diagram.webp new file mode 100644 index 0000000..baa6997 Binary files /dev/null and b/a2a/assets/architecture_diagram.webp differ diff --git a/a2a/assets/diagrams/00_01_architecture_overview.webp b/a2a/assets/diagrams/00_01_architecture_overview.webp new file mode 100644 index 0000000..b068d8a Binary files /dev/null and b/a2a/assets/diagrams/00_01_architecture_overview.webp differ diff --git a/a2a/assets/diagrams/00_01_checkout_states.webp b/a2a/assets/diagrams/00_01_checkout_states.webp new file mode 100644 index 0000000..545bc60 Binary files /dev/null and b/a2a/assets/diagrams/00_01_checkout_states.webp differ diff --git a/a2a/assets/diagrams/00_02_architecture_layers.webp b/a2a/assets/diagrams/00_02_architecture_layers.webp new file mode 100644 index 0000000..90219cd Binary files /dev/null and b/a2a/assets/diagrams/00_02_architecture_layers.webp differ diff --git a/a2a/assets/diagrams/01_01_system_overview.webp b/a2a/assets/diagrams/01_01_system_overview.webp new file mode 100644 index 0000000..27cb161 Binary files /dev/null and b/a2a/assets/diagrams/01_01_system_overview.webp differ diff --git a/a2a/assets/diagrams/01_02_request_flow.webp b/a2a/assets/diagrams/01_02_request_flow.webp new file mode 100644 index 0000000..d69faea Binary files /dev/null and b/a2a/assets/diagrams/01_02_request_flow.webp differ diff --git a/a2a/assets/diagrams/01_03_mock_store_structure.webp b/a2a/assets/diagrams/01_03_mock_store_structure.webp new file mode 100644 index 0000000..a6b62eb Binary files /dev/null and b/a2a/assets/diagrams/01_03_mock_store_structure.webp differ diff --git a/a2a/assets/diagrams/02_01_tool_execution_flow.webp b/a2a/assets/diagrams/02_01_tool_execution_flow.webp new file mode 100644 index 0000000..a4d7513 Binary files /dev/null and b/a2a/assets/diagrams/02_01_tool_execution_flow.webp differ diff --git a/a2a/assets/diagrams/02_02_multi_tool_conversation.webp b/a2a/assets/diagrams/02_02_multi_tool_conversation.webp new file mode 100644 index 0000000..b148cf2 Binary files /dev/null and b/a2a/assets/diagrams/02_02_multi_tool_conversation.webp differ diff --git a/a2a/assets/diagrams/03_01_capability_negotiation.webp b/a2a/assets/diagrams/03_01_capability_negotiation.webp new file mode 100644 index 0000000..3abd70d Binary files /dev/null and b/a2a/assets/diagrams/03_01_capability_negotiation.webp differ diff --git a/a2a/assets/diagrams/03_02_type_hierarchy.webp b/a2a/assets/diagrams/03_02_type_hierarchy.webp new file mode 100644 index 0000000..86f9e09 Binary files /dev/null and b/a2a/assets/diagrams/03_02_type_hierarchy.webp differ diff --git a/a2a/assets/diagrams/04_01_checkout_state_machine.webp b/a2a/assets/diagrams/04_01_checkout_state_machine.webp new file mode 100644 index 0000000..3a27fc3 Binary files /dev/null and b/a2a/assets/diagrams/04_01_checkout_state_machine.webp differ diff --git a/a2a/assets/diagrams/04_02_payment_flow.webp b/a2a/assets/diagrams/04_02_payment_flow.webp new file mode 100644 index 0000000..b9cc7a3 Binary files /dev/null and b/a2a/assets/diagrams/04_02_payment_flow.webp differ diff --git a/a2a/assets/diagrams/05_01_request_response_cycle.webp b/a2a/assets/diagrams/05_01_request_response_cycle.webp new file mode 100644 index 0000000..82a5162 Binary files /dev/null and b/a2a/assets/diagrams/05_01_request_response_cycle.webp differ diff --git a/a2a/assets/diagrams/05_02_component_hierarchy.webp b/a2a/assets/diagrams/05_02_component_hierarchy.webp new file mode 100644 index 0000000..19ffa82 Binary files /dev/null and b/a2a/assets/diagrams/05_02_component_hierarchy.webp differ diff --git a/a2a/assets/diagrams/06_01_extension_decision_tree.webp b/a2a/assets/diagrams/06_01_extension_decision_tree.webp new file mode 100644 index 0000000..c7380a9 Binary files /dev/null and b/a2a/assets/diagrams/06_01_extension_decision_tree.webp differ diff --git a/a2a/assets/diagrams/06_02_tool_architecture.webp b/a2a/assets/diagrams/06_02_tool_architecture.webp new file mode 100644 index 0000000..d1dd8c6 Binary files /dev/null and b/a2a/assets/diagrams/06_02_tool_architecture.webp differ diff --git a/a2a/assets/diagrams/06_03_capability_hierarchy.webp b/a2a/assets/diagrams/06_03_capability_hierarchy.webp new file mode 100644 index 0000000..3cb2978 Binary files /dev/null and b/a2a/assets/diagrams/06_03_capability_hierarchy.webp differ diff --git a/a2a/assets/diagrams/06_04_custom_payment_flow.webp b/a2a/assets/diagrams/06_04_custom_payment_flow.webp new file mode 100644 index 0000000..ceef7e3 Binary files /dev/null and b/a2a/assets/diagrams/06_04_custom_payment_flow.webp differ diff --git a/a2a/assets/diagrams/07_01_setup_flow.webp b/a2a/assets/diagrams/07_01_setup_flow.webp new file mode 100644 index 0000000..923b5c5 Binary files /dev/null and b/a2a/assets/diagrams/07_01_setup_flow.webp differ diff --git a/a2a/assets/diagrams/07_02_debug_strategy.webp b/a2a/assets/diagrams/07_02_debug_strategy.webp new file mode 100644 index 0000000..d9688fe Binary files /dev/null and b/a2a/assets/diagrams/07_02_debug_strategy.webp differ diff --git a/a2a/assets/diagrams/08_01_current_vs_production.webp b/a2a/assets/diagrams/08_01_current_vs_production.webp new file mode 100644 index 0000000..9f8f6f8 Binary files /dev/null and b/a2a/assets/diagrams/08_01_current_vs_production.webp differ diff --git a/a2a/assets/diagrams/08_02_security_gaps.webp b/a2a/assets/diagrams/08_02_security_gaps.webp new file mode 100644 index 0000000..bccae63 Binary files /dev/null and b/a2a/assets/diagrams/08_02_security_gaps.webp differ diff --git a/a2a/assets/ucp_a2a_demo.gif b/a2a/assets/ucp_a2a_demo.gif deleted file mode 100644 index 05254d1..0000000 Binary files a/a2a/assets/ucp_a2a_demo.gif and /dev/null differ diff --git a/a2a/assets/ucp_a2a_demo.webp b/a2a/assets/ucp_a2a_demo.webp new file mode 100644 index 0000000..6e33b95 Binary files /dev/null and b/a2a/assets/ucp_a2a_demo.webp differ diff --git a/a2a/assets/well_known.png b/a2a/assets/well_known.png deleted file mode 100644 index 51f0840..0000000 Binary files a/a2a/assets/well_known.png and /dev/null differ diff --git a/a2a/assets/well_known.webp b/a2a/assets/well_known.webp new file mode 100644 index 0000000..d3da549 Binary files /dev/null and b/a2a/assets/well_known.webp differ diff --git a/a2a/docs/00-glossary.md b/a2a/docs/00-glossary.md new file mode 100644 index 0000000..4d8806d --- /dev/null +++ b/a2a/docs/00-glossary.md @@ -0,0 +1,157 @@ +# Glossary + +Quick reference for key terms used throughout this documentation. + +## TL;DR + +- **A2A** = How agents talk to each other (discovery + messaging) +- **UCP** = Standard data types for commerce (Checkout, LineItem, Payment) +- **ADK** = Google's framework for building AI agents + +--- + +## Core Concepts + +| Term | Definition | Example in This Sample | +|------|------------|------------------------| +| **A2A** | Agent-to-Agent Protocol - How AI agents discover and communicate with each other | `/.well-known/agent-card.json` endpoint | +| **UCP** | Universal Commerce Protocol - Standard data types for commerce transactions | `Checkout`, `LineItem`, `PaymentInstrument` | +| **ADK** | Agent Development Kit - Google's framework for building agents with tools | `Agent()`, `ToolContext`, `Runner` | +| **Agent** | In this sample: the Cymbal Retail Agent service (includes LLM + tools + state) | The backend running on port 10999 | +| **Tool** | A Python function the LLM can invoke to perform actions | `search_shopping_catalog()`, `add_to_checkout()` | +| **Capability** | A feature set the agent supports, declared in UCP profile | `dev.ucp.shopping.checkout` | +| **Negotiation** | Client and merchant agreeing on shared capabilities before transacting | Happens when first message is sent | + +--- + +## Protocol Terms + +| Term | What It Does | Where to Find It | +|------|--------------|------------------| +| **Agent Card** | JSON file declaring agent identity and capabilities | `/.well-known/agent-card.json` | +| **UCP Profile** | JSON file declaring commerce capabilities and payment handlers | `/.well-known/ucp` | +| **JSON-RPC 2.0** | Message format used for A2A communication | Request/response structure in A2A calls | +| **UCP-Agent Header** | HTTP header containing client's profile URL | Sent with every A2A request | + +--- + +## State Management + +State is stored in ADK's session service (in-memory by default). + +| Key | Purpose | Lifetime | +|-----|---------|----------| +| `user:checkout_id` | Current checkout session ID | Until checkout completed or session expires | +| `__ucp_metadata__` | Negotiated capabilities from client/merchant profiles | Set once per session | +| `__payment_data__` | Payment instrument for current checkout | Set during payment flow | +| `__session_extensions__` | Active A2A extensions for this session | Set once per session | +| `temp:LATEST_TOOL_RESULT` | Temporary storage for last UCP tool response | Cleared after each agent response | + +**Naming conventions**: +- `user:` prefix — User-scoped data (persists across turns) +- `__` prefix — System/internal data (managed by framework) +- `temp:` prefix — Temporary data (cleared after use) + +--- + +## Checkout States + +The checkout follows a 3-state lifecycle: + +
+ Checkout State Lifecycle +

Figure 1: Checkout state transitions from incomplete → ready_for_complete → completed

+
+ +| State | Meaning | What's Needed to Progress | +|-------|---------|---------------------------| +| `incomplete` | Missing required info | Add email, address, or items | +| `ready_for_complete` | Ready for payment | User confirms payment | +| `completed` | Order placed successfully | Terminal state - checkout finalized | + +--- + +## ADK Components + +| Component | Role | File | +|-----------|------|------| +| **Agent** | Orchestrates LLM and tools | `agent.py` | +| **Tool** | Individual function the LLM can call | Defined in `agent.py` | +| **ToolContext** | Provides state access to tools | Passed to each tool function | +| **Runner** | Executes agent with session management | `InMemoryRunner` | +| **Session** | Stores conversation history and state | `InMemorySessionService` | +| **Callback** | Hook to modify tool/agent output | `after_tool_callback`, `after_agent_callback` | + +--- + +## Architecture Layers + +
+ Architecture Layers +

Figure 2: Vertical stack from Chat Client through A2A Server, Agent Executor, ADK Agent, to RetailStore

+
+ +--- + +## Common Acronyms + +| Acronym | Full Name | Context | +|---------|-----------|---------| +| A2A | Agent-to-Agent | Protocol for agent communication | +| UCP | Universal Commerce Protocol | Commerce data standard | +| ADK | Agent Development Kit | Google's agent framework | +| LLM | Large Language Model | Gemini 3.0 Flash in this sample | +| SDK | Software Development Kit | UCP Python SDK | + +--- + +## External Resources + +Official documentation for the core technologies used in this sample. + +### ADK (Agent Development Kit) + +| Resource | URL | +|----------|-----| +| **Official Docs** | [google.github.io/adk-docs](https://google.github.io/adk-docs/) | +| **Getting Started** | [ADK Get Started Guide](https://google.github.io/adk-docs/get-started/) | +| **Agents Guide** | [Building Agents](https://google.github.io/adk-docs/agents/) | +| **GitHub (Python SDK)** | [github.com/google/adk-python](https://github.com/google/adk-python) | +| **Google Cloud Docs** | [Vertex AI Agent Builder](https://docs.cloud.google.com/agent-builder/agent-development-kit/overview) | + +### A2A (Agent-to-Agent Protocol) + +| Resource | URL | +|----------|-----| +| **Official Protocol Site** | [a2a-protocol.org](https://a2a-protocol.org/latest/) | +| **Specification** | [A2A Specification](https://a2a-protocol.org/latest/specification/) | +| **ADK Integration** | [ADK with A2A](https://google.github.io/adk-docs/a2a/) | +| **GitHub Repository** | [github.com/a2aproject/A2A](https://github.com/a2aproject/A2A) | +| **Google Cloud Docs** | [A2A Agents on Cloud Run](https://docs.cloud.google.com/run/docs/ai/a2a-agents) | + +### UCP (Universal Commerce Protocol) + +| Resource | URL | +|----------|-----| +| **Official Site** | [ucp.dev](https://ucp.dev/) | +| **Specification Overview** | [UCP Specification](https://ucp.dev/specification/overview/) | +| **Developer Guide** | [Google Merchant UCP Guide](https://developers.google.com/merchant/ucp) | +| **GitHub Repository** | [github.com/Universal-Commerce-Protocol/ucp](https://github.com/Universal-Commerce-Protocol/ucp) | +| **Python SDK** | [github.com/Universal-Commerce-Protocol/python-sdk](https://github.com/Universal-Commerce-Protocol/python-sdk) | + +### Related Technologies + +| Technology | Documentation | +|------------|---------------| +| **Gemini API** | [ai.google.dev/gemini-api/docs](https://ai.google.dev/gemini-api/docs) | +| **MCP (Model Context Protocol)** | [modelcontextprotocol.io](https://modelcontextprotocol.io/) | +| **JSON-RPC 2.0** | [jsonrpc.org/specification](https://www.jsonrpc.org/specification) | + +--- + +## Related Documentation + +- [Architecture Overview](01-architecture.md) - System components +- [ADK Agent Guide](02-adk-agent.md) - Tool and callback patterns +- [UCP Integration](03-ucp-integration.md) - Capability negotiation +- [Commerce Flows](04-commerce-flows.md) - Checkout state machine diff --git a/a2a/docs/01-architecture.md b/a2a/docs/01-architecture.md new file mode 100644 index 0000000..29c9757 --- /dev/null +++ b/a2a/docs/01-architecture.md @@ -0,0 +1,175 @@ +# System Architecture + +## TL;DR + +- **4 layers**: A2A Server → Agent Executor → ADK Agent → Retail Store +- **2 protocols**: A2A (agent communication) + UCP (commerce data) +- **Request flow**: JSON-RPC → ADK Runner → Tool execution → Response + +## System Overview + +
+ Cymbal Retail Agent System Architecture +

Figure 1: System architecture showing the 4-layer structure — Chat Client (React), A2A Server, ADK Agent Layer, and Business Layer with their components and connections.

+
+ +The architecture follows a clean separation of concerns: + +- **Chat Client (React :3000)** — User interface, A2A messaging client, and CredentialProviderProxy for mock payments +- **Cymbal Retail Agent (Python :10999)** — A2A Starlette server, ADKAgentExecutor bridge, and ProfileResolver for UCP negotiation +- **ADK Layer** — Runner for execution, Agent with 8 shopping tools, and Session service for state +- **Business Layer** — RetailStore for products/checkouts/orders and MockPaymentProcessor for payment simulation + +## Components + +### Backend + +| Component | File | Responsibility | +|-----------|------|----------------| +| A2A Server | `main.py` | HTTP server, routing, static files | +| Agent Executor | `agent_executor.py` | Bridge A2A ↔ ADK, session management | +| Profile Resolver | `ucp_profile_resolver.py` | UCP capability negotiation | +| ADK Agent | `agent.py` | LLM reasoning, tool execution | +| Retail Store | `store.py` | Products, checkouts, orders | +| Payment Processor | `payment_processor.py` | Mock payment handling | + +### Frontend + +| Component | File | Responsibility | +|-----------|------|----------------| +| App | `App.tsx` | State management, A2A messaging | +| ChatMessage | `components/ChatMessage.tsx` | Message rendering | +| Checkout | `components/Checkout.tsx` | Checkout display | +| ProductCard | `components/ProductCard.tsx` | Product cards | +| PaymentMethodSelector | `components/PaymentMethodSelector.tsx` | Payment selection | + +## Request Flow + +
+ Request Flow Sequence Diagram +

Figure 2: Request flow from user query through A2A Server, Agent Executor, ADK Agent, to RetailStore and back. Shows the tool execution loop and callback processing.

+
+ +**Key steps in the request flow:** + +1. **React UI** sends a POST request with JSON-RPC payload and `UCP-Agent` header +2. **A2A Server** routes to the AgentExecutor +3. **AgentExecutor** resolves UCP profile, prepares input, and gets/creates session +4. **ADK Agent** runs via `Runner.run_async()` and executes tools as needed +5. **Tool execution loop** — Agent calls store methods, receives results, triggers `after_tool_callback` +6. **Response path** — `after_agent_callback` processes final response, returns Parts[] to client + +## Layer Responsibilities + +| Layer | Input | Output | Key Class | +|-------|-------|--------|-----------| +| **A2A Server** | HTTP request | HTTP response | `A2AStarletteApplication` | +| **Agent Executor** | A2A context | Event queue | `ADKAgentExecutor` | +| **ADK Agent** | User query + state | Tool results | `Agent` (google.adk) | +| **Retail Store** | Method calls | Domain objects | `RetailStore` | + +## Mock Store Architecture + +### Why a Mock Store? + +The sample uses an in-memory mock store (`store.py`) to demonstrate UCP integration without requiring a real commerce backend. This lets you: + +- **Run standalone** - Zero external dependencies (no database, no API keys beyond Gemini) +- **Learn the patterns** - Understand UCP/ADK integration before connecting real systems +- **Prototype quickly** - Test new features without backend complexity + +### Store Structure + +
+ Mock Store Architecture - What to Keep vs Replace +

Figure 3: Mock store architecture showing the integration layer (keep), mock layer (replace), and your backend implementation. Solid arrows show current data flow; dashed arrows show migration paths.

+
+ +The diagram illustrates the separation between: + +- **Keep These (Integration Layer)** — Agent Tools, RetailStore Methods, UCP Type Generation — these patterns remain the same regardless of backend +- **Replace These (Mock Layer)** — products.json, In-Memory Dict, MockPaymentProcessor — swap these with real implementations +- **Your Backend** — Commerce API (Shopify, Magento), Database, Payment Provider (Stripe, Adyen) + +| Storage | Type | Purpose | +|---------|------|---------| +| `_products` | `dict[str, Product]` | Product catalog (loaded from `products.json`) | +| `_checkouts` | `dict[str, Checkout]` | Active shopping sessions | +| `_orders` | `dict[str, Checkout]` | Completed orders | + +### Key Methods + +| Method | Line | Called By | Purpose | +|--------|------|-----------|---------| +| `search_products()` | 100 | `search_shopping_catalog` tool | Keyword search in catalog | +| `add_to_checkout()` | 186 | `add_to_checkout` tool | Create/update checkout session | +| `get_checkout()` | 244 | `get_checkout` tool | Retrieve current checkout state | +| `start_payment()` | 463 | `start_payment` tool | Validate checkout for payment | +| `place_order()` | 498 | `complete_checkout` tool | Finalize order, generate confirmation | + +### Replacing with Real Backend + +To connect a real commerce platform (Shopify, Magento, custom API): + +**1. Create interface** (recommended for clean separation): + +```python +# interfaces.py +from abc import ABC, abstractmethod + +class IRetailStore(ABC): + @abstractmethod + def search_products(self, query: str) -> ProductResults: ... + + @abstractmethod + def add_to_checkout(self, checkout_id: str | None, product_id: str, + quantity: int, ucp_metadata: UcpMetadata) -> Checkout: ... + + @abstractmethod + def get_checkout(self, checkout_id: str) -> Checkout | None: ... +``` + +**2. Implement adapter** for your platform: + +```python +# shopify_store.py +class ShopifyStore(IRetailStore): + def __init__(self, api_key: str, store_url: str): + self.client = ShopifyClient(api_key, store_url) + + def search_products(self, query: str) -> ProductResults: + shopify_products = self.client.products.search(query) + # Convert to UCP ProductResults format + return ProductResults(results=[...]) +``` + +**3. Swap in agent.py** (line 43): + +```python +# Before +store = RetailStore() + +# After +store = ShopifyStore( + api_key=os.getenv("SHOPIFY_API_KEY"), + store_url=os.getenv("SHOPIFY_STORE_URL") +) +``` + +### What to Keep vs Replace + +| Keep (UCP Patterns) | Replace (Mock Specifics) | +|---------------------|--------------------------| +| Tool function signatures | Data storage layer | +| State management via ToolContext | Product catalog source | +| Checkout type generation | Tax/shipping calculation | +| Response formatting with UCP keys | Payment processing | +| A2A/ADK bridging | Order persistence | + +## Discovery Endpoints + +| Endpoint | Purpose | Source | +|----------|---------|--------| +| `/.well-known/agent-card.json` | A2A agent capabilities | `data/agent_card.json` | +| `/.well-known/ucp` | UCP merchant profile | `data/ucp.json` | +| `/images/*` | Product images | `data/images/` | diff --git a/a2a/docs/02-adk-agent.md b/a2a/docs/02-adk-agent.md new file mode 100644 index 0000000..b8bf9f3 --- /dev/null +++ b/a2a/docs/02-adk-agent.md @@ -0,0 +1,358 @@ +# ADK Agent Patterns + +## TL;DR + +- **Agent**: Gemini 3.0 Flash with 8 shopping tools +- **Tools**: Access `ToolContext` for state, return dict with UCP keys +- **Callbacks**: `after_tool_callback` captures results, `after_agent_callback` formats output + +## Agent Configuration + +```python +# agent.py:437 +root_agent = Agent( + name="shopper_agent", + model="gemini-3-flash-preview", + description="Agent to help with shopping", + instruction="You are a helpful agent who can help user with shopping...", + tools=[ + search_shopping_catalog, + add_to_checkout, + remove_from_checkout, + update_checkout, + get_checkout, + start_payment, + update_customer_details, + complete_checkout, + ], + after_tool_callback=after_tool_modifier, + after_agent_callback=modify_output_after_agent, +) +``` + +## Tool Pattern + +Every tool follows this pattern: + +```python +def tool_function(tool_context: ToolContext, param: str) -> dict: + """Docstring visible to LLM for reasoning.""" + + # 1. Get state + checkout_id = tool_context.state.get(ADK_USER_CHECKOUT_ID) + metadata = tool_context.state.get(ADK_UCP_METADATA_STATE) + + # 2. Validate + if not metadata: + return _create_error_response("Missing UCP metadata") + + # 3. Execute business logic + try: + result = store.method(...) + except ValueError as e: + return _create_error_response(str(e)) + + # 4. Update state if needed + tool_context.state[ADK_USER_CHECKOUT_ID] = result.id + + # 5. Return UCP-formatted response + return {UCP_CHECKOUT_KEY: result.model_dump(mode="json")} +``` + +## All 8 Tools + +| Tool | Line | Purpose | State Access | +|------|------|---------|--------------| +| `search_shopping_catalog` | 51 | Search products | Read metadata | +| `add_to_checkout` | 73 | Add item to cart | Read/write checkout_id | +| `remove_from_checkout` | 115 | Remove item | Read checkout_id | +| `update_checkout` | 151 | Update quantity | Read checkout_id | +| `get_checkout` | 187 | Get current state | Read checkout_id | +| `update_customer_details` | 212 | Set buyer/address | Read checkout_id | +| `start_payment` | 340 | Begin payment flow | Read checkout_id | +| `complete_checkout` | 270 | Finalize order | Read checkout_id, payment | + +## Tool Execution Flow + +
+ ADK Tool Execution Flow +

Figure 1: Tool execution flow from user query through the ADK Agent (LLM + Tool Selection), Tool execution (ToolContext, State, Business Logic), to the data store (Products, Checkouts).

+
+ +The flow illustrates how each tool invocation works: + +- **Input** — User's natural language query enters the system +- **Agent** — Gemini 3.0 Flash reasons about the query and selects the appropriate tool +- **Tool** — ToolContext provides state access, then business logic executes +- **Store** — Products and Checkouts data are queried or modified + +### Multi-Tool Conversation Flow + +In a typical shopping session, multiple tools are called across turns: + +
+ Multi-Tool Shopping Conversation Flow +

Figure 2: A complete shopping conversation showing 4 steps — product search, add to cart, customer details, and payment initiation. Each step involves tool execution and state updates.

+
+ +**The 4-step shopping flow:** + +1. **Product Search** — User asks for cookies → `search_shopping_catalog` returns ProductResults +2. **Add to Cart** — User selects item → `add_to_checkout` creates Checkout (status: incomplete) +3. **Customer Details** — User provides email/address → `update_customer_details` adds buyer + fulfillment +4. **Payment** — User says "checkout" → `start_payment` sets status to ready_for_complete + +## Callbacks + +### Why Callbacks? + +ADK callbacks solve a key problem: **the LLM sees tool results as text, but the frontend needs structured data**. + +Without callbacks: +- Tool returns `{UCP_CHECKOUT_KEY: {...checkout data...}}` +- LLM summarizes: "Added cookies to your cart for $4.99" +- Frontend only sees text, can't render checkout UI + +With callbacks: +- `after_tool_callback` captures the structured data in state +- `after_agent_callback` attaches it to the response as a `data` part +- Frontend receives both text AND structured data for rich UI + +### after_tool_callback + +Captures UCP data from tool results for later use: + +```python +# agent.py:379 +def after_tool_modifier( + tool: BaseTool, + args: dict[str, Any], + tool_context: ToolContext, + tool_response: dict, +) -> dict | None: + """Stores UCP responses in state for output transformation.""" + extensions = tool_context.state.get(ADK_EXTENSIONS_STATE_KEY, []) + ucp_response_keys = [UCP_CHECKOUT_KEY, "a2a.product_results"] + + # Only capture if UCP extension is active + if UcpExtension.URI in extensions and any( + key in tool_response for key in ucp_response_keys + ): + tool_context.state[ADK_LATEST_TOOL_RESULT] = tool_response + + return None # Don't modify the response +``` + +### after_agent_callback + +Transforms agent output to include structured data: + +```python +# agent.py:408 +from google.genai import types + +def modify_output_after_agent( + callback_context: CallbackContext, +) -> types.Content | None: + """Adds UCP data parts to agent's response.""" + latest_result = callback_context.state.get(ADK_LATEST_TOOL_RESULT) + if latest_result: + # Create function response with UCP data + return types.Content( + parts=[ + types.Part( + function_response=types.FunctionResponse( + response={"result": latest_result} + ) + ) + ], + role="model", + ) + return None +``` + +## Session & State Management + +### State Keys + +```python +# constants.py +ADK_USER_CHECKOUT_ID = "user:checkout_id" # Checkout session ID +ADK_PAYMENT_STATE = "__payment_data__" # Payment instrument +ADK_UCP_METADATA_STATE = "__ucp_metadata__" # Capabilities +ADK_EXTENSIONS_STATE_KEY = "__session_extensions__" +ADK_LATEST_TOOL_RESULT = "temp:LATEST_TOOL_RESULT" +``` + +### State Flow + +1. **Request arrives** → Executor builds initial state delta +2. **Tools execute** → Read/write state via `tool_context.state` +3. **Callbacks fire** → Capture results in state +4. **Response sent** → State persisted in session + +## ADK → A2A Bridge + +`ADKAgentExecutor` bridges the protocols: + +```python +# agent_executor.py +class ADKAgentExecutor: + async def execute(self, context, event_queue): + # 1. Activate extensions + self._activate_extensions(context) + + # 2. Prepare UCP metadata + ucp_metadata = UcpRequestProcessor.prepare_ucp_metadata(context) + + # 3. Extract input + query, payment_data = self._prepare_input(context) + + # 4. Build message content + content = types.Content( + role="user", parts=[types.Part.from_text(text=query)] + ) + + # 5. Build state delta + state_delta = self._build_initial_state_delta( + context, ucp_metadata, payment_data + ) + + # 6. Run agent (async iterator) + async for event in self.runner.run_async( + user_id=user_id, + session_id=session_id, + new_message=content, + state_delta=state_delta, + ): + if event.is_final_response(): + # Process final response parts + result_parts = self._process_event(event) + + # 7. Enqueue response + event_queue.enqueue(result_parts) +``` + +## Prompt Engineering + +### Current System Instruction + +The agent uses a single instruction (`agent.py:441-454`): + +```python +instruction=( + "You are a helpful agent who can help user with shopping actions such" + " as searching the catalog, add to checkout session, complete checkout" + " and handle order placed event. Given the user ask, plan ahead and" + " invoke the tools available to complete the user's ask. Always make" + " sure you have completed all aspects of the user's ask. If the user" + " says add to my list or remove from the list, add or remove from the" + " cart, add the product or remove the product from the checkout" + " session. If the user asks to add any items to the checkout session," + " search for the products and then add the matching products to" + " checkout session. If the user asks to replace products," + " use remove_from_checkout and add_to_checkout tools to replace the" + " products to match the user request" +) +``` + +### Improving the Instruction + +For production agents, consider structured prompting with explicit tool ordering and error handling: + +```python +instruction=""" +You are a shopping assistant for Cymbal Retail. Help users find products +and complete purchases. + +TOOLS (use in this order when applicable): +1. search_shopping_catalog - Always search first when user asks for products +2. add_to_checkout - Add items after finding them +3. update_customer_details - Collect email and address before payment +4. start_payment - Begin payment when customer info is complete +5. complete_checkout - Finalize after payment is confirmed + +RULES: +- Always search before adding items (don't guess product IDs) +- Never assume addresses or payment methods - ask the user +- If a tool returns an error, explain it clearly and suggest next steps +- Confirm quantities and prices before proceeding to payment + +ERROR HANDLING: +- "Product not found" → Ask user to clarify product name or show alternatives +- "Missing address" → Politely ask for shipping address +- "Checkout not found" → Help user add items first +- "Payment declined" → Explain and offer to try different payment method +""" +``` + +### Model Configuration + +| Setting | Current Value | Purpose | +|---------|---------------|---------| +| `model` | `gemini-3-flash-preview` | Fast, accurate tool calling | +| `temperature` | Default (not set) | Balanced creativity vs determinism | +| `max_tokens` | Default (not set) | Response length limit | + +**Model Selection Guide:** + +| Model | Best For | Tradeoff | +|-------|----------|----------| +| Gemini 3.0 Flash | Tool-heavy agents (this sample) | Fastest, 99% tool accuracy | +| Gemini 2.0 Pro | Complex reasoning, ambiguous queries | Slower, better nuanced understanding | + +To change the model, edit `agent.py:437`: + +```python +root_agent = Agent( + model="gemini-2-flash", # or other model ID + ... +) +``` + +### Tool Docstring Best Practices + +The LLM uses tool docstrings to decide when to call each tool. Clear docstrings improve tool selection accuracy: + +```python +# GOOD: Clear, specific docstring +def search_shopping_catalog(tool_context: ToolContext, query: str) -> dict: + """Search the product catalog for items matching the query. + + Use this tool when the user asks about products, wants to browse items, + or needs to find something to buy. + + Args: + query: Product name, category, or description to search for. + Examples: "cookies", "chocolate chip", "snacks under $5" + + Returns: + List of matching products with names, prices, and availability. + """ + +# BAD: Vague docstring +def search_shopping_catalog(tool_context: ToolContext, query: str) -> dict: + """Search products.""" # LLM won't know when to use this +``` + +## Adding a New Tool + +1. **Define function** with `ToolContext` parameter: + +```python +def my_new_tool(tool_context: ToolContext, param: str) -> dict: + """Description for LLM reasoning.""" + # Implementation + return {UCP_CHECKOUT_KEY: result.model_dump(mode="json")} +``` + +2. **Add to agent**: + +```python +root_agent = Agent( + ... + tools=[...existing..., my_new_tool], +) +``` + +3. **Update instruction** if needed to guide LLM usage. diff --git a/a2a/docs/03-ucp-integration.md b/a2a/docs/03-ucp-integration.md new file mode 100644 index 0000000..38d002a --- /dev/null +++ b/a2a/docs/03-ucp-integration.md @@ -0,0 +1,195 @@ +# UCP Integration + +## TL;DR + +- **UCP** provides standardized commerce data types (Checkout, LineItem, Payment) +- **Capability negotiation** finds common features between client and merchant +- **Dynamic types** generated based on negotiated capabilities + +## Why Negotiate Capabilities? + +Different commerce platforms support different features. A basic merchant might only support checkout, while an advanced one offers loyalty points, subscriptions, and gift cards. + +**Without negotiation**: +- Client assumes all features available → breaks when merchant lacks support +- Merchant sends all data → client can't render unknown fields +- Tight coupling between specific client and merchant versions + +**With negotiation**: +- Client declares what it supports: "I can handle checkout, fulfillment, discounts" +- Merchant declares what it offers: "I support checkout, fulfillment" +- Intersection becomes the contract: "We'll use checkout + fulfillment" +- Both sides know exactly what data structures to expect + +This enables any UCP-compliant client to work with any UCP-compliant merchant. + +## UCP Capabilities + +| Capability | Purpose | Extends | +|------------|---------|---------| +| `dev.ucp.shopping.checkout` | Base checkout session | - | +| `dev.ucp.shopping.fulfillment` | Shipping address, delivery options | checkout | +| `dev.ucp.shopping.discount` | Promotional codes | checkout | +| `dev.ucp.shopping.buyer_consent` | Consent management | checkout | + +## Profile Structure + +### Merchant Profile (`data/ucp.json`) + +```json +{ + "ucp": { + "version": "2026-01-11", + "services": { + "dev.ucp.shopping": { + "version": "2026-01-11", + "spec": "https://ucp.dev/specs/shopping", + "a2a": { + "endpoint": "http://localhost:10999/.well-known/agent-card.json" + } + } + }, + "capabilities": [ + { + "name": "dev.ucp.shopping.checkout", + "version": "2026-01-11", + "spec": "https://ucp.dev/specs/shopping/checkout", + "schema": "https://ucp.dev/schemas/shopping/checkout.json" + }, + { + "name": "dev.ucp.shopping.fulfillment", + "version": "2026-01-11", + "extends": "dev.ucp.shopping.checkout" + } + ] + }, + "payment": { + "handlers": [{ + "id": "example_payment_provider", + "name": "example.payment.provider", + "version": "2026-01-11" + }] + } +} +``` + +**Note**: Merchant currently supports 2 capabilities (checkout, fulfillment). Client may request more, but negotiation finds the intersection. + +### Client Profile (`chat-client/profile/agent_profile.json`) + +```json +{ + "ucp": { + "version": "2026-01-11", + "capabilities": [ + {"name": "dev.ucp.shopping.checkout"}, + {"name": "dev.ucp.shopping.fulfillment"}, + {"name": "dev.ucp.shopping.discount"}, + {"name": "dev.ucp.shopping.buyer_consent"} + ] + } +} +``` + +## Capability Negotiation + +
+ UCP Capability Negotiation Flow +

Figure 1: UCP capability negotiation flow — Chat Client sends request with UCP-Agent header, ProfileResolver fetches client profile, validates versions, finds common capabilities, and returns UcpMetadata for dynamic type generation.

+
+ +### Negotiation Steps + +1. **Extract header**: Parse `UCP-Agent: profile=""` from request +2. **Fetch profile**: HTTP GET client's profile URL +3. **Validate version**: Client version must be ≤ merchant version +4. **Find intersection**: Common capabilities between client and merchant +5. **Create metadata**: `UcpMetadata` with shared capabilities + +```python +# ucp_profile_resolver.py +def get_ucp_metadata(self, client_profile) -> UcpMetadata: + client_caps = {(c.name, c.version) for c in client_capabilities} + common = [m for m in merchant_caps if (m.name, m.version) in client_caps] + return UcpMetadata(capabilities=common, ...) +``` + +## Dynamic Type Generation + +Based on negotiated capabilities, a custom `Checkout` class is created: + +```python +# helpers/type_generator.py +def get_checkout_type(ucp_metadata: UcpMetadata) -> type[Checkout]: + active = {cap.name for cap in ucp_metadata.capabilities} + bases = [] + + if "dev.ucp.shopping.fulfillment" in active: + bases.append(FulfillmentCheckout) + if "dev.ucp.shopping.buyer_consent" in active: + bases.append(BuyerConsentCheckout) + if "dev.ucp.shopping.discount" in active: + bases.append(DiscountCheckout) + + if not bases: + return Checkout + + return create_model("DynamicCheckout", __base__=tuple(bases)) +``` + +### Type Hierarchy + +
+ UCP Checkout Type Hierarchy +

Figure 2: Checkout type hierarchy — Base Checkout class is extended by FulfillmentCheckout, BuyerConsentCheckout, and DiscountCheckout based on negotiated capabilities. The get_checkout_type() function dynamically combines these at runtime.

+
+ +**Dynamic composition**: If both `fulfillment` and `discount` capabilities are negotiated, `get_checkout_type()` creates a class that inherits from both. + +## UCP SDK Usage + +### Import Patterns + +```python +# Checkout types +from ucp_sdk.models.schemas.shopping.checkout_resp import CheckoutResponse +from ucp_sdk.models.schemas.shopping.checkout import Checkout, FulfillmentCheckout + +# Common types +from ucp_sdk.models.schemas.shopping.types.line_item import LineItem +from ucp_sdk.models.schemas.shopping.types.payment_response import PaymentResponse +from ucp_sdk.models.schemas.shopping.types.order_confirmation import OrderConfirmation + +# Fulfillment +from ucp_sdk.models.schemas.shopping.types.fulfillment_option_response import FulfillmentOptionResponse +from ucp_sdk.models.schemas.shopping.types.postal_address import PostalAddress +``` + +### Response Keys + +```python +# constants.py +UCP_CHECKOUT_KEY = "a2a.ucp.checkout" # Checkout data +UCP_PAYMENT_DATA_KEY = "a2a.ucp.checkout.payment_data" # Payment instrument +UCP_RISK_SIGNALS_KEY = "a2a.ucp.checkout.risk_signals" # Risk data +``` + +## Adding a New Capability + +1. **Update merchant profile** (`data/ucp.json`): +```json +{ + "capabilities": [ + ..., + {"name": "dev.ucp.shopping.new_capability", "extends": "checkout"} + ] +} +``` + +2. **Update type generator** (`helpers/type_generator.py`): +```python +if "dev.ucp.shopping.new_capability" in active: + bases.append(NewCapabilityCheckout) +``` + +3. **Handle in tools** if new fields need processing diff --git a/a2a/docs/04-commerce-flows.md b/a2a/docs/04-commerce-flows.md new file mode 100644 index 0000000..f8c18c0 --- /dev/null +++ b/a2a/docs/04-commerce-flows.md @@ -0,0 +1,201 @@ +# Commerce Flows + +## TL;DR + +- **Checkout states**: `incomplete` → `ready_for_complete` → `completed` +- **Payment flow**: Select method → Get token → Complete checkout +- **Totals**: Recalculated on every change (subtotal, tax, shipping) + +## Why 3 States? + +The checkout state machine prevents common e-commerce errors: + +| State | Purpose | +|-------|---------| +| `incomplete` | Cart mode - freely add/remove items, no commitment yet | +| `ready_for_complete` | Validation gate - all required info collected, price locked | +| `completed` | Finalized - order placed, no modifications possible | + +**Why not just "in cart" and "ordered"?** + +The `ready_for_complete` state serves as a critical checkpoint: +- Validates buyer email exists (for order confirmation) +- Validates shipping address (for fulfillment) +- Locks in pricing (prevents race conditions during payment) +- Allows payment provider to assess risk before charging + +Without this intermediate state, you'd risk creating orders with missing shipping info or charging cards for outdated prices. + +## Checkout State Machine + +
+ Checkout State Machine +

Figure 1: Checkout state machine showing the 3 states — incomplete (cart mode), ready_for_complete (validation gate), and completed (finalized). Transitions are triggered by tool calls: add_to_checkout(), start_payment(), and complete_checkout().

+
+ +### State Definitions + +| State | Meaning | Missing | +|-------|---------|---------| +| `incomplete` | Cart has items but missing info | Buyer email or fulfillment address | +| `ready_for_complete` | All info collected | Awaiting payment confirmation | +| `completed` | Order placed | - | + +### Transition Triggers + +| From | To | Tool | Condition | +|------|----|----- |-----------| +| - | incomplete | `add_to_checkout` | First item added | +| incomplete | ready_for_complete | `start_payment` | Buyer + address present | +| ready_for_complete | completed | `complete_checkout` | Payment validated | + +## Checkout Object Structure + +```json +{ + "id": "checkout-uuid", + "status": "incomplete", + "currency": "USD", + + "line_items": [{ + "id": "item-uuid", + "item": { + "id": "PROD-001", + "title": "Product Name", + "price": 499, + "image_url": "http://localhost:10999/images/product.jpg" + }, + "quantity": 2, + "totals": [{"type": "subtotal", "amount": 998}] + }], + + "totals": [ + {"type": "subtotal", "display_text": "Subtotal", "amount": 998}, + {"type": "tax", "display_text": "Tax (10%)", "amount": 100}, + {"type": "shipping", "display_text": "Shipping", "amount": 500}, + {"type": "total", "display_text": "Total", "amount": 1598} + ], + + "fulfillment": { + "destination": {"address": {...}}, + "groups": [{"selected_method": {...}}] + }, + + "payment": { + "handlers": [{"id": "provider", "name": "example.payment.provider"}] + }, + + "order": { + "id": "order-uuid", + "permalink_url": "https://example.com/order?id=order-uuid" + } +} +``` + +**Note**: Prices are in **cents** (e.g., `499` = $4.99) + +## Payment Flow + +
+ Payment Flow Sequence Diagram +

Figure 2: End-to-end payment flow showing 3 phases — Payment Method Selection (UI ↔ CredentialProviderProxy), Get Payment Token, and Complete Checkout (UI → A2A Server → ADK Agent → MockPaymentProcessor). The agent calls place_order() after payment validation.

+
+ +### Payment Components + +| Component | Location | Role | +|-----------|----------|------| +| CredentialProviderProxy | `chat-client/mocks/` | Mock payment method provider | +| PaymentMethodSelector | `chat-client/components/` | UI for method selection | +| MockPaymentProcessor | `business_agent/payment_processor.py` | Simulates payment validation | + +### PaymentInstrument Structure + +```json +{ + "type": "card", + "last_digits": "1111", + "brand": "AMEX", + "expiry": "12/2026", + "handler_id": "example_payment_provider", + "handler_name": "example.payment.provider", + "credential": { + "type": "card_token", + "token": "mock_token_abc123" + } +} +``` + +## Total Calculation + +`_recalculate_checkout()` in `store.py` updates all totals: + +```python +def _recalculate_checkout(self, checkout: Checkout) -> None: + subtotal = 0 + + # 1. Line item totals + for item in checkout.line_items: + item_total = item.item.price * item.quantity + item.totals = [Total(type="subtotal", amount=item_total)] + subtotal += item_total + + # 2. Tax (10% flat rate, only if address set) + tax = subtotal // 10 if has_fulfillment_address else 0 + + # 3. Shipping (from selected method) + shipping = selected_method.price if selected_method else 0 + + # 4. Grand total + total = subtotal + tax + shipping + + # 5. Update checkout.totals + checkout.totals = [ + Total(type="subtotal", amount=subtotal), + Total(type="tax", amount=tax), + Total(type="shipping", amount=shipping), + Total(type="total", amount=total), + ] +``` + +### Fulfillment Options + +```python +# store.py:525 - _get_fulfillment_options() +[ + FulfillmentOptionResponse( + id="standard", + title="Standard Shipping", + description="Arrives in 4-5 days", + carrier="USPS", + totals=[ + Total(type="subtotal", display_text="Subtotal", amount=500), + Total(type="tax", display_text="Tax", amount=0), + Total(type="total", display_text="Total", amount=500), + ], + ), + FulfillmentOptionResponse( + id="express", + title="Express Shipping", + description="Arrives in 1-2 days", + carrier="FedEx", + totals=[ + Total(type="subtotal", display_text="Subtotal", amount=1000), + Total(type="tax", display_text="Tax", amount=0), + Total(type="total", display_text="Total", amount=1000), + ], + ), +] +``` + +## OrderConfirmation + +Created in `place_order()` when checkout completes (store.py:498): + +```python +checkout.status = "completed" +checkout.order = OrderConfirmation( + id=order_id, + permalink_url=f"https://example.com/order?id={order_id}", +) +``` diff --git a/a2a/docs/05-frontend.md b/a2a/docs/05-frontend.md new file mode 100644 index 0000000..afd4c58 --- /dev/null +++ b/a2a/docs/05-frontend.md @@ -0,0 +1,236 @@ +# Frontend Reference + +## TL;DR + +- **React 19** + TypeScript + Vite + Tailwind +- **App.tsx** handles A2A messaging and state +- **Components** render UCP data types (Checkout, Product, Payment) + +## Why a Mock Payment Provider? + +Real payment flows involve sensitive credentials and complex integrations. The `CredentialProviderProxy` mock lets you: + +- **Test the full checkout flow** without real payment credentials +- **Understand the data contracts** before implementing real providers +- **Demonstrate UCP patterns** without external dependencies + +In production, you'd replace this with your actual payment SDK (Stripe, Adyen, etc.). + +## Request/Response Cycle + +
+ Frontend Request/Response Cycle +

Figure 1: Request/response cycle — React App sends POST /api with UCP-Agent header, Vite Proxy rewrites path and forwards to A2A Server at :10999, ADK Agent returns response with parts[] containing text and structured data (checkout, products).

+
+ +## Component Hierarchy + +
+ React Component Hierarchy +

Figure 2: React component tree — App.tsx manages state and A2A messaging, with Header, ChatMessage, and ChatInput as children. ChatMessage contains UCP data components (green): ProductCard, Checkout, PaymentMethodSelector, and PaymentConfirmation.

+
+ +## App.tsx - State & Handlers + +### State + +```typescript +const [messages, setMessages] = useState([initialMessage]); +const [isLoading, setIsLoading] = useState(false); +const [contextId, setContextId] = useState(null); +const [taskId, setTaskId] = useState(null); +``` + +### Handler Functions + +| Handler | Purpose | +|---------|---------| +| `handleSendMessage(content, options)` | Send A2A message, parse response | +| `handleAddToCheckout(product)` | Add product to cart | +| `handleStartPayment()` | Initiate payment flow | +| `handlePaymentMethodSelection(checkout)` | Fetch available methods | +| `handlePaymentMethodSelected(method)` | Get payment token | +| `handleConfirmPayment(instrument)` | Complete checkout | + +## A2A Communication + +### Request Format + +```typescript +const request = { + jsonrpc: "2.0", + id: crypto.randomUUID(), + method: "message/send", + params: { + message: { + role: "user", + parts: [{type: "text", text: "show me cookies"}], + contextId: contextId, // From previous response + taskId: taskId, // For multi-turn tasks + }, + configuration: { historyLength: 0 } + } +}; + +fetch("/api", { + method: "POST", + headers: { + "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"` + }, + body: JSON.stringify(request) +}); +``` + +### Response Parsing + +```typescript +const data = await response.json(); + +// Extract context for next request +setContextId(data.result?.contextId); + +// Parse response parts +for (const part of data.result?.status?.message?.parts || []) { + if (part.text) { + message.text += part.text; + } + if (part.data?.["a2a.product_results"]) { + message.products = part.data["a2a.product_results"].results; + } + if (part.data?.["a2a.ucp.checkout"]) { + message.checkout = part.data["a2a.ucp.checkout"]; + } +} +``` + +## Key Components + +| Component | Props | Renders | +|-----------|-------|---------| +| `ProductCard` | `product`, `onAddToCart` | Product image, name, price, stock | +| `Checkout` | `checkout`, `onCheckout`, `onCompletePayment` | Line items, totals, action buttons | +| `PaymentMethodSelector` | `paymentMethods`, `onSelect` | Radio list of methods | +| `PaymentConfirmation` | `paymentInstrument`, `onConfirm` | Confirm button | +| `ChatMessage` | `message`, handlers | Combines all above based on data | + +## Types (types.ts) + +```typescript +interface ChatMessage { + id: string; + sender: Sender; // USER | MODEL + text: string; + products?: Product[]; + isLoading?: boolean; + paymentMethods?: PaymentMethod[]; + isUserAction?: boolean; + checkout?: Checkout; + paymentInstrument?: PaymentInstrument; +} + +interface Checkout { + id: string; + line_items: CheckoutItem[]; + currency: string; + continue_url?: string | null; + status: string; // incomplete | ready_for_complete | completed + totals: CheckoutTotal[]; + order_id?: string; + order_permalink_url?: string; + payment?: Payment; +} + +interface Product { + productID: string; + name: string; + image: string[]; + brand: { name: string }; + offers: { + price: string; + priceCurrency: string; + availability: string; + }; + url: string; + description: string; + size: { name: string }; +} + +interface PaymentMethod { + id: string; + type: string; + brand: string; + last_digits: string; + expiry_month: number; + expiry_year: number; +} +``` + +## CredentialProviderProxy + +Mock payment provider in `mocks/credentialProviderProxy.ts`: + +```typescript +class CredentialProviderProxy { + handler_id = 'example_payment_provider'; + handler_name = 'example.payment.provider'; + + // Returns mock payment methods (wrapped in object) + async getSupportedPaymentMethods( + user_email: string, + config: any + ): Promise<{payment_method_aliases: PaymentMethod[]}> { + return { + payment_method_aliases: [ + { id: "instr_1", type: "card", brand: "amex", + last_digits: "1111", expiry_month: 12, expiry_year: 2026 }, + { id: "instr_2", type: "card", brand: "visa", + last_digits: "8888", expiry_month: 12, expiry_year: 2026 }, + ] + }; + } + + // Converts method to PaymentInstrument with token + async getPaymentToken( + user_email: string, + payment_method_id: string + ): Promise { + return { + ...payment_method, + handler_id: this.handler_id, + handler_name: this.handler_name, + credential: { type: "token", token: `mock_token_${uuid}` } + }; + } +} +``` + +## Configuration + +### Vite Proxy (`vite.config.ts`) + +```typescript +server: { + port: 3000, + proxy: { + "/api": { + target: "http://localhost:10999", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, "") + } + } +} +``` + +### App Config (`config.ts`) + +```typescript +export const appConfig = new AppProperties( + "Business Agent", + "Your personal shopping assistant.", + "/images/logo.jpg", + "Hello, I am your Business Agent...", + "Shop with Business Agent" +); +``` diff --git a/a2a/docs/06-extending.md b/a2a/docs/06-extending.md new file mode 100644 index 0000000..33dfcdf --- /dev/null +++ b/a2a/docs/06-extending.md @@ -0,0 +1,509 @@ +# Extending the Sample + +## TL;DR + +- **Quick wins**: Add products, modify checkout flow, change tax/shipping +- **Medium effort**: Add new tools, custom payment handlers +- **Advanced**: New UCP capabilities, replace mock store, multi-agent patterns + +## Which Extension Do You Need? + +
+ Extension Decision Tree +

Figure 1: Extension decision tree — Choose your path based on what you want to add. New products require only JSON edits, while new capabilities need profile updates, type generation, and tool handling.

+
+ +--- + +## Part 1: Quick Customizations + +### Add Products + +**File**: `data/products.json` + +```json +{ + "productID": "SNACK-007", + "sku": "SNACK-007", + "name": "Organic Trail Mix", + "@type": "Product", + "image": ["http://localhost:10999/images/trail_mix.jpg"], + "brand": {"name": "Nature's Best", "@type": "Brand"}, + "offers": { + "price": "6.99", + "priceCurrency": "USD", + "availability": "InStock", + "itemCondition": "NewCondition", + "@type": "Offer" + }, + "description": "A healthy mix of nuts and dried fruits", + "category": "Food > Snacks > Trail Mix" +} +``` + +Add image to `data/images/`, restart server. + +### Modify Tax Calculation + +**File**: `store.py` - `_recalculate_checkout()` method + +```python +# Current: flat 10% tax +tax = subtotal // 10 + +# Custom: location-based tax +tax_rate = self._get_tax_rate(checkout.fulfillment.destination.address) +tax = int(subtotal * tax_rate) + +def _get_tax_rate(self, address: PostalAddress) -> float: + state_rates = {"CA": 0.0725, "NY": 0.08, "TX": 0.0625} + return state_rates.get(address.addressRegion, 0.05) +``` + +### Add Minimum Order + +**File**: `store.py` - `start_payment()` method (line 463) + +```python +def start_payment(self, checkout_id: str) -> Checkout | str: + checkout = self._checkouts.get(checkout_id) + + # Add minimum order check + subtotal = next(t for t in checkout.totals if t.type == "subtotal").amount + if subtotal < 1000: # $10 minimum (cents) + return "Minimum order is $10.00" + + # ... rest of validation +``` + +### Custom Shipping Options + +**File**: `store.py` - `_get_fulfillment_options()` method (line 525) + +```python +def _get_fulfillment_options(self, address: PostalAddress) -> list: + options = [ + FulfillmentOptionResponse( + id="standard", title="Standard", price=500, + description="4-5 business days" + ), + FulfillmentOptionResponse( + id="express", title="Express", price=1000, + description="1-2 business days" + ), + ] + # Add same-day for local addresses + if self._is_local(address): + options.append(FulfillmentOptionResponse( + id="same_day", title="Same Day", price=1500 + )) + return options +``` + +--- + +## Part 2: Adding Tools (ADK Perspective) + +### Tool Architecture + +Every ADK tool follows this pattern: + +
+ Tool Execution Pattern +

Figure 2: Tool execution pattern — User query flows through Agent → LLM (tool selection) → Tool (with ToolContext) → Store. Each tool follows: get state → validate → execute → return UCP response.

+
+ +### Template: Creating a New Tool + +```python +# agent.py + +def my_new_tool(tool_context: ToolContext, param: str) -> dict: + """Docstring becomes the LLM's understanding of this tool. + + Args: + param: Description helps LLM know what to pass + + Returns: + Description of what the tool returns + """ + # 1. Get state + checkout_id = tool_context.state.get(ADK_USER_CHECKOUT_ID) + metadata = tool_context.state.get(ADK_UCP_METADATA_STATE) + + # 2. Validate + if not checkout_id: + return _create_error_response("No active checkout") + + # 3. Execute business logic + try: + result = store.my_method(checkout_id, param) + except ValueError as e: + return _create_error_response(str(e)) + + # 4. Return UCP-formatted response + return {UCP_CHECKOUT_KEY: result.model_dump(mode="json")} +``` + +### Example: Apply Discount Tool + +```python +def apply_discount(tool_context: ToolContext, promo_code: str) -> dict: + """Apply a promotional code to the current checkout. + + Args: + promo_code: The promotional code to apply (e.g., "SAVE10") + """ + checkout_id = tool_context.state.get(ADK_USER_CHECKOUT_ID) + if not checkout_id: + return _create_error_response("No active checkout") + + try: + checkout = store.apply_discount(checkout_id, promo_code) + return {UCP_CHECKOUT_KEY: checkout.model_dump(mode="json")} + except ValueError as e: + return _create_error_response(str(e)) + +# Add to agent tools list +root_agent = Agent(..., tools=[...existing..., apply_discount]) +``` + +### Example: Order Tracking Tool + +```python +def get_order_status(tool_context: ToolContext, order_id: str) -> dict: + """Get the status of a placed order. + + Args: + order_id: The order ID from order confirmation + """ + order = store.get_order(order_id) + if not order: + return _create_error_response("Order not found") + + return { + "order": { + "id": order.order.id, + "status": "shipped", # or "processing", "delivered" + "tracking_number": "1Z999AA10123456784", + "estimated_delivery": "2026-01-25", + "permalink_url": order.order.permalink_url, + } + } +``` + +### Example: Product Recommendations Tool + +```python +def get_recommendations( + tool_context: ToolContext, + rec_type: str = "popular" +) -> dict: + """Get product recommendations for the customer. + + Args: + rec_type: Type of recommendations - "popular", "similar", "cart_based" + """ + checkout_id = tool_context.state.get(ADK_USER_CHECKOUT_ID) + + if rec_type == "cart_based" and checkout_id: + checkout = store.get_checkout(checkout_id) + product_ids = [item.item.id for item in checkout.line_items] + products = store.get_related_products(product_ids) + elif rec_type == "popular": + products = store.get_popular_products(limit=4) + else: + products = store.search_products("").results[:4] + + return {"a2a.product_results": {"results": products}} +``` + +--- + +## Part 3: UCP Capabilities + +### Why Capabilities? + +UCP capabilities let you extend checkout data in a standardized way. The client and merchant negotiate which capabilities they both support. + +
+ Capability Extension Hierarchy +

Figure 3: Capability extension hierarchy — Base Checkout class extended by FulfillmentCheckout, DiscountCheckout (existing), and LoyaltyCheckout, WishlistCheckout (new capabilities you can add).

+
+ +### Adding a New Capability + +**Step 1**: Update merchant profile (`data/ucp.json`) + +```json +{ + "capabilities": [ + ...existing..., + { + "name": "dev.ucp.shopping.loyalty", + "version": "2026-01-11", + "extends": "dev.ucp.shopping.checkout" + } + ] +} +``` + +**Step 2**: Create checkout type extension + +```python +# helpers/type_generator.py or models.py + +from pydantic import BaseModel + +class Reward(BaseModel): + id: str + name: str + points_required: int + +class LoyaltyCheckout(Checkout): + loyalty_points: int | None = None + rewards: list[Reward] | None = None +``` + +**Step 3**: Update type generator (`helpers/type_generator.py`) + +```python +def get_checkout_type(ucp_metadata: UcpMetadata) -> type[Checkout]: + active = {cap.name for cap in ucp_metadata.capabilities} + bases = [] + + if "dev.ucp.shopping.fulfillment" in active: + bases.append(FulfillmentCheckout) + if "dev.ucp.shopping.loyalty" in active: # NEW + bases.append(LoyaltyCheckout) + # ... other capabilities + + if not bases: + return Checkout + return create_model("DynamicCheckout", __base__=tuple(bases)) +``` + +**Step 4**: Handle in tools (if needed) + +```python +def apply_loyalty_points(tool_context: ToolContext, points: int) -> dict: + """Apply loyalty points to reduce checkout total.""" + checkout_id = tool_context.state.get(ADK_USER_CHECKOUT_ID) + checkout = store.apply_loyalty_points(checkout_id, points) + return {UCP_CHECKOUT_KEY: checkout.model_dump(mode="json")} +``` + +### Example: Wishlist Capability + +```python +# 1. Profile: {"name": "dev.ucp.shopping.wishlist", "extends": "checkout"} + +# 2. Type +class WishlistCheckout(Checkout): + wishlist_items: list[str] | None = None # Product IDs + +# 3. Tool +def add_to_wishlist(tool_context: ToolContext, product_id: str) -> dict: + """Save a product to the customer's wishlist.""" + wishlist = store.add_to_wishlist(user_id, product_id) + return {"wishlist": wishlist} + +def move_to_checkout(tool_context: ToolContext, product_id: str) -> dict: + """Move a wishlist item to the checkout.""" + # ... +``` + +--- + +## Part 4: Payment Customization + +### Payment Flow with Custom Handler + +
+ Custom Payment Flow +

Figure 4: Custom payment flow — Replace CredentialProviderProxy with your payment provider and MockPaymentProcessor with your StripeProcessor (or other provider). Shows the complete flow from payment method selection through Stripe API to OrderConfirmation.

+
+ +### Step 1: Update Profiles + +**Merchant** (`data/ucp.json`): +```json +{ + "payment": { + "handlers": [{ + "id": "stripe_handler", + "name": "stripe.payment.provider", + "version": "2026-01-11", + "config": {"business_id": "acct_123456"} + }] + } +} +``` + +**Client** (`chat-client/profile/agent_profile.json`): +```json +{ + "payment": { + "handlers": [{ + "id": "stripe_handler", + "name": "stripe.payment.provider" + }] + } +} +``` + +### Step 2: Implement Payment Processor + +```python +# payment_processor.py + +import stripe + +class StripePaymentProcessor: + def __init__(self, api_key: str): + stripe.api_key = api_key + + async def process_payment( + self, + payment_data: PaymentInstrument, + amount: int, + currency: str, + risk_data: dict | None = None + ) -> Task: + try: + result = stripe.PaymentIntent.create( + amount=amount, + currency=currency.lower(), + payment_method=payment_data.credential.token, + confirm=True + ) + return Task( + state=TaskState.completed + if result.status == "succeeded" + else TaskState.failed + ) + except stripe.error.CardError as e: + return Task(state=TaskState.failed, message=str(e)) +``` + +### Step 3: Update Frontend Mock + +Replace `CredentialProviderProxy` in `chat-client/mocks/`: + +```typescript +class StripeCredentialProvider { + handler_id = 'stripe_handler'; + handler_name = 'stripe.payment.provider'; + + async getSupportedPaymentMethods(email: string) { + // Call your payment service to get saved methods + const response = await fetch(`/api/payment-methods?email=${email}`); + return response.json(); + } + + async getPaymentToken(email: string, method_id: string) { + // Generate Stripe token + const { token } = await stripe.createToken(card); + return { + ...method, + credential: { type: "token", token: token.id } + }; + } +} +``` + +--- + +## Part 5: User Journeys + +### Journey: Order Tracking + +**User Goal**: "Where's my order?" + +``` +User: "Where's my order?" +→ Agent: Asks for order ID or shows recent orders +User: "Order #ORD-12345" +→ Agent: get_order_status("ORD-12345") +→ Agent: "Your order shipped via FedEx. Tracking: 1Z999..." +User: "Can I change the delivery address?" +→ Agent: Checks if order is shipped +→ Agent: update_delivery() if not shipped, else "Sorry, already shipped" +``` + +**Tools needed**: `get_order_status`, `get_recent_orders`, `update_delivery` + +### Journey: Returns & Refunds + +**User Goal**: "I want to return this item" + +``` +User: "I want to return the cookies I ordered" +→ Agent: get_recent_orders() - finds order with cookies +→ Agent: "Found cookies in order #ORD-12345. Why the return?" +User: "They arrived damaged" +→ Agent: initiate_return(order_id, item_id, reason="damaged") +→ Agent: "Return approved. Shipping label sent to your email. + Refund of $4.99 will process when we receive the item." +``` + +**Tools needed**: `get_recent_orders`, `initiate_return`, `get_return_status` + +### Journey: Smart Recommendations + +**User Goal**: Discover related products + +``` +User: Adds cookies to cart +→ Agent: After add_to_checkout callback +→ Agent: get_recommendations("cart_based") +→ Agent: "Customers who bought these cookies also liked: + - Organic Milk ($3.99) + - Hot Cocoa Mix ($5.99)" +User: "Show me similar cookies" +→ Agent: get_recommendations("similar", product_id="COOKIE-001") +→ Agent: Shows 4 similar cookie products +``` + +**Tools needed**: `get_recommendations` (with types: popular, similar, cart_based) + +### Journey: Guest vs Returning Customer + +**Guest User**: +``` +User: Adds items, enters email: new@example.com +→ Agent: No saved addresses, asks for full address +→ Agent: No saved payment methods, shows all options +``` + +**Returning Customer**: +``` +User: Adds items, enters email: returning@example.com +→ Agent: get_saved_addresses(email) +→ Agent: "Ship to 123 Main St (your default)?" +User: "Yes" +→ Agent: get_saved_payment_methods(email) +→ Agent: "Pay with Visa ending 4242?" +``` + +**Tools needed**: `get_saved_addresses`, `get_saved_payment_methods` + +--- + +## Part 6: Replacing the Mock Store + +See [Architecture: Mock Store](./01-architecture.md#mock-store-architecture) for: +- Store structure diagram +- Key methods to implement +- Interface definition +- Adapter pattern example +- What to keep vs replace + +Quick summary: + +| Keep (UCP/ADK patterns) | Replace (Mock specifics) | +|-------------------------|--------------------------| +| Tool signatures | Data storage | +| State management | Product catalog | +| Type generation | Tax/shipping logic | +| Response formatting | Payment processing | diff --git a/a2a/docs/07-testing-guide.md b/a2a/docs/07-testing-guide.md new file mode 100644 index 0000000..0516b35 --- /dev/null +++ b/a2a/docs/07-testing-guide.md @@ -0,0 +1,265 @@ +# Testing & Development Guide + +## TL;DR + +- Start backend (port 10999), then frontend (port 3000) +- Happy path: search → add to cart → checkout → pay +- Debug with verbose logging and endpoint verification + +## Setup Flow + +
+ Setup and Test Flow +

Figure 1: Setup and test flow — Start Backend (port 10999), then Frontend (port 3000), verify endpoints, then proceed to testing: Search Products → Add to Cart → Complete Payment.

+
+ +## Quick Start + +### 1. Start Backend + +```bash +cd a2a/business_agent +uv sync +cp env.example .env # Add GOOGLE_API_KEY +uv run business_agent # Starts on :10999 +``` + +### 2. Start Frontend + +```bash +cd a2a/chat-client +npm install +npm run dev # Starts on :3000 +``` + +### 3. Verify Endpoints + +```bash +# Agent card (A2A discovery) +curl -s http://localhost:10999/.well-known/agent-card.json | jq . + +# UCP profile (merchant capabilities) +curl -s http://localhost:10999/.well-known/ucp | jq . + +# Client profile (client capabilities) +curl -s http://localhost:3000/profile/agent_profile.json | jq . +``` + +## Testing Workflows + +### Happy Path (Complete Purchase) + +1. Open http://localhost:3000 +2. Type "show me cookies" +3. Click "Add to Checkout" on a product +4. Enter email when prompted: `test@example.com` +5. Enter address: `123 Main St, San Francisco, CA 94105` +6. Click "Complete Payment" +7. Select a payment method +8. Click "Confirm Purchase" +9. Verify order confirmation appears with order ID and permalink + +**Expected State Transitions**: +- After step 3: `status: "incomplete"` +- After step 5: `status: "incomplete"` (ready for payment start) +- After step 6: `status: "ready_for_complete"` +- After step 8: `status: "completed"` + +### Error Scenarios + +| Scenario | How to Test | Expected Behavior | +|----------|-------------|-------------------| +| No checkout exists | Call `get_checkout` without adding items | "Checkout not created" error | +| Missing address | Skip address, call `start_payment` | Agent prompts for address | +| Missing email | Skip email, call `start_payment` | Agent prompts for email | +| Invalid product | `add_to_checkout("INVALID-ID", 1)` | "Product not found" error | +| Quantity update | Add item, then `update_checkout` with qty=0 | Item removed from checkout | + +## Debugging Guide + +### Debug Strategy + +When something breaks, follow this systematic approach: + +
+ Debug Strategy Decision Tree +

Figure 2: Debug strategy decision tree — Systematically check backend, frontend, browser console, UCP-Agent header, and capability negotiation to isolate issues.

+
+ +### Enable Verbose Logging + +```python +# main.py - add at top +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +### Inspect Tool State + +```python +# In any tool - add temporarily for debugging +def my_tool(tool_context: ToolContext, param: str) -> dict: + print("=== DEBUG ===") + print("State keys:", list(tool_context.state.keys())) + print("Checkout ID:", tool_context.state.get(ADK_USER_CHECKOUT_ID)) + print("UCP Metadata:", tool_context.state.get(ADK_UCP_METADATA_STATE)) + # ... rest of tool +``` + +### Check A2A Messages + +```typescript +// App.tsx - in handleSendMessage, add before fetch +console.log("Request:", JSON.stringify(request, null, 2)); + +// After response +console.log("Response:", JSON.stringify(data, null, 2)); +``` + +### Test A2A Directly (bypass UI) + +```bash +curl -X POST http://localhost:10999/ \ + -H "Content-Type: application/json" \ + -H "UCP-Agent: profile=\"http://localhost:3000/profile/agent_profile.json\"" \ + -d '{ + "jsonrpc": "2.0", + "id": "1", + "method": "message/send", + "params": { + "message": { + "role": "user", + "parts": [{"type": "text", "text": "show me products"}] + } + } + }' +``` + +## Common Issues + +| Issue | Likely Cause | Fix | +|-------|--------------|-----| +| Server won't start | Missing `GOOGLE_API_KEY` | Add key to `.env` file | +| "Profile fetch failed" | Frontend not running | Start chat-client on :3000 | +| "Version unsupported" | Profile version mismatch | Align `version` in both `ucp.json` and `agent_profile.json` | +| "Checkout not found" | Session expired or no items | Call `add_to_checkout` first | +| UI not updating | Missing contextId | Check `contextId` in response, ensure it's passed to next request | +| "Missing UCP metadata" | Header not sent | Verify `UCP-Agent` header in request | +| Payment methods empty | CredentialProviderProxy issue | Check browser console for mock provider errors | + +## Troubleshooting Guide + +### Setup Failures + +| Error Message | Cause | Solution | +|---------------|-------|----------| +| `Address already in use :10999` | Agent already running or port in use | `kill $(lsof -t -i:10999)` or use different port | +| `GOOGLE_API_KEY not found` | Missing or empty .env file | Create `.env` from `env.example`, add your key | +| `No module named 'business_agent'` | Not in virtualenv or deps not installed | Run `uv sync` in `business_agent/` directory | +| `npm ERR! ENOENT package.json` | Wrong directory | `cd chat-client` before running `npm install` | +| `Connection refused :10999` | Backend not running | Start backend first with `uv run business_agent` | + +### Runtime Errors + +| Symptom | Debug Steps | +|---------|-------------| +| **"Checkout not found"** | 1. Check `contextId` is passed from previous response
2. Verify session hasn't expired
3. Add an item first with `add_to_checkout` | +| **Products not returning** | 1. Enable DEBUG logging
2. Check if `search_shopping_catalog` tool is being called
3. Verify products.json exists and is valid JSON | +| **Payment flow hangs** | 1. Check browser console for CredentialProviderProxy errors
2. Verify mock payment methods are returned
3. Check `start_payment` was called successfully | +| **UI not updating after action** | 1. Verify `contextId` threading in App.tsx
2. Check response structure in browser DevTools
3. Look for React state update issues | + +### Step-by-Step Diagnosis + +**Problem: Agent returns generic errors** + +```bash +# Step 1: Enable verbose logging +# Edit main.py, add at top: +import logging +logging.basicConfig(level=logging.DEBUG) + +# Step 2: Restart agent and check logs +uv run business_agent + +# Step 3: Look for specific error messages in output +``` + +**Problem: Capability negotiation failing** + +```bash +# Step 1: Verify both profiles are accessible +curl -s http://localhost:10999/.well-known/ucp | jq .version +curl -s http://localhost:3000/profile/agent_profile.json | jq .version + +# Step 2: Ensure versions match +# Both should return the same version string + +# Step 3: Check UCP-Agent header is being sent +# In browser DevTools > Network, look for requests to localhost:10999 +# Verify UCP-Agent header contains profile URL +``` + +**Problem: Tools not being called by LLM** + +```python +# Check 1: Tool is registered +# In agent.py, verify tool is in tools=[] list + +# Check 2: Tool docstring is clear +# LLM needs clear description to know when to use the tool +@tool +def my_tool(tool_context: ToolContext, query: str) -> dict: + """Search for products matching the query. + + Args: + query: Product name or description to search for + """ + # ... + +# Check 3: Add debug output to confirm tool is called +def my_tool(tool_context: ToolContext, query: str) -> dict: + print(f"=== TOOL CALLED: my_tool({query}) ===") + # ... +``` + +### Browser Debugging + +**Check Network Requests:** +1. Open DevTools (F12) → Network tab +2. Filter by "localhost:10999" +3. Click on request → Headers tab +4. Verify `UCP-Agent` header is present +5. Click Response tab to see A2A response + +**Check Console Errors:** +1. Open DevTools → Console tab +2. Look for red error messages +3. Common issues: + - CORS errors → Backend not running + - JSON parse errors → Malformed response + - TypeError → Missing data in response + +## Reference + +### Ports & URLs + +| Service | Port | Endpoints | +|---------|------|-----------| +| Backend | 10999 | `/` (A2A), `/.well-known/agent-card.json`, `/.well-known/ucp` | +| Frontend | 3000 | `/`, `/profile/agent_profile.json` | + +### Environment Variables + +| Variable | Required | Purpose | +|----------|----------|---------| +| `GOOGLE_API_KEY` | Yes | Gemini API access for LLM | + +### Key Files for Debugging + +| Symptom | Check This File | What to Look For | +|---------|-----------------|------------------| +| Tool not called | `agent.py` | Tool in `tools=[]` list | +| State issues | `constants.py` | State key names | +| Checkout errors | `store.py` | State machine logic | +| UCP negotiation | `ucp_profile_resolver.py` | Version/capability matching | +| Frontend errors | `App.tsx` | Request/response handling | diff --git a/a2a/docs/08-production-notes.md b/a2a/docs/08-production-notes.md new file mode 100644 index 0000000..e7da6f2 --- /dev/null +++ b/a2a/docs/08-production-notes.md @@ -0,0 +1,260 @@ +# Production Notes + +> **WARNING**: This sample is NOT production-ready. This document explains what changes are needed before deploying to production. + +## TL;DR + +- In-memory state loses data on restart +- No authentication or rate limiting +- No health check endpoints +- See deployment checklist below + +--- + +## Architecture Limitations + +
+ Current vs Production Architecture +

Figure 1: In-memory sample components vs production-ready replacements

+
+ +| Component | Current State | Production Requirement | +|-----------|---------------|------------------------| +| Session Storage | `InMemorySessionService` (lost on restart) | Redis or database | +| Task Storage | `InMemoryTaskStore` (lost on restart) | Persistent task store | +| Checkout Storage | `RetailStore._checkouts` dict (in-memory) | PostgreSQL or similar | +| Order Storage | `RetailStore._orders` dict (in-memory) | PostgreSQL or similar | +| Concurrency | No locking on `_checkouts` | Checkout-level locks | +| Authentication | None (trusts `context_id`) | JWT or API key validation | +| Secrets | Plaintext .env | Secret Manager (GCP/AWS) | + +**Key insight**: The `RetailStore` class (in `store.py`) stores all business data in plain Python dictionaries: +- `self._checkouts = {}` — All active checkout sessions +- `self._orders = {}` — All completed orders +- `self._products` — Product catalog (loaded from JSON) + +These are lost on restart and have no concurrency protection. + +--- + +## Security Gaps + +
+ Security Gaps Overview +

Figure 2: Four critical security vulnerabilities and their required fixes

+
+ +### 1. No Authentication + +Any client can claim any `context_id`. In production, validate user identity from JWTs or API keys. + +```python +# Current (insecure) +user_id = context.context_id # Trusts client-provided value + +# Production +user_id = validate_jwt(context.headers.get("Authorization")) +``` + +### 2. Profile URL Not Validated + +The `UCP-Agent` header can point to any URL. A malicious client could point to: +- Internal services (SSRF attack) +- Slow servers (DoS the agent startup) + +```python +# Production: Whitelist allowed domains +ALLOWED_DOMAINS = ["localhost", "trusted-partner.com"] +parsed = urlparse(client_profile_url) +if parsed.netloc not in ALLOWED_DOMAINS: + raise ValueError("Untrusted profile URL") +``` + +### 3. No Rate Limiting + +Unlimited requests are accepted. Add rate limiting per user/IP. + +### 4. Payment Data Unencrypted + +Payment instruments are stored as plaintext in session state. If logs capture state, card data leaks. + +```python +# Production: Never log payment data +logger.info(f"Processing payment for checkout {checkout_id}") +# NOT: logger.info(f"Payment data: {payment_data}") +``` + +--- + +## Concurrency Issues + +### Race Condition in Checkout + +Between `start_payment()` and `complete_checkout()`, another request could: +- Add items (changing the total) +- Change the address (affecting tax) +- Call `start_payment()` again + +**Fix**: Add checkout-level locking. + +```python +# Production pattern +async def add_to_checkout(checkout_id: str, ...): + async with self.get_lock(checkout_id): + # Safe from concurrent modifications + checkout = self._checkouts[checkout_id] + checkout.line_items.append(...) +``` + +### Duplicate Items on Retry + +If a network error causes the client to retry `add_to_checkout`, items are duplicated. + +**Fix**: Implement idempotency via client-provided keys. + +```python +def add_to_checkout( + tool_context: ToolContext, + product_id: str, + quantity: int, + idempotency_key: str | None = None # Client-provided +) -> dict: + if idempotency_key and idempotency_key in self.processed_requests: + return self.processed_requests[idempotency_key] + # ... proceed with add ... +``` + +--- + +## Deployment Checklist + +### Required Before Production + +**Infrastructure:** +- [ ] Create Dockerfile with non-root user +- [ ] Add `/health` endpoint (liveness probe) +- [ ] Add `/ready` endpoint (readiness probe) +- [ ] Configure Kubernetes manifests or Cloud Run + +**State Management:** +- [ ] Replace `InMemorySessionService` with Redis-backed store +- [ ] Replace `InMemoryTaskStore` with persistent store +- [ ] Replace `RetailStore._checkouts` dict with database (PostgreSQL) +- [ ] Replace `RetailStore._orders` dict with database +- [ ] Add session TTL and cleanup + +**Security:** +- [ ] Add request authentication (JWT/API key) +- [ ] Validate profile URLs against whitelist +- [ ] Move `GOOGLE_API_KEY` to Secret Manager +- [ ] Add rate limiting per user + +**Observability:** +- [ ] Add structured JSON logging +- [ ] Add Prometheus metrics endpoint +- [ ] Add request tracing (OpenTelemetry) + +**Reliability:** +- [ ] Add checkout-level locking +- [ ] Implement idempotency for state-changing operations +- [ ] Add circuit breaker for Gemini API calls + +--- + +## Scaling Considerations + +### Session Affinity + +If running multiple replicas, all requests for a session must go to the same instance (or use shared storage). + +```yaml +# Kubernetes: Enable session affinity +spec: + sessionAffinity: ClientIP +``` + +### LLM Call Blocking + +Each `runner.run_async()` call blocks until the LLM responds (2-5 seconds typical). With synchronous execution, you need one thread per concurrent user. + +**Consider**: Async runner with connection pooling. + +### Gemini API Rate Limits + +The Gemini API has rate limits. Add a circuit breaker to fail fast when limits are hit. + +--- + +## Sample Dockerfile + +```dockerfile +FROM python:3.13-slim + +# Create non-root user +RUN useradd -m agent + +WORKDIR /app + +# Copy files +COPY --chown=agent:agent . . + +# Install dependencies +RUN pip install uv && uv sync + +# Switch to non-root user +USER agent + +# Expose port +EXPOSE 10999 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD curl -f http://localhost:10999/health || exit 1 + +# Run +CMD ["uv", "run", "business_agent"] +``` + +--- + +## Sample Health Check Endpoint + +Add to `main.py`: + +```python +@app.route("/health", methods=["GET"]) +async def health(request: Request) -> JSONResponse: + return JSONResponse({"status": "healthy"}) + +@app.route("/ready", methods=["GET"]) +async def ready(request: Request) -> JSONResponse: + # Check dependencies here + try: + # Verify store is accessible + _ = store.products + return JSONResponse({"status": "ready"}) + except Exception as e: + return JSONResponse({"status": "not_ready", "error": str(e)}, status_code=503) +``` + +--- + +## Environment Variables + +Externalize these hardcoded values: + +| Variable | Current | Purpose | +|----------|---------|---------| +| `AGENT_HOST` | `localhost` | Bind address | +| `AGENT_PORT` | `10999` | Listen port | +| `LOG_LEVEL` | `INFO` | Logging verbosity | +| `GOOGLE_API_KEY` | `.env` file | Gemini API access | +| `SESSION_TTL` | Unlimited | Session expiration (seconds) | +| `REDIS_URL` | N/A | Distributed session storage | + +--- + +## Related Documentation + +- [Architecture](01-architecture.md) - System design +- [Testing Guide](07-testing-guide.md) - Debugging and troubleshooting