import { StateGraph, START, END, Command } from "@langchain/langgraph";import { HumanMessage } from "@langchain/core/messages";import { ChatAnthropic } from "@langchain/anthropic";const llm = new ChatAnthropic({ model: "claude-sonnet-4-5-20250929" });async function readEmail(state: EmailAgentStateType) {// Extract and parse email content// In production, this would connect to your email serviceconsole.log(`Processing email: ${state.emailContent}`);return {};}async function classifyIntent(state: EmailAgentStateType) {// Use LLM to classify email intent and urgency, then route accordingly// Create structured LLM that returns EmailClassification objectconst structuredLlm = llm.withStructuredOutput(EmailClassificationSchema);// Format the prompt on-demand, not stored in stateconst classificationPrompt = `Analyze this customer email and classify it:Email: ${state.emailContent}From: ${state.senderEmail}Provide classification including intent, urgency, topic, and summary.`;// Get structured response directly as objectconst classification = await structuredLlm.invoke(classificationPrompt);// Determine next node based on classificationlet nextNode: "searchDocumentation" | "humanReview" | "draftResponse" | "bugTracking";if (classification.intent === "billing" || classification.urgency === "critical") { nextNode = "humanReview";} else if (classification.intent === "question" || classification.intent === "feature") { nextNode = "searchDocumentation";} else if (classification.intent === "bug") { nextNode = "bugTracking";} else { nextNode = "draftResponse";}// Store classification as a single object in statereturn new Command({ update: { classification }, goto: nextNode,});}
搜索和跟踪节点
复制
向 AI 提问
async function searchDocumentation(state: EmailAgentStateType) {// Search knowledge base for relevant information// Build search query from classificationconst classification = state.classification!;const query = `${classification.intent} ${classification.topic}`;let searchResults: string[];try { // Implement your search logic here // Store raw search results, not formatted text searchResults = [ "Reset password via Settings > Security > Change Password", "Password must be at least 12 characters", "Include uppercase, lowercase, numbers, and symbols", ];} catch (error) { // For recoverable search errors, store error and continue searchResults = [`Search temporarily unavailable: ${error}`];}return new Command({ update: { searchResults }, // Store raw results or error goto: "draftResponse",});}async function bugTracking(state: EmailAgentStateType) {// Create or update bug tracking ticket// Create ticket in your bug tracking systemconst ticketId = "BUG-12345"; // Would be created via APIreturn new Command({ update: { searchResults: [`Bug ticket ${ticketId} created`] }, goto: "draftResponse",});}
响应节点
复制
向 AI 提问
import { Command, interrupt } from "@langchain/langgraph";async function draftResponse(state: EmailAgentStateType) {// Generate response using context and route based on qualityconst classification = state.classification!;// Format context from raw state data on-demandconst contextSections: string[] = [];if (state.searchResults) { // Format search results for the prompt const formattedDocs = state.searchResults.map(doc => `- ${doc}`).join("\n"); contextSections.push(`Relevant documentation:\n${formattedDocs}`);}if (state.customerHistory) { // Format customer data for the prompt contextSections.push(`Customer tier: ${state.customerHistory.tier ?? "standard"}`);}// Build the prompt with formatted contextconst draftPrompt = `Draft a response to this customer email:${state.emailContent}Email intent: ${classification.intent}Urgency level: ${classification.urgency}${contextSections.join("\n\n")}Guidelines:- Be professional and helpful- Address their specific concern- Use the provided documentation when relevant`;const response = await llm.invoke([new HumanMessage(draftPrompt)]);// Determine if human review needed based on urgency and intentconst needsReview = ( classification.urgency === "high" || classification.urgency === "critical" || classification.intent === "complex");// Route to appropriate next nodeconst nextNode = needsReview ? "humanReview" : "sendReply";return new Command({ update: { responseText: response.content.toString() }, // Store only the raw response goto: nextNode,});}async function humanReview(state: EmailAgentStateType) {// Pause for human review using interrupt and route based on decisionconst classification = state.classification!;// interrupt() must come first - any code before it will re-run on resumeconst humanDecision = interrupt({ emailId: state.emailId, originalEmail: state.emailContent, draftResponse: state.responseText, urgency: classification.urgency, intent: classification.intent, action: "Please review and approve/edit this response",});// Now process the human's decisionif (humanDecision.approved) { return new Command({ update: { responseText: humanDecision.editedResponse || state.responseText }, goto: "sendReply", });} else { // Rejection means human will handle directly return new Command({ update: {}, goto: END });}}async function sendReply(state: EmailAgentStateType): Promise<{}> {// Send the email response// Integrate with email serviceconsole.log(`Sending reply: ${state.responseText!.substring(0, 100)}...`);return {};}
// Test with an urgent billing issueconst initialState: EmailAgentStateType = { emailContent: "I was charged twice for my subscription! This is urgent!", senderEmail: "customer@example.com", emailId: "email_123"};// Run with a thread_id for persistenceconst config = { configurable: { thread_id: "customer_123" } };const result = await app.invoke(initialState, config);// The graph will pause at human_reviewconsole.log(`Draft ready for review: ${result.responseText?.substring(0, 100)}...`);// When ready, provide human input to resumeimport { Command } from "@langchain/langgraph";const humanResponse = new Command({ resume: { approved: true, editedResponse: "We sincerely apologize for the double charge. I've initiated an immediate refund...", }});// Resume executionconst finalResult = await app.invoke(humanResponse, config);console.log("Email sent successfully!");