# Part 3: Approval UI URL: /docs/runtimes/langgraph/tutorial/part-3 Add human-in-the-loop approval for tool calls. *** title: "Part 3: Approval UI" description: Add human-in-the-loop approval for tool calls. ----------------------------------------------------------- ## Background: LangGraph implementation details import Image from "next/image"; import { InstallCommand } from "@/components/docs/fumadocs/install/install-command"; import approval from "./images/stockbroker-langgraph.png"; LangChain LangGraph Our LangGraph backend interrupts the `purchase_stock` tool execution in order to ensure the user confirms the purchase. The user confirms the purchase by submitting a tool message with the `approve` field set to `true`. ```ts title="assistant-ui-stockbroker/backend/src/index.ts" {6,18-19,32-35} const purchaseApproval = async (state: typeof GraphAnnotation.State) => { const { messages } = state; const lastMessage = messages[messages.length - 1]; if (!(lastMessage instanceof ToolMessage)) { // Interrupt the node to request permission to execute the purchase. throw new NodeInterrupt("Please confirm the purchase before executing."); } }; const shouldExecutePurchase = (state: typeof GraphAnnotation.State) => { const { messages } = state; const lastMessage = messages[messages.length - 1]; if (!(lastMessage instanceof ToolMessage)) { // Interrupt the node to request permission to execute the purchase. throw new NodeInterrupt("Please confirm the purchase before executing."); } const { approve } = JSON.parse(lastMessage.content as string); return approve ? "execute_purchase" : "agent"; }; const workflow = new StateGraph(GraphAnnotation) .addNode("agent", callModel) .addEdge(START, "agent") .addNode("tools", toolNode) .addNode("prepare_purchase_details", preparePurchaseDetails) .addNode("purchase_approval", purchaseApproval) .addNode("execute_purchase", executePurchase) .addEdge("prepare_purchase_details", "purchase_approval") .addEdge("execute_purchase", END) .addEdge("tools", "agent") .addConditionalEdges("purchase_approval", shouldExecutePurchase, [ "agent", "execute_purchase", ]) .addConditionalEdges("agent", shouldContinue, [ "tools", END, "prepare_purchase_details", ]); ``` ## Add approval UI We create a new file under `/components/tools/purchase-stock/PurchaseStockTool.tsx` to define the tool. First, we define the tool arguments and result types: ```ts title="@/components/tools/purchase-stock/PurchaseStockTool.tsx" type PurchaseStockArgs = { ticker: string; companyName: string; quantity: number; maxPurchasePrice: number; }; type PurchaseStockResult = { approve?: boolean; cancelled?: boolean; error?: string; }; ``` Then we use `makeAssistantToolUI` to define the tool UI: ```tsx title="@/components/tools/purchase-stock/PurchaseStockTool.tsx" "use client"; import { TransactionConfirmationPending } from "./transaction-confirmation-pending"; import { TransactionConfirmationFinal } from "./transaction-confirmation-final"; import { makeAssistantToolUI } from "@assistant-ui/react"; import { updateState } from "@/lib/chatApi"; export const PurchaseStockTool = makeAssistantToolUI( { toolName: "purchase_stock", render: function PurchaseStockUI({ args, result, status, addResult }) { const handleReject = async () => { addResult({ approve: false }); }; const handleConfirm = async () => { addResult({ approve: true }); }; return (
              purchase_stock({JSON.stringify(args)})
            
{!result && status.type !== "running" && ( )}
); }, }, ); ``` Finally, we add a `TransactionConfirmationPending` component to ask for approval. This requires shadcn/ui's `Card` and `Button` components. We will install them as a dependency. Then create a new file under `/components/tools/purchase-stock/transaction-confirmation-pending.tsx` to define the approval UI. ```tsx title="@/components/tools/purchase-stock/transaction-confirmation-pending.tsx" "use client"; import { CheckIcon, XIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; type TransactionConfirmation = { ticker: string; companyName: string; quantity: number; maxPurchasePrice: number; onConfirm: () => void; onReject: () => void; }; export function TransactionConfirmationPending(props: TransactionConfirmation) { const { ticker, companyName, quantity, maxPurchasePrice, onConfirm, onReject, } = props; return ( Confirm Transaction

Ticker:

{ticker}

Company:

{companyName}

Quantity:

{quantity} shares

Max Purchase Price:

${maxPurchasePrice?.toFixed(2)}

Total Maximum Cost:

${(quantity * maxPurchasePrice)?.toFixed(2)}

); } ``` ### Bind approval UI ```tsx title="@/app/page.tsx" {1,8} import { PurchaseStockTool } from "@/components/tools/purchase-stock/PurchaseStockTool"; export default function Home() { return (
); } ``` ### Try it out! Ask the assistant to buy 5 shares of Tesla. You should see the following appear: import purchase from "./images/acme-approve.png"; Approval UI ## Add `TransactionConfirmationFinal` to show approval result We will add a component to display the approval result. ```ts title="@/components/tools/purchase-stock/transaction-confirmation-final.tsx" "use client"; import { CheckCircle } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; type TransactionConfirmation = { ticker: string; companyName: string; quantity: number; maxPurchasePrice: number; }; export function TransactionConfirmationFinal(props: TransactionConfirmation) { const { ticker, companyName, quantity, maxPurchasePrice } = props; return ( Transaction Confirmed

Purchase Summary

Ticker:

{ticker}

Company:

{companyName}

Quantity:

{quantity} shares

Price per Share:

${maxPurchasePrice?.toFixed(2)}

Total Cost:

${(quantity * maxPurchasePrice)?.toFixed(2)}

Your purchase of {quantity} shares of {companyName} ({ticker}) has been successfully processed.

); } ``` ### Update `PurchaseStockTool` We will import the new `` component and use it in the `render` function whenever an approval result is available. ```tsx title="@/components/tools/purchase-stock/PurchaseStockTool.tsx" {3,25-30,37-42} "use client"; import { TransactionConfirmationPending } from "./transaction-confirmation-pending"; import { TransactionConfirmationFinal } from "./transaction-confirmation-final"; import { makeAssistantToolUI } from "@assistant-ui/react"; import { updateState } from "@/lib/chatApi"; type PurchaseStockArgs = { ticker: string; companyName: string; quantity: number; maxPurchasePrice: number; }; type PurchaseStockResult = { approve?: boolean; cancelled?: boolean; error?: string; }; export const PurchaseStockTool = makeAssistantToolUI( { toolName: "purchase_stock", render: function PurchaseStockUI({ args, result, status, addResult }) { let resultObj: PurchaseStockResult; try { resultObj = result ? JSON.parse(result) : {}; } catch (e) { resultObj = { error: result! }; } const handleReject = () => { addResult({ cancelled: true }); }; const handleConfirm = async () => { addResult({ approve: true }); }; return (
              purchase_stock({JSON.stringify(args)})
            
{!result && status.type !== "running" && ( )} {resultObj.approve && } {resultObj.approve === false && (
User rejected purchase
)} {resultObj.cancelled && (
Cancelled
)}
); }, }, ); ``` ### Try it out! Confirm the purchase of shares. You should see the approval confimration UI appear. import purchase2 from "./images/acme-confirmed.png"; Approval result