Example: Commerce Assistant
A complete commerce chat with a product catalog tool, approval-gated add_to_cart, cart state management, a ToolApprovalSurface, and custom renderers for product carousels and order summaries.
File structure
lib/
commerceClient.ts -- chat client setup
commerceTools.ts -- tool definitions with cart callback
components/
CommerceChat.tsx -- React chat component
ProductCarousel.tsx -- custom product carousel renderer
OrderSummary.tsx -- custom order summary renderer
1. Client tools with cart callback
// lib/commerceTools.ts
import { defineClientTool, type ClientTool } from 'gecx-chat';
export interface CartLine {
productId: string;
name: string;
quantity: number;
price: string;
}
export function createCommerceTools(onAddToCart: (line: CartLine) => void) {
const searchProducts = defineClientTool({
name: 'search_products',
description: 'Search the product catalog by keyword',
inputSchema: {
type: 'object',
required: ['query'],
properties: {
query: { type: 'string' },
maxResults: { type: 'integer', minimum: 1, maximum: 20 },
},
},
execute: async (input) => {
const { query, maxResults } = input as { query: string; maxResults?: number };
const res = await fetch(`/api/products/search?q=${encodeURIComponent(query)}&limit=${maxResults ?? 6}`);
return res.json();
},
});
const addToCart = defineClientTool({
name: 'add_to_cart',
description: 'Add a product to the shopping cart',
inputSchema: {
type: 'object',
required: ['productId'],
properties: {
productId: { type: 'string' },
quantity: { type: 'integer', minimum: 1 },
},
},
permissions: { requiresUserApproval: true },
execute: async (input) => {
const { productId, quantity = 1 } = input as { productId: string; quantity?: number };
const res = await fetch(`/api/cart/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, quantity }),
});
const result = await res.json();
// Notify the host app so it can update cart UI outside the chat
onAddToCart({
productId,
name: result.productName,
quantity,
price: result.lineTotal,
});
return result;
},
});
return [searchProducts, addToCart];
}
2. Chat client
// lib/commerceClient.ts
import {
createChatClient,
tokenEndpointAuth,
createProxyTransport,
type ClientTool,
} from 'gecx-chat';
export function createCommerceClient(tools: ClientTool[]) {
return createChatClient({
auth: tokenEndpointAuth({ endpoint: '/api/chat/token' }),
transport: createProxyTransport({ endpoint: '/api/chat/proxy' }),
tools,
environment: 'production',
});
}
3. Custom renderers
// components/ProductCarousel.tsx
import type { ProductCarouselPart } from 'gecx-chat';
export function ProductCarousel({ products }: ProductCarouselPart) {
return (
<div className="product-carousel" style={{ display: 'flex', gap: '1rem', overflowX: 'auto' }}>
{products.map((product) => (
<article key={product.id} className="product-card" style={{ minWidth: 200 }}>
{product.imageUrl && (
<img src={product.imageUrl} alt={product.title} style={{ width: '100%' }} />
)}
<h3>{product.title}</h3>
{product.description && <p>{product.description}</p>}
{product.price && <strong>{product.price}</strong>}
{product.url && (
<a href={product.url} target="_blank" rel="noopener noreferrer">
View
</a>
)}
</article>
))}
</div>
);
}
// components/OrderSummary.tsx
import type { OrderSummaryPart } from 'gecx-chat';
export function OrderSummary({ orderId, items, subtotal, tax, shipping, total, status }: OrderSummaryPart) {
return (
<div className="order-summary">
<h3>Order {orderId}</h3>
{status && <span className="order-status">{status}</span>}
<table>
<thead>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.name}>
<td>{item.name}</td>
<td>{item.quantity}</td>
<td>{item.price}</td>
</tr>
))}
</tbody>
<tfoot>
<tr><td colSpan={2}>Subtotal</td><td>{subtotal}</td></tr>
{tax && <tr><td colSpan={2}>Tax</td><td>{tax}</td></tr>}
{shipping && <tr><td colSpan={2}>Shipping</td><td>{shipping}</td></tr>}
<tr><td colSpan={2}><strong>Total</strong></td><td><strong>{total}</strong></td></tr>
</tfoot>
</table>
</div>
);
}
4. React component with tool approval
// components/CommerceChat.tsx
'use client';
import { useState, useMemo, useCallback } from 'react';
import {
ChatProvider,
useChatSession,
MessageList,
Composer,
ToolApprovalSurface,
} from 'gecx-chat/react';
import type { RendererMap, ToolApprovalHandler } from 'gecx-chat/react';
import { createCommerceClient } from '@/lib/commerceClient';
import { createCommerceTools, type CartLine } from '@/lib/commerceTools';
import { ProductCarousel } from './ProductCarousel';
import { OrderSummary } from './OrderSummary';
const renderers: RendererMap = {
'product-carousel': (part) => <ProductCarousel {...part} />,
'order-summary': (part) => <OrderSummary {...part} />,
};
function CommerceChatInner() {
// Tool approval state
const [pendingApproval, setPendingApproval] = useState<{
toolName: string;
resolve: (approved: boolean) => void;
} | null>(null);
const approvalHandler: ToolApprovalHandler = useCallback(
(toolName, _input) =>
new Promise<boolean>((resolve) => {
setPendingApproval({ toolName, resolve });
}),
[],
);
const {
messages,
status,
isStreaming,
canSend,
input,
send,
stop,
setApprovalHandler,
} = useChatSession({
onError: (err) => console.error('[CommerceChat]', err),
});
// Register the approval handler once the session is ready
useMemo(() => {
setApprovalHandler(approvalHandler);
}, [setApprovalHandler, approvalHandler]);
const handleSend = useCallback(
(text: string) => { send(text); },
[send],
);
return (
<>
<MessageList messages={messages} emptyState={<p>What are you looking for?</p>} />
{isStreaming && <p className="typing-indicator">Thinking...</p>}
{/* Tool approval dialog */}
<ToolApprovalSurface
toolName={pendingApproval?.toolName ?? ''}
open={pendingApproval !== null}
onApprove={() => {
pendingApproval?.resolve(true);
setPendingApproval(null);
}}
onDeny={() => {
pendingApproval?.resolve(false);
setPendingApproval(null);
}}
/>
<Composer
input={input}
canSend={canSend}
isStreaming={isStreaming}
onSend={handleSend}
onStop={stop}
placeholder="Ask about products..."
/>
</>
);
}
export default function CommerceChat() {
const [cart, setCart] = useState<CartLine[]>([]);
const handleAddToCart = useCallback((line: CartLine) => {
setCart((prev) => [...prev, line]);
}, []);
const tools = useMemo(() => createCommerceTools(handleAddToCart), [handleAddToCart]);
const client = useMemo(() => createCommerceClient(tools), [tools]);
return (
<ChatProvider client={client} renderers={renderers}>
{/* Cart badge outside the chat */}
{cart.length > 0 && (
<div className="cart-badge">
Cart: {cart.reduce((n, l) => n + l.quantity, 0)} items
</div>
)}
<CommerceChatInner />
</ChatProvider>
);
}
How tool approval works
add_to_cartis defined withpermissions: { requiresUserApproval: true }.- When the agent calls
add_to_cart, the SDK pauses execution and invokes theToolApprovalHandler. - The handler sets React state that opens the
ToolApprovalSurfacedialog. - The user clicks Approve or Deny. The handler's promise resolves accordingly.
- On approval the tool's
executefunction runs and fires theonAddToCartcallback, updating the host app's cart state outside the chat widget.
What to adapt
- Product search: Replace the
/api/products/searchfetch with your catalog API. - Cart API: Replace
/api/cart/addwith your real cart service. - Renderers: The
ProductCarouselandOrderSummarycomponents are unstyled starting points. Add your design system classes or swap in your component library. - Approval UX: The
ToolApprovalSurfaceis a minimal dialog. Wrap it in a modal from your UI framework and show the tool input (product name, quantity, price) so the user knows what they are approving. - Cart state: This example lifts cart state into the parent component. In production you would likely push it into a Zustand/Redux store or server-side session.
Going further
- Production-quality reference. Applied AI Retail is the production-style retail demo where the whole journey — browse → detail → add → cart → checkout → returns — lives inside one chat panel. Use it as the next-step reference when this example feels too small.
- A2UI catalog gift-bundle moment. The applied retail demo's gift-bundle flow uses A2UI generative UI to compose a multi-step gift recommendation surface. See A2UI Catalog for the 20 vetted catalog components installable via
gecx add ui:<name>(includingconfirmation-receipt,coupon-applier,quantity-stepper). - Commerce outcomes. Call
chat.trackPurchase({ orderId, revenue, currency, items?, paymentMethod? })at the order-confirmation point. The event flows through<GmvBreakdown>ingecx-chat/dashboards. - PCI / PII fail-closed. Configure
governance.commercein strict mode so card and SSN literals throwCOMMERCE_PII_LEAKrather than landing in transcripts. Wiregecx validateinto CI to catch them statically. - Triage with
agentGraph. When a single system prompt struggles to handle returns, billing, and order tracking, split into specialists. Seeapps/applied-ai-retail/src/lib/supportGraph.tsfor the reference and the Agent Graph guide.
Source:
docs/examples/commerce-assistant.md