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.
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
- 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 } from "@assistant-ui/react";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { Client } from "@langchain/langgraph-sdk";
const langGraphClient = new Client({
apiUrl: process.env.LANGGRAPH_API_URL,
});
export default function StockbrokerChat() {
const runtime = useLangGraphRuntime({
stream: async (messages, { abortSignal, initialize }) => {
const { externalId } = await initialize();
if (!externalId) throw new Error("Thread not found");
return langGraphClient.runs.stream(
externalId,
"stockbroker-assistant",
{
input: { messages },
streamMode: "messages",
},
{ signal: abortSignal }
);
},
create: async () => {
const { thread_id } = await langGraphClient.threads.create();
return { externalId: 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.memory 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")
app = graph.compile(interrupt_before=["execute"], checkpointer=MemorySaver())Approval UI Component
const toolkit = defineToolkit({
execute_trade: {
type: "backend",
render: ({ args, status, addResult }) => {
if (status.type === "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.type}...</p>;
},
},
});