A stockbroker showing human in the loop with LangGraph
Overview
A stock trading assistant built with LangGraph that demonstrates human-in-the-loop workflows. The assistant can research stocks, analyze market data, and prepare trades, but requires explicit user approval before executing any transactions. This pattern is essential for applications where AI assists with high-stakes decisions.
Features
- Multi-step Workflows: Complex operations broken into LangGraph nodes
- Human-in-the-Loop: Critical actions require user confirmation
- State Persistence: Workflow state maintained across interactions
- Tool Integration: Connected to stock market APIs
- Approval UI: Clear interface for reviewing and approving actions
- Audit Trail: Complete history of AI decisions and user approvals
How It Works
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Research │────▶│ Analyze │────▶│ Prepare │
│ Stock │ │ Data │ │ Trade │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────────┐
│ Human Approval │
│ (Required) │
└─────────────────┘
│
▼
┌─────────────────┐
│ Execute Trade │
└─────────────────┘- User asks about a stock (e.g., "Should I buy AAPL?")
- AI researches the stock and analyzes market data
- AI prepares a trade recommendation
- Workflow pauses for user approval
- User reviews and approves/rejects the trade
- If approved, trade executes
Integration
Uses @assistant-ui/react-langgraph for LangGraph integration:
import { useLangGraphRuntime } from "@assistant-ui/react-langgraph";
import { AssistantRuntimeProvider, Thread } from "@assistant-ui/react";
import { Client } from "@langchain/langgraph-sdk";
const langGraphClient = new Client({
apiUrl: process.env.LANGGRAPH_API_URL,
});
export default function StockbrokerChat() {
const runtime = useLangGraphRuntime({
threadId,
stream: async (messages, { abortSignal }) => {
return langGraphClient.runs.stream(
threadId,
"stockbroker-assistant",
{
input: { messages },
streamMode: "messages",
},
{ signal: abortSignal }
);
},
onSwitchToNewThread: async () => {
const { thread_id } = await langGraphClient.threads.create();
return thread_id;
},
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
</AssistantRuntimeProvider>
);
}Human-in-the-Loop Pattern
In your LangGraph workflow, define an interrupt point:
from langgraph.graph import StateGraph
from langgraph.checkpoint import MemorySaver
def prepare_trade(state):
# Prepare trade details
return {"trade": trade_details, "needs_approval": True}
def execute_trade(state):
# Execute the approved trade
return {"result": "Trade executed"}
# Graph with interrupt
graph = StateGraph(State)
graph.add_node("prepare", prepare_trade)
graph.add_node("execute", execute_trade)
# Interrupt before execute for human approval
graph.add_edge("prepare", "execute")
graph.set_interrupt_before(["execute"])Approval UI Component
const TradeApproval = makeAssistantToolUI({
toolName: "execute_trade",
render: ({ args, status, addResult }) => {
if (status === "requires_action") {
return (
<div className="rounded-lg border p-4">
<h3>Trade Approval Required</h3>
<p>Buy {args.shares} shares of {args.symbol}</p>
<div className="flex gap-2">
<Button onClick={() => addResult({ approved: true })}>
Approve
</Button>
<Button onClick={() => addResult({ approved: false })}>
Reject
</Button>
</div>
</div>
);
}
return <p>Trade {status}...</p>;
},
});