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

  1. add_to_cart is defined with permissions: { requiresUserApproval: true }.
  2. When the agent calls add_to_cart, the SDK pauses execution and invokes the ToolApprovalHandler.
  3. The handler sets React state that opens the ToolApprovalSurface dialog.
  4. The user clicks Approve or Deny. The handler's promise resolves accordingly.
  5. On approval the tool's execute function runs and fires the onAddToCart callback, updating the host app's cart state outside the chat widget.

What to adapt

  • Product search: Replace the /api/products/search fetch with your catalog API.
  • Cart API: Replace /api/cart/add with your real cart service.
  • Renderers: The ProductCarousel and OrderSummary components are unstyled starting points. Add your design system classes or swap in your component library.
  • Approval UX: The ToolApprovalSurface is 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> (including confirmation-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> in gecx-chat/dashboards.
  • PCI / PII fail-closed. Configure governance.commerce in strict mode so card and SSN literals throw COMMERCE_PII_LEAK rather than landing in transcripts. Wire gecx validate into CI to catch them statically.
  • Triage with agentGraph. When a single system prompt struggles to handle returns, billing, and order tracking, split into specialists. See apps/applied-ai-retail/src/lib/supportGraph.ts for the reference and the Agent Graph guide.
Source: docs/examples/commerce-assistant.md